From 409c88411bec62d0a899e94e1a8b9f94ba1cf6f2 Mon Sep 17 00:00:00 2001 From: Jaret Arnold <96366172+musharna@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:41:07 -0400 Subject: [PATCH] privacy: the tracked config, corpus and docs carry no real paths or private names The 2026-08-19 scrub genericised projects.yaml; the 08-31 cwd-identity work put the real roots back, plus a 250-row export of the production job DB as a test corpus, a plan doc with an ssh line to the broker's tailnet address, and a collaborator's name in a path. A scrub is a state, not a commit, and nothing was checking the state. - config/projects.local.yaml (gitignored, beside projects.yaml) now carries machine-specific roots and private names; each entry replaces the same-named tracked entry wholesale. load_local_projects is a no-op when the file is absent, so existing deployments start unchanged -- but they lose cwd-derived identity for the real roots until the file is created (docs/projects-yaml.md). - projects.yaml, tests/data/project_cwd_corpus.csv, tests, plans, docs and docstrings are pseudonymised: /home/user, greek-letter project names, the broker address as a hostname. Every name variant a test relied on was mapped by substring, so folding/variant behaviour is unchanged (1296 passed). - tests/test_no_private_paths.py scans every tracked file for private paths, hosts, addresses and session trailers, with a planted-hit positive control. --- .gitignore | 1 + CHANGELOG.md | 33 +- config/projects.yaml | 46 +- docs/plans/2026-06-30-submit-cwd-probe.md | 9 - .../2026-08-31-cwd-project-identity-design.md | 34 +- .../2026-08-31-cwd-project-identity-plan.md | 174 +++--- docs/projects-yaml.md | 53 +- scripts/export_project_cwd_corpus.py | 2 +- src/job_cli/cli.py | 2 +- src/jobd/broker/routes/config.py | 8 +- src/jobd/config.py | 45 +- tests/data/project_cwd_corpus.csv | 500 +++++++++--------- tests/test_cli.py | 14 +- tests/test_corpus_replay.py | 24 +- tests/test_no_private_paths.py | 65 +++ tests/test_projects_yaml.py | 60 ++- tests/test_submit_resolve_agreement.py | 6 +- tests/unit/conftest.py | 8 +- tests/unit/test_cwd_identity_event.py | 8 +- tests/unit/test_cwd_identity_resolution.py | 36 +- tests/unit/test_cwd_identity_resolve_api.py | 16 +- .../test_list_filter_matches_either_name.py | 12 +- tests/unit/test_project_from_cwd.py | 86 +-- 23 files changed, 692 insertions(+), 550 deletions(-) create mode 100644 tests/test_no_private_paths.py diff --git a/.gitignore b/.gitignore index 6c7df81..610f9e0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ htmlcov/ # Subagent-driven-development scratch: per-plan ledgers, briefs, review packages. .superpowers/ +\n\n# Machine-specific project roots/names; the tracked projects.yaml stays generic.\nconfig/projects.local.yaml\n \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2349e..5d1c474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,22 +2,33 @@ All notable changes to jobd. Format roughly follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [Unreleased]\n\n### Privacy — tracked config no longer carries real paths + +- `config/projects.local.yaml` (gitignored, beside `projects.yaml`) now holds + machine-specific `roots:` and private project names; each entry replaces the + same-named tracked entry wholesale. Operators must create it on deploy or lose + cwd-derived identity for those roots — see docs/projects-yaml.md. +- The tracked `projects.yaml`, the replay corpus `tests/data/project_cwd_corpus.csv`, + tests, plans and docs are pseudonymised (`/home/user/...`, greek-letter project + names). `tests/test_no_private_paths.py` fails the suite if a private path, host, + address or session trailer is ever tracked again. + ## [Unreleased] ## [0.5.42] — 2026-09-01 ### Added -- **The cwd-derived project identity feature is no longer inert: `agrigen`, `jepagame`, - `orchid-sdxl`, and `dreamer-chassis` now declare `roots:` in `config/projects.yaml`.** +- **The cwd-derived project identity feature is no longer inert: `alpha`, `beta`, + `gamma`, and `delta` now declare `roots:` in `config/projects.yaml`.** The matching machinery and its acceptance gate against 3,608 rows of real job history (`tests/test_corpus_replay.py`) landed first, but no project had ever declared a root, so a job typed with an unregistered run label (`pillar2a1_sweep`, `arf-promoter`, - `orchid-sdxl-stage4b`, ...) still fell through to `_default` regardless. Each root was + `gamma-stage4b`, ...) still fell through to `_default` regardless. Each root was picked by replaying `tests/data/project_cwd_corpus.csv` grouped by cwd and keeping only directories where every project name ever typed from them reads as a variant of one - project — deliberately excluding `/home/mjarnold/trellis2` and `.../hunyuan3d` - (>=97% agrigen-typed in the corpus, trellis2 the single largest cwd in it) because both + project — deliberately excluding `/home/user/trellis2` and `.../hunyuan3d` + (>=97% alpha-typed in the corpus, trellis2 the single largest cwd in it) because both are generic third-party tool checkouts a differently-owned future clone could silently inherit. `docs/projects-yaml.md` gains a `roots:` section covering the schema, the three-rule resolution order (an explicit, registered `--project` always wins over cwd — @@ -29,7 +40,7 @@ All notable changes to jobd. Format roughly follows [Keep a Changelog](https://k Shipping in the same release, the user-visible surface that makes a substituted identity findable and explainable rather than merely correct: - `job list --project NAME` now matches **either** the scheduling identity or the typed - run label. Without this a job submitted as `pillar2a1_sweep` and priced as `jepagame` + run label. Without this a job submitted as `pillar2a1_sweep` and priced as `beta` was unfindable under the only name its submitter ever knew it by. - `JobInfo` gains `project_label`: the name as typed, `null` when it agrees with `project`, so the field reads as "something was substituted here". @@ -48,8 +59,8 @@ All notable changes to jobd. Format roughly follows [Keep a Changelog](https://k projects fold onto the same key the fold is refused and cwd decides, yet the names still fold together. `job submit --explain` carries `matched_root` and answers it. - Path matching now collapses `..` lexically before comparing components. It previously - did not, so `/home/mjarnold/jepagame/../../tmp` — a job actually running in `/tmp` — - matched a root of `/home/mjarnold/jepagame` and was priced at that project's `78`. + did not, so `/home/user/beta/../../tmp` — a job actually running in `/tmp` — + matched a root of `/home/user/beta` and was priced at that project's `78`. `cwd` is free text on the wire, so this was caller-reachable. A root containing a `..` component is now a load error, in the same raise-don't-drop style as the other root validations. Symlinks and bind mounts remain unresolved by design: the broker cannot @@ -76,12 +87,12 @@ All notable changes to jobd. Format roughly follows [Keep a Changelog](https://k - **A registered project no longer loses its priority to how the name was typed.** Project names are free text chosen at submit time and were matched with a bare - `name in projects`, so `ARFDSynInt` ran at `_default` 40 while `arfdsynint` sat + `name in projects`, so `epsilon` ran at `_default` 40 while `epsilon` sat deliberately registered at 65 — a difference of case alone, raising no error and visible only in a warning nothing consumed. Case and `-`/`_` are now folded when matching, the resolved name is what `/submit` stores and `/resolve` previews, and an - exact hit always wins. Folding is deliberately **not** fuzzy: `phelipanche` is not - folded onto `phelipanche-fm`, because a suffix difference is a registration decision, + exact hit always wins. Folding is deliberately **not** fuzzy: `kappa` is not + folded onto `kappa-fm`, because a suffix difference is a registration decision, and guessing there would run work at another project's priority — the same bug inverted. Two registered names that fold together are reported at load and fall back to exact matching rather than one being picked arbitrarily. diff --git a/config/projects.yaml b/config/projects.yaml index 1e78c1f..716a008 100644 --- a/config/projects.yaml +++ b/config/projects.yaml @@ -18,33 +18,33 @@ projects: # never a shared scratch path, and never a directory the corpus also shows # in the hands of a genuinely different project. `tests/test_corpus_replay.py` # replays the same corpus against the resolver these roots feed. - agrigen: + alpha: priority: 80 roots: - # 287 jobs across agrigen's own tree (backend + worktrees), all typed - # `agrigen` or an `agrigen-*`/`phase*` variant. Deliberately NOT - # including /home/mjarnold/trellis2, .../hunyuan3d or .../instantmesh — - # each is >=97% agrigen-typed in the corpus too (trellis2 alone is the + # 287 jobs across alpha's own tree (backend + worktrees), all typed + # `alpha` or an `alpha-*`/`phase*` variant. Deliberately NOT + # including /home/user/trellis2, .../hunyuan3d or .../instantmesh — + # each is >=97% alpha-typed in the corpus too (trellis2 alone is the # single largest cwd in the whole corpus, 1209 jobs), but all three are # generic third-party tool checkouts (TRELLIS, Hunyuan3D, InstantMesh); # a differently-owned future clone at the same path would silently - # inherit agrigen's identity. Revisit if that risk turns out to be + # inherit alpha's identity. Revisit if that risk turns out to be # theoretical. - - /home/mjarnold/agrigen - jepagame: + - /home/user/alpha + beta: priority: 78 roots: - # 187 jobs directly in /home/mjarnold/jepagame plus several hundred more + # 187 jobs directly in /home/user/beta plus several hundred more # in its .claude/worktrees/* and .worktrees/* subdirectories (matched - # via the same root, component-wise) — every typed name is `jepagame` + # via the same root, component-wise) — every typed name is `beta` # or a `pillarN...` sweep label. - - /home/mjarnold/jepagame + - /home/user/beta # 12 jobs, one typed label (`pillar1l-emerge`), unregistered — every one # of them falls to _default today. A sibling of the root above (not - # nested under it, so it needs its own entry), and unlike orchid-data/ - # dreamer-chassis below this one is NOT forward-looking: it recovers + # nested under it, so it needs its own entry), and unlike gamma-data/ + # delta below this one is NOT forward-looking: it recovers # real historical jobs today. - - /home/mjarnold/jepagame-1l-sweep + - /home/user/beta-1l-sweep # Tier 2 — active development project-c: @@ -55,27 +55,27 @@ projects: # grace window) — only safe for jobs that checkpoint. See docs/preemption.md. preemptible: true checkpoint_grace_s: 60 - orchid-sdxl: + gamma: priority: 60 roots: - # 335 jobs at /home/mjarnold/orchid-sdxl under `orchid-sdxl` plus a - # dozen `orchid-*`/`stage*` variants — real historical recovery. - - /home/mjarnold/orchid-sdxl + # 335 jobs at /home/user/gamma under `gamma` plus a + # dozen `gamma-*`/`stage*` variants — real historical recovery. + - /home/user/gamma # Forward-looking, not evidence-backed: every one of the 17 jobs here - # was already typed `orchid-sdxl` exactly, so this root recovers ZERO + # was already typed `gamma` exactly, so this root recovers ZERO # historical jobs (rule 1 already handled all of them). Kept because # the directory is 17/17 clean for this project — it exists so a # future mistyped run from here lands right, not because the corpus # needed it. - - /home/mjarnold/orchid-data - dreamer-chassis: + - /home/user/gamma-data + delta: priority: 60 roots: # Forward-looking, not evidence-backed: all 18 jobs here were already - # typed `dreamer-chassis` exactly, so this root recovers ZERO + # typed `delta` exactly, so this root recovers ZERO # historical jobs (rule 1 already handled all of them). Kept as a # safety net for a future mistyped label from this directory. - - /home/mjarnold/dreamer-chassis + - /home/user/delta # Tier 3 — background / best-effort project-d: { priority: 40 } diff --git a/docs/plans/2026-06-30-submit-cwd-probe.md b/docs/plans/2026-06-30-submit-cwd-probe.md index d371958..bceca11 100644 --- a/docs/plans/2026-06-30-submit-cwd-probe.md +++ b/docs/plans/2026-06-30-submit-cwd-probe.md @@ -18,7 +18,6 @@ - **Follow existing patterns:** mirror the `*_json` nullable-TEXT column style (`Worker.mount_roots_json`, `host_aliases_json`, `tags_json`) and the existing refuse-admission call site (`job_worker.py:1416`). - **Commit footer (every commit):** ``` - Co-Authored-By: Claude Opus 4.8 ``` - **Deploy is out of scope for these tasks** (multi-host; gated on user go-ahead). Task 8 writes the deploy checklist only. @@ -90,7 +89,6 @@ feat(matcher): WorkerSnapshot.mount_roots + _build_snapshots population Threads the worker-reported mount_roots prefixes onto the snapshot so submit-time routability checks can read them. Default [] = back-compat. -Co-Authored-By: Claude Opus 4.8 EOF ``` @@ -245,7 +243,6 @@ Empty mount_roots = unknown (never false-reject). A shared-prefix path (/home) is routable here by design — the worker-side isdir check catches host-local paths under it. -Co-Authored-By: Claude Opus 4.8 EOF ``` @@ -333,7 +330,6 @@ Hard-400s a pinned cwd no mount_root covers (generalizes /mnt/c); folds the any-pin "no worker covers cwd" case into the submit warnings. /mnt/c fast-path unchanged. -Co-Authored-By: Claude Opus 4.8 EOF ``` @@ -412,7 +408,6 @@ Per-job set of hosts that refused the job for a missing cwd, so the matcher won't re-offer it to them. Nullable TEXT, NULL -> [] (back-compat). [Schema init path noted for deploy: create_all vs ALTER.] -Co-Authored-By: Claude Opus 4.8 EOF ``` @@ -575,7 +570,6 @@ A worker that finds cwd missing refuses with reason=cwd_missing; broker records the host in excluded_workers_json and re-queues, or fails the job cwd_unreachable when no eligible worker remains. gpu_contention path unchanged. -Co-Authored-By: Claude Opus 4.8 EOF ``` @@ -631,7 +625,6 @@ git add src/jobd/app.py tests/test_api.py git commit -F - <<'EOF' feat(broker): /next-job skips workers in a job's cwd-exclusion set -Co-Authored-By: Claude Opus 4.8 EOF ``` @@ -716,7 +709,6 @@ Before the launcher check, verify os.path.isdir(cwd). If absent, POST refuse-admission reason=cwd_missing so the broker re-routes to a host that has the path, instead of cd-failing to exit 127. -Co-Authored-By: Claude Opus 4.8 EOF ``` @@ -753,7 +745,6 @@ git add -A git commit -F - <<'EOF' docs(routing): cwd-probe deploy checklist + changelog; full suite green -Co-Authored-By: Claude Opus 4.8 EOF ``` diff --git a/docs/plans/2026-08-31-cwd-project-identity-design.md b/docs/plans/2026-08-31-cwd-project-identity-design.md index b982bcf..cb4cf7c 100644 --- a/docs/plans/2026-08-31-cwd-project-identity-design.md +++ b/docs/plans/2026-08-31-cwd-project-identity-design.md @@ -17,11 +17,11 @@ field means every submitter has to satisfy 1 while thinking about 2, and the failure is silent: an unregistered name falls through to `_default` and runs at 40 with no error. -v0.5.39–41 closed the _spelling_ half of this (`ARFDSynInt` now folds onto -`arfdsynint`; a write reports the name it landed on). The retyped-string +v0.5.39–41 closed the _spelling_ half of this (`epsilon` now folds onto +`epsilon`; a write reports the name it landed on). The retyped-string mechanism itself is untouched, and it is the larger half. -### Measured, live DB `/home/mjarnold/jobd/data/jobd.db`, 2026-08-31 +### Measured, live DB `/home/user/jobd/data/jobd.db`, 2026-08-31 3,608 job rows; 249 distinct `(project, cwd)` pairs; 138 distinct project names against 148 distinct cwds. @@ -39,11 +39,11 @@ The top entries show the mechanism plainly — these are run labels, not projects: ``` - 27 pillar2a1_sweep /home/mjarnold/jepagame (jepagame is registered at 78) - 26 orchid-sdxl-geo /home/mjarnold/orchid-sdxl - 18 dreamer-chassis /home/mjarnold/dreamer-chassis - 16 pillar4_1 /home/mjarnold/jepagame - 15 orchid-stage2 /home/mjarnold/orchid-sdxl + 27 pillar2a1_sweep /home/user/beta (beta is registered at 78) + 26 gamma-geo /home/user/gamma + 18 delta /home/user/delta + 16 pillar4_1 /home/user/beta + 15 gamma-stage2 /home/user/gamma ``` The relation is genuinely many-to-many — 33 projects span more than one cwd, 26 @@ -79,19 +79,19 @@ per-job field defaults and a root is not one of those. ```yaml projects: - jepagame: + beta: priority: 78 roots: - - /home/mjarnold/jepagame - orchid-sdxl: + - /home/user/beta + gamma: priority: 60 roots: - - /home/mjarnold/orchid-sdxl + - /home/user/gamma ``` Matching is **longest root wins**, compared **path-component-wise, not as a -string prefix**. `/home/mjarnold/jepagame2` must not match root -`/home/mjarnold/jepagame`; a naive `str.startswith` says it does. This is the +string prefix**. `/home/user/beta2` must not match root +`/home/user/beta`; a naive `str.startswith` says it does. This is the single most likely defect in the feature and gets a dedicated negative test. Symlinks are not resolved. The broker has no access to the worker's filesystem, @@ -158,7 +158,7 @@ through to `_default`" signal is not weakened by the new one. ## Testing Unit: precedence (all three rules), longest-root selection, the -`jepagame2`/`jepagame` boundary negative, ambiguous-roots warn-and-fall-through, +`beta2`/`beta` boundary negative, ambiguous-roots warn-and-fall-through, label preserved in every branch, bad-root load error. The real-execution check is a **corpus replay**: run the resolver over every one @@ -194,8 +194,8 @@ would read as a clean pass. ## Out of scope Eighteen entries currently sit at priority 65, and most of them are -experiment-arm names rather than projects (`host_grammar`, -`convergent_mimicry`, `outgroup_control`, `effector_host`, `kai2d`, +experiment-arm names rather than projects (`h_grammar`, +`c_mimicry`, `og_control`, `eff_host`, `k2d`, `arf-dimer-g1`, …) registered one at a time as a workaround for exactly this gap. They become redundant once roots exist, but rule 1 keeps them winning and nothing breaks while they stay. Retiring them is separate cleanup, after this diff --git a/docs/plans/2026-08-31-cwd-project-identity-plan.md b/docs/plans/2026-08-31-cwd-project-identity-plan.md index 2a4e30d..887591b 100644 --- a/docs/plans/2026-08-31-cwd-project-identity-plan.md +++ b/docs/plans/2026-08-31-cwd-project-identity-plan.md @@ -13,7 +13,7 @@ ## Global Constraints - **Rule 1 wins.** A typed name that resolves to a registered project via `canonical_project_name` is the identity. cwd is consulted **only** when it does not. No currently-correct submit may change priority. -- **Path-component matching, never `str.startswith`.** `/home/mjarnold/jepagame2` must not match root `/home/mjarnold/jepagame`. +- **Path-component matching, never `str.startswith`.** `/home/user/beta2` must not match root `/home/user/beta`. - **No symlink resolution.** The broker cannot see the worker's filesystem; it compares the path it was handed. - **Schema changes go through the existing additive `migrate()`** in `src/jobd/db.py:222` plus an entry in `_JOB_ADDS` (`db.py:184`). No alembic. - **`cwd_identity_applied` must land in `KNOWN_EVENTS` (`src/jobd/models.py:406`) in the same commit that emits it.** An unlisted name collapses into the `other` bucket at `broker/events.py:74` and any alert on it silently never fires. @@ -47,14 +47,14 @@ def test_roots_are_parsed_and_normalized(tmp_path): p = tmp_path / "projects.yaml" p.write_text( "projects:\n" - " jepagame:\n" + " beta:\n" " priority: 78\n" " roots:\n" - " - /home/mjarnold/jepagame/\n" - " - /srv/jepagame\n" + " - /home/user/beta/\n" + " - /srv/beta\n" ) projects = load_projects(p) - assert projects["jepagame"].roots == ["/home/mjarnold/jepagame", "/srv/jepagame"] + assert projects["beta"].roots == ["/home/user/beta", "/srv/beta"] def test_a_project_without_roots_gets_an_empty_list(tmp_path): @@ -72,7 +72,7 @@ def test_a_project_without_roots_gets_an_empty_list(tmp_path): [ ("roots: [relative/path]", "not absolute"), ("roots: [17]", "not a string"), - ("roots: /home/mjarnold/x", "not a list"), + ("roots: /home/user/x", "not a list"), ("roots: ['/']", "filesystem root captures every job"), ], ) @@ -81,7 +81,7 @@ def test_a_bad_root_is_a_load_error_not_a_silent_drop(tmp_path, bad, why): present to the operator as 'the feature just doesn't work on my machine', so roots are validated loudly instead.""" p = tmp_path / "projects.yaml" - p.write_text(f"projects:\n jepagame:\n priority: 78\n {bad}\n") + p.write_text(f"projects:\n beta:\n priority: 78\n {bad}\n") with pytest.raises(ValueError, match="roots"): load_projects(p) ``` @@ -199,8 +199,8 @@ Create `tests/unit/test_project_from_cwd.py`: """cwd -> project identity matching. The boundary case is the whole point of this module. A `str.startswith` root -match is the obvious implementation and it is wrong: `/home/mjarnold/jepagame2` -starts with `/home/mjarnold/jepagame`, so a sibling project would silently +match is the obvious implementation and it is wrong: `/home/user/beta2` +starts with `/home/user/beta`, so a sibling project would silently inherit its neighbour's priority -- the exact failure class this feature exists to end, pointing the other way. """ @@ -216,43 +216,43 @@ def _projects(**roots_by_name): def test_a_cwd_inside_a_root_resolves_to_that_project(): - projects = _projects(jepagame=["/home/mjarnold/jepagame"]) - assert project_from_cwd(projects, "/home/mjarnold/jepagame/sweeps/a") == ( - "jepagame", - "/home/mjarnold/jepagame", + projects = _projects(beta=["/home/user/beta"]) + assert project_from_cwd(projects, "/home/user/beta/sweeps/a") == ( + "beta", + "/home/user/beta", ) def test_the_root_itself_matches(): - projects = _projects(jepagame=["/home/mjarnold/jepagame"]) - assert project_from_cwd(projects, "/home/mjarnold/jepagame") == ( - "jepagame", - "/home/mjarnold/jepagame", + projects = _projects(beta=["/home/user/beta"]) + assert project_from_cwd(projects, "/home/user/beta") == ( + "beta", + "/home/user/beta", ) def test_a_sibling_directory_sharing_a_name_prefix_does_not_match(): """The regression this module exists to prevent.""" - projects = _projects(jepagame=["/home/mjarnold/jepagame"]) - assert project_from_cwd(projects, "/home/mjarnold/jepagame2") is None - assert project_from_cwd(projects, "/home/mjarnold/jepagame-scratch") is None + projects = _projects(beta=["/home/user/beta"]) + assert project_from_cwd(projects, "/home/user/beta2") is None + assert project_from_cwd(projects, "/home/user/beta-scratch") is None def test_the_deepest_root_wins(): """Nesting is legitimate: a sub-project living inside a parent's tree must take its own identity, not the parent's.""" projects = _projects( - jepagame=["/home/mjarnold/jepagame"], - jepagame_xcheck=["/home/mjarnold/jepagame/xcheck"], + beta=["/home/user/beta"], + beta_xcheck=["/home/user/beta/xcheck"], ) - assert project_from_cwd(projects, "/home/mjarnold/jepagame/xcheck/run1") == ( - "jepagame-xcheck", - "/home/mjarnold/jepagame/xcheck", + assert project_from_cwd(projects, "/home/user/beta/xcheck/run1") == ( + "beta-xcheck", + "/home/user/beta/xcheck", ) def test_an_unmatched_cwd_returns_none(): - projects = _projects(jepagame=["/home/mjarnold/jepagame"]) + projects = _projects(beta=["/home/user/beta"]) assert project_from_cwd(projects, "/tmp") is None @@ -260,7 +260,7 @@ def test_a_project_with_no_roots_never_matches(): """Positive control for the opt-in: roots are opt-in, and a project that declares none must not be reachable by cwd at all.""" projects = {"_default": ProjectEntry(priority=40), "plain": ProjectEntry(priority=60)} - assert project_from_cwd(projects, "/home/mjarnold/plain") is None + assert project_from_cwd(projects, "/home/user/plain") is None def test_two_projects_claiming_the_same_depth_yield_no_identity(caplog): @@ -268,30 +268,30 @@ def test_two_projects_claiming_the_same_depth_yield_no_identity(caplog): canonical_project_name's posture on an ambiguous fold (config.py:355). Picking one would run someone's jobs at a neighbour's priority.""" projects = _projects( - alpha=["/home/mjarnold/shared"], - beta=["/home/mjarnold/shared"], + alpha=["/home/user/shared"], + beta=["/home/user/shared"], ) with caplog.at_level("WARNING"): - assert project_from_cwd(projects, "/home/mjarnold/shared/x") is None + assert project_from_cwd(projects, "/home/user/shared/x") is None assert "alpha" in caplog.text and "beta" in caplog.text def test_an_ambiguous_shallow_root_still_loses_to_a_deeper_unambiguous_one(): """Ambiguity at depth N must not poison a clear winner at depth N+1.""" projects = _projects( - alpha=["/home/mjarnold/shared"], - beta=["/home/mjarnold/shared"], - gamma=["/home/mjarnold/shared/g"], + alpha=["/home/user/shared"], + beta=["/home/user/shared"], + gamma=["/home/user/shared/g"], ) - assert project_from_cwd(projects, "/home/mjarnold/shared/g/run") == ( + assert project_from_cwd(projects, "/home/user/shared/g/run") == ( "gamma", - "/home/mjarnold/shared/g", + "/home/user/shared/g", ) def test_default_is_never_returned_as_an_identity(): - projects = {"_default": ProjectEntry(priority=40, roots=["/home/mjarnold"])} - assert project_from_cwd(projects, "/home/mjarnold/anything") is None + projects = {"_default": ProjectEntry(priority=40, roots=["/home/user"])} + assert project_from_cwd(projects, "/home/user/anything") is None ``` - [ ] **Step 2: Run the tests to verify they fail** @@ -307,8 +307,8 @@ Add to `src/jobd/config.py`, immediately after `canonical_project_name`: def _path_is_within(cwd: str, root: str) -> bool: """True when `cwd` is `root` or lives underneath it, compared COMPONENT-WISE. - Not `cwd.startswith(root)`: that says `/home/mjarnold/jepagame2` is inside - `/home/mjarnold/jepagame`, which would hand a sibling project its + Not `cwd.startswith(root)`: that says `/home/user/beta2` is inside + `/home/user/beta`, which would hand a sibling project its neighbour's priority. Comparing tuples of path components makes the boundary structural rather than textual. """ @@ -408,18 +408,18 @@ def _req(project: str, cwd: str) -> JobSubmit: def _projects() -> dict[str, ProjectEntry]: return { "_default": ProjectEntry(priority=40), - "jepagame": ProjectEntry(priority=78, roots=["/home/mjarnold/jepagame"]), - "orchid-sdxl": ProjectEntry(priority=60, roots=["/home/mjarnold/orchid-sdxl"]), + "beta": ProjectEntry(priority=78, roots=["/home/user/beta"]), + "gamma": ProjectEntry(priority=60, roots=["/home/user/gamma"]), } def test_rule1_a_registered_name_wins_over_a_conflicting_cwd(): - """THE invariant. Submitted as orchid-sdxl from inside jepagame's tree: + """THE invariant. Submitted as gamma from inside beta's tree: the typed, registered name must win, so no correct submit is repriced.""" eff = resolve_effective_config( - _req("orchid-sdxl", "/home/mjarnold/jepagame/sweeps"), _projects(), None + _req("gamma", "/home/user/beta/sweeps"), _projects(), None ) - assert eff.project == "orchid-sdxl" + assert eff.project == "gamma" assert eff.priority.value == 60 assert eff.matched_root is None @@ -428,20 +428,20 @@ def test_rule1_still_wins_when_the_name_needed_folding(): """A name reaching rule 1 only via case/`-_` folding is still a rule-1 hit and must not be overridden by cwd.""" eff = resolve_effective_config( - _req("Orchid_SDXL", "/home/mjarnold/jepagame"), _projects(), None + _req("GAMMA", "/home/user/beta"), _projects(), None ) - assert eff.project == "orchid-sdxl" + assert eff.project == "gamma" assert eff.matched_root is None - assert eff.project_label == "Orchid_SDXL" + assert eff.project_label == "GAMMA" def test_rule2_an_unregistered_label_takes_its_identity_from_cwd(): eff = resolve_effective_config( - _req("pillar2a1_sweep", "/home/mjarnold/jepagame/sweeps"), _projects(), None + _req("pillar2a1_sweep", "/home/user/beta/sweeps"), _projects(), None ) - assert eff.project == "jepagame" + assert eff.project == "beta" assert eff.priority.value == 78 - assert eff.matched_root == "/home/mjarnold/jepagame" + assert eff.matched_root == "/home/user/beta" assert eff.project_label == "pillar2a1_sweep" assert eff.unknown_project_warning is None @@ -449,9 +449,9 @@ def test_rule2_an_unregistered_label_takes_its_identity_from_cwd(): def test_rule2_supplies_project_defaults_too_not_just_priority(): """Identity means the whole entry, not only the number.""" projects = _projects() - projects["jepagame"].defaults.max_wall_s = 3600 + projects["beta"].defaults.max_wall_s = 3600 eff = resolve_effective_config( - _req("pillar2a1_sweep", "/home/mjarnold/jepagame"), projects, None + _req("pillar2a1_sweep", "/home/user/beta"), projects, None ) assert eff.max_wall_s.value == 3600 assert eff.max_wall_s.source == "project_default" @@ -470,8 +470,8 @@ def test_rule3_an_unmatched_job_still_warns_as_before(): def test_the_typed_label_is_preserved_in_every_branch(): projects = _projects() for name, cwd in [ - ("orchid-sdxl", "/home/mjarnold/jepagame"), - ("pillar2a1_sweep", "/home/mjarnold/jepagame"), + ("gamma", "/home/user/beta"), + ("pillar2a1_sweep", "/home/user/beta"), ("whatever", "/tmp"), ]: eff = resolve_effective_config(_req(name, cwd), projects, None) @@ -706,7 +706,7 @@ def test_the_event_is_emitted_with_the_label_and_the_root(rooted_logs): "/submit", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) @@ -718,9 +718,9 @@ def test_the_event_is_emitted_with_the_label_and_the_root(rooted_logs): ] hits = [e for e in rows if e["event"] == "cwd_identity_applied"] assert len(hits) == 1, f"expected exactly one, got {[e['event'] for e in rows]}" - assert hits[0]["project"] == "jepagame" + assert hits[0]["project"] == "beta" assert hits[0]["payload"]["project_label"] == "pillar2a1_sweep" - assert hits[0]["payload"]["matched_root"] == "/home/mjarnold/jepagame" + assert hits[0]["payload"]["matched_root"] == "/home/user/beta" def test_no_event_when_the_typed_name_was_already_registered(rooted_logs): @@ -731,7 +731,7 @@ def test_no_event_when_the_typed_name_was_already_registered(rooted_logs): client, logs_dir = rooted_logs r = client.post( "/submit", - json={"cmd": ["true"], "cwd": "/home/mjarnold/jepagame", "project": "jepagame"}, + json={"cmd": ["true"], "cwd": "/home/user/beta", "project": "beta"}, ) assert r.status_code == 200, r.text rows = [ @@ -768,12 +768,12 @@ def rooted_app(tmp_path, sample_profiles_yaml, sample_classifier_yaml): projects = tmp_path / "rooted-projects.yaml" projects.write_text( "projects:\n" - " jepagame:\n" + " beta:\n" " priority: 78\n" - " roots: ['/home/mjarnold/jepagame']\n" - " orchid-sdxl:\n" + " roots: ['/home/user/beta']\n" + " gamma:\n" " priority: 60\n" - " roots: ['/home/mjarnold/orchid-sdxl']\n" + " roots: ['/home/user/gamma']\n" " _default: { priority: 40 }\n" ) return build_app( @@ -917,13 +917,13 @@ def test_filtering_by_the_label_finds_the_job(rooted_client): "/submit", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) rows = client.get("/jobs", params={"project": "pillar2a1_sweep"}).json()["jobs"] assert len(rows) == 1 - assert rows[0]["project"] == "jepagame" + assert rows[0]["project"] == "beta" def test_filtering_by_the_identity_finds_the_same_job(rooted_client): @@ -931,11 +931,11 @@ def test_filtering_by_the_identity_finds_the_same_job(rooted_client): "/submit", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) - rows = client.get("/jobs", params={"project": "jepagame"}).json()["jobs"] + rows = client.get("/jobs", params={"project": "beta"}).json()["jobs"] assert len(rows) == 1 @@ -946,11 +946,11 @@ def test_an_unrelated_project_filter_still_matches_nothing(rooted_client): "/submit", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) - assert client.get("/jobs", params={"project": "orchid-sdxl"}).json()["jobs"] == [] + assert client.get("/jobs", params={"project": "gamma"}).json()["jobs"] == [] ``` Create `tests/unit/test_cwd_identity_resolve_api.py`: @@ -963,20 +963,20 @@ def test_resolve_reports_the_root_that_supplied_the_identity(rooted_client): "/resolve", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) body = r.json() - assert body["project"] == "jepagame" + assert body["project"] == "beta" assert body["project_label"] == "pillar2a1_sweep" - assert body["matched_root"] == "/home/mjarnold/jepagame" + assert body["matched_root"] == "/home/user/beta" def test_resolve_reports_no_root_for_a_registered_name(rooted_client): r = client.post( "/resolve", - json={"cmd": ["true"], "cwd": "/home/mjarnold/jepagame", "project": "jepagame"}, + json={"cmd": ["true"], "cwd": "/home/user/beta", "project": "beta"}, ) assert r.json()["matched_root"] is None ``` @@ -1121,7 +1121,7 @@ signal.signal( ) signal.alarm(60) -DB = os.environ.get("JOBD_DB", "/home/mjarnold/jobd/data/jobd.db") +DB = os.environ.get("JOBD_DB", "/home/user/jobd/data/jobd.db") rows = sqlite3.connect(f"file:{DB}?mode=ro", uri=True).execute( "SELECT project, cwd, COUNT(*) FROM jobs GROUP BY project, cwd ORDER BY project, cwd" ).fetchall() @@ -1135,7 +1135,7 @@ w.writerows(rows) ```bash mkdir -p tests/data -ssh mjarnold@100.113.204.41 'timeout 90 python3 -' \ +ssh user@broker-host 'timeout 90 python3 -' \ < scripts/export_project_cwd_corpus.py > tests/data/project_cwd_corpus.csv wc -l tests/data/project_cwd_corpus.csv # expect ~250 (249 pairs + header) ``` @@ -1176,9 +1176,9 @@ CORPUS = Path(__file__).parent / "data" / "project_cwd_corpus.csv" # The roots this feature ships with, mirroring config/projects.yaml. ROOTS = { - "jepagame": ["/home/mjarnold/jepagame"], - "orchid-sdxl": ["/home/mjarnold/orchid-sdxl"], - "dreamer-chassis": ["/home/mjarnold/dreamer-chassis"], + "beta": ["/home/user/beta"], + "gamma": ["/home/user/gamma"], + "delta": ["/home/user/delta"], } @@ -1194,10 +1194,10 @@ def corpus(): def projects(): p = {"_default": ProjectEntry(priority=40)} for name, prio in [ - ("jepagame", 78), - ("orchid-sdxl", 60), - ("dreamer-chassis", 60), - ("agrigen", 80), + ("beta", 78), + ("gamma", 60), + ("delta", 60), + ("alpha", 80), ]: p[name] = ProjectEntry(priority=prio, roots=ROOTS.get(name, [])) return p @@ -1246,7 +1246,7 @@ def test_no_job_is_assigned_an_identity_from_a_scratch_directory(corpus, project projects' work, so an identity derived from either would be a confident wrong answer -- worse than the _default it replaced.""" for typed, cwd, _n in corpus: - if cwd.rstrip("/") in ("/tmp", "/home/mjarnold", ""): + if cwd.rstrip("/") in ("/tmp", "/home/user", ""): assert project_from_cwd(projects, cwd) is None, f"{cwd} claimed by a root" ``` @@ -1329,12 +1329,12 @@ for name, e in sorted(p.items()): if e.roots: print(f"{name:30s} {e.roots}") print() -for cwd in ["/home/mjarnold/jepagame/sweeps", "/home/mjarnold/jepagame2", "/tmp"]: +for cwd in ["/home/user/beta/sweeps", "/home/user/beta2", "/tmp"]: print(f"{cwd:40s} -> {project_from_cwd(p, cwd)}") PY ``` -Expected: the roots print as declared; `/home/mjarnold/jepagame/sweeps` resolves to `jepagame`; `/home/mjarnold/jepagame2` and `/tmp` resolve to `None`. A load error here means a malformed root — fix it now rather than at broker start. +Expected: the roots print as declared; `/home/user/beta/sweeps` resolves to `beta`; `/home/user/beta2` and `/tmp` resolve to `None`. A load error here means a malformed root — fix it now rather than at broker start. - [ ] **Step 3: Document `roots:` in `docs/projects-yaml.md`** @@ -1369,10 +1369,10 @@ uv pip install --python .venv/bin/python --force-reinstall --no-deps . After deploying, verify against the live broker rather than the test suite: ```bash -job submit --project some_new_label --cwd /home/mjarnold/jepagame --dry-run -- true +job submit --project some_new_label --cwd /home/user/beta --dry-run -- true ``` -Expected: `validation.effective_project` is `jepagame`, +Expected: `validation.effective_project` is `beta`, `validation.effective_priority` is 78, and `validation.effective_matched_root` -is `/home/mjarnold/jepagame`. Add `--json` if the CLI's dry-run output does not +is `/home/user/beta`. Add `--json` if the CLI's dry-run output does not show the raw validation block. diff --git a/docs/projects-yaml.md b/docs/projects-yaml.md index de137b0..66d866b 100644 --- a/docs/projects-yaml.md +++ b/docs/projects-yaml.md @@ -620,7 +620,7 @@ reload` is sufficient. `--project` is free text, chosen once at submit time and typed by a human every time after. A directory does not change; the spelling a person types -for it does — `pillar2a1_sweep`, `arf-promoter`, `orchid-sdxl-stage4b` are +for it does — `pillar2a1_sweep`, `arf-promoter`, `gamma-stage4b` are all real, one-off run labels for jobs that in every other sense belong to one long-running project. Each of those typos-that-aren't-typos used to fall through to `_default`, at `_default`'s priority, with no warning anyone @@ -631,10 +631,10 @@ what the caller typed, but by where the job runs from. ```yaml projects: - jepagame: + beta: priority: 78 roots: - - /home/mjarnold/jepagame + - /home/user/beta ``` `roots` is a top-level key, a sibling of `priority` and `defaults:`, not @@ -651,8 +651,8 @@ time rather than being dropped, so a bad root is caught at reload/broker start, not discovered later as a job that quietly ran at the wrong priority. See `tests/test_projects_yaml.py::test_a_bad_root_is_a_load_error_not_a_silent_drop`. -Trailing slashes are normalized away (`/home/mjarnold/jepagame/` and -`/home/mjarnold/jepagame` are the same root) but nothing else about a root +Trailing slashes are normalized away (`/home/user/beta/` and +`/home/user/beta` are the same root) but nothing else about a root is inferred — see path matching, below. ### Resolution order — three rules @@ -667,8 +667,8 @@ order, and stopping at the first that produces an answer: consulted.** This is the safety property the other two rules sit behind: a directory's `roots:` can never override an operator's explicit, correctly-spelled project selection. Rule 2 only ever gets a turn when - rule 1 comes up empty — a job typed `--project jepagame` from inside - `/home/mjarnold/orchid-sdxl` is `jepagame`, full stop, not a fight between + rule 1 comes up empty — a job typed `--project beta` from inside + `/home/user/gamma` is `beta`, full stop, not a fight between the typed name and the cwd. 2. **The cwd, rooted.** Only when rule 1 found nothing: every registered project's `roots` are checked against `cwd`. The **deepest matching root @@ -691,19 +691,19 @@ silently convert every unmatched job into a confidently-identified one A root matches when `cwd` equals the root or lives under it, compared **path-component by path-component** — not `cwd.startswith(root)`. That distinction is the whole reason this is safe to ship: a bare string prefix -would say `/home/mjarnold/jepagame2` is inside `/home/mjarnold/jepagame`, +would say `/home/user/beta2` is inside `/home/user/beta`, handing a sibling directory (and whatever unrelated project lives there) its neighbour's priority. Comparing tuples of path parts (`_path_is_within` in -`src/jobd/config.py`) makes the boundary structural: `/home/mjarnold/jepagame2` -and `/home/mjarnold/jepagame/../jepagame2` do not match a root of -`/home/mjarnold/jepagame`; `/home/mjarnold/jepagame/sweeps` does. +`src/jobd/config.py`) makes the boundary structural: `/home/user/beta2` +and `/home/user/beta/../beta2` do not match a root of +`/home/user/beta`; `/home/user/beta/sweeps` does. `..` is collapsed before the comparison, lexically (`os.path.normpath`), so the components compared are the ones the path actually denotes. Without that -step the example above went the other way: `/home/mjarnold/jepagame/../jepagame2` -led with a literal `jepagame` component and matched, and worse, -`/home/mjarnold/jepagame/../../tmp` — a job really running in `/tmp` — priced -at jepagame's `78`. `cwd` is free text on the wire (`--cwd` is a CLI flag, +step the example above went the other way: `/home/user/beta/../beta2` +led with a literal `beta` component and matched, and worse, +`/home/user/beta/../../tmp` — a job really running in `/tmp` — priced +at beta's `78`. `cwd` is free text on the wire (`--cwd` is a CLI flag, `JobSubmit.cwd` a plain string), so that was reachable from a caller, not just in theory. A root containing a `..` component is rejected at load instead: a root is an identity boundary, so write the directory it denotes. @@ -753,7 +753,7 @@ debugging: returned in `JobInfo` (`project_label` is `null` when the two agree, so the field reads as "something was substituted here"). `job list --project` matches **either**, so `job list --project pillar2a1_sweep` still finds - the job that was priced as `jepagame` — though the rendered table column + the job that was priced as `beta` — though the rendered table column shows `project`, the scheduling identity. - `matched_root` — the root that supplied the identity — is **not** on the Job row. It is computed at resolution time and surfaced in exactly two @@ -771,3 +771,24 @@ where every project name ever typed from that directory read as a variant of one project. See the comments beside each project's `roots:` entry for the specific evidence, and `tests/test_corpus_replay.py` for the same corpus replayed end-to-end through the real resolver. + + +## `projects.local.yaml` — private roots and names + +`config/projects.yaml` is tracked and public, so its `roots:` are illustrative. +Real filesystem roots (and any project name you would rather not publish) go in +`config/projects.local.yaml`, which is gitignored and read from the same directory +as `projects.yaml`. An entry there replaces the same-named tracked entry wholesale +(priority, defaults, roots) or adds a new project; names it does not mention are +left as tracked, and `_default` changes only if the file declares it. + +```yaml +projects: + alpha: + priority: 80 + roots: [/home/me/alpha] +``` + +Deploying: copy the file next to the broker's `projects.yaml` (the Docker image reads +`$JOBD_CONFIG_DIR`, so mount it there). Without it the broker still starts; only +cwd-derived identity (rule 2) is limited to the roots the tracked file ships. diff --git a/scripts/export_project_cwd_corpus.py b/scripts/export_project_cwd_corpus.py index b9d9866..ba377b8 100644 --- a/scripts/export_project_cwd_corpus.py +++ b/scripts/export_project_cwd_corpus.py @@ -25,7 +25,7 @@ ) signal.alarm(60) -DB = os.environ.get("JOBD_DB", "/home/mjarnold/jobd/data/jobd.db") +DB = os.environ.get("JOBD_DB", "/home/user/jobd/data/jobd.db") rows = ( sqlite3.connect(f"file:{DB}?mode=ro", uri=True) .execute("SELECT project, cwd, COUNT(*) FROM jobs GROUP BY project, cwd ORDER BY project, cwd") diff --git a/src/job_cli/cli.py b/src/job_cli/cli.py index f5d8675..9bba976 100644 --- a/src/job_cli/cli.py +++ b/src/job_cli/cli.py @@ -364,7 +364,7 @@ def _project_substitution_note(label: str | None, project: str | None) -> str | Two different mechanisms can substitute a name, and naming the wrong one sends an operator to the wrong file: `canonical_project_name` folds case - and `-`/`_` onto a REGISTERED spelling (`Orchid_SDXL` -> `orchid-sdxl`) + and `-`/`_` onto a REGISTERED spelling (`GAMMA` -> `gamma`) without ever consulting cwd, so "identity from cwd" would point at `roots:`, which had nothing to do with it. diff --git a/src/jobd/broker/routes/config.py b/src/jobd/broker/routes/config.py index 121c063..f3db7b7 100644 --- a/src/jobd/broker/routes/config.py +++ b/src/jobd/broker/routes/config.py @@ -74,8 +74,8 @@ def set_project_priority(name: str, payload: SetPriorityRequest): # the shared projects dict; unsynchronized, a concurrent set/nudge can # blow up mid-iteration or last-write-win the overlay (F7). with projects_mutation_lock: - # Resolve the spelling first, so `job projects set ARFDSynInt 65` - # RE-PRICES the registered `arfdsynint` instead of minting a second + # Resolve the spelling first, so `job projects set epsilon 65` + # RE-PRICES the registered `epsilon` instead of minting a second # entry that folds onto it. Without this the write path could still # create the very collision the read path has to warn about. name = canonical_project_name(state["projects"], name) @@ -136,8 +136,8 @@ def resolve_job(req: JobSubmit) -> ResolvedConfig: # is the shared FieldResolution verbatim — same precedence /submit runs. requires_value = eff.requires.value return ResolvedConfig( - # The resolved name, so a caller previewing `ARFDSynInt` is told it - # will run as the registered `arfdsynint` rather than being handed + # The resolved name, so a caller previewing `epsilon` is told it + # will run as the registered `epsilon` rather than being handed # its own spelling back beside that project's priority. project=eff.project, project_label=eff.project_label, diff --git a/src/jobd/config.py b/src/jobd/config.py index ec7d900..81fb0c5 100644 --- a/src/jobd/config.py +++ b/src/jobd/config.py @@ -281,6 +281,30 @@ def apply_project_overrides( return projects +def load_local_projects(path: Path | str) -> dict[str, ProjectEntry]: + """Optional, gitignored `projects.local.yaml` beside `projects.yaml`. + + The tracked `projects.yaml` is public: it ships in the repo and in the + image. A project's `roots:` are real directories on the operator's + machines, and the names themselves can be private, so both belong in a + file git never sees. Every entry declared here REPLACES the same-named + baseline entry wholesale (priority, defaults, roots) or adds a new one; + undeclared names are untouched, and `_default` is only affected if the + file declares it explicitly (`load_projects` would otherwise mint one). + + Missing file: {} — the baseline stands alone, exactly as before this + overlay existed. + """ + p = Path(path) + if not p.exists(): + return {} + declared = set((yaml.safe_load(p.read_text()) or {}).get("projects", {}) or {}) + entries = {name: e for name, e in load_projects(p).items() if name in declared} + if entries: + log.info("projects.local.yaml at %s overlays %s", p, ", ".join(sorted(entries))) + return entries + + def load_effective_projects( projects_path: Path | str, overrides_path: Path | str ) -> tuple[dict[str, ProjectEntry], dict[str, int]]: @@ -291,6 +315,7 @@ def load_effective_projects( only the genuine deltas. """ baseline = load_projects(projects_path) + baseline.update(load_local_projects(Path(projects_path).with_name("projects.local.yaml"))) baseline_priorities = {name: entry.priority for name, entry in baseline.items()} overrides = load_project_overrides(overrides_path) effective = apply_project_overrides(baseline, overrides) @@ -375,8 +400,8 @@ def load_classifier_rules(path: Path | str) -> list[ClassifierRule]: def project_key(name: str) -> str: """Fold a project name to the key used for spelling-insensitive matching. - Case and `-`/`_` only. Deliberately NOT fuzzy: `phelipanche` is not folded - onto `phelipanche-fm`, nor `arf-promoter` onto `arf_promoter_analysis`. + Case and `-`/`_` only. Deliberately NOT fuzzy: `kappa` is not folded + onto `kappa-fm`, nor `arf-promoter` onto `arf_promoter_analysis`. Those differ by a SUFFIX, and treating them as the same project would be a guess that silently routes work at another project's priority — the exact failure this matching is meant to end, inverted. If two such names really @@ -391,7 +416,7 @@ def canonical_project_name(projects: dict[str, ProjectEntry], name: str) -> str: Project names are free text typed at submit time and were matched with a bare `name in projects`, so a REGISTERED project silently lost its priority whenever the submitter typed it differently: measured 2026-08-30, jobs - submitted as `ARFDSynInt` ran at `_default` 40 while `arfdsynint` was + submitted as `epsilon` ran at `_default` 40 while `epsilon` was deliberately registered at 65 — a difference of case alone, with no error and no way to see it except by reading the warning nothing consumed. @@ -422,16 +447,16 @@ def canonical_project_name(projects: dict[str, ProjectEntry], name: str) -> str: def _path_is_within(cwd: str, root: str) -> bool: """True when `cwd` is `root` or lives underneath it, compared COMPONENT-WISE. - Not `cwd.startswith(root)`: that says `/home/mjarnold/jepagame2` is inside - `/home/mjarnold/jepagame`, which would hand a sibling project its + Not `cwd.startswith(root)`: that says `/home/user/beta2` is inside + `/home/user/beta`, which would hand a sibling project its neighbour's priority. Comparing tuples of path components makes the boundary structural rather than textual. `..` is collapsed first, LEXICALLY (`os.path.normpath`). Without it the components lied about where the job runs: measured against the shipped - config, `/home/mjarnold/jepagame/../../tmp` matched a root of - `/home/mjarnold/jepagame`, so a job actually running in /tmp priced at - jepagame's 78. `--cwd` is a free-text CLI flag and `JobSubmit.cwd` is a + config, `/home/user/beta/../../tmp` matched a root of + `/home/user/beta`, so a job actually running in /tmp priced at + beta's 78. `--cwd` is a free-text CLI flag and `JobSubmit.cwd` is a bare `str` with no normalization, so that path is caller-reachable. Lexical is the RIGHT normalization here, not a weaker stand-in for @@ -612,8 +637,8 @@ def resolve_effective_config( precedence cascade both endpoints used to hand-encode separately.""" # Resolve the name ONCE, here, before anything reads it. Every field below # and the Job row /submit writes then agree by construction; matching in - # each consumer instead is how `ARFDSynInt` got priced as an unknown - # project while `arfdsynint` sat registered at 65. + # each consumer instead is how `epsilon` got priced as an unknown + # project while `epsilon` sat registered at 65. project_label = req.project project = canonical_project_name(projects, project_label) matched_root: str | None = None diff --git a/tests/data/project_cwd_corpus.csv b/tests/data/project_cwd_corpus.csv index fa25d3f..f4f2803 100644 --- a/tests/data/project_cwd_corpus.csv +++ b/tests/data/project_cwd_corpus.csv @@ -1,250 +1,250 @@ -project,cwd,n -ARFD,/home/mjarnold/auxin/ARFD-Joseph,27 -ARFD-Joseph,/home/mjarnold/auxin/ARFD-Joseph,10 -ARFDSynInt,/home/mjarnold/.claude/jobs/85a955cb/tmp/figrepro,3 -ARFDSynInt,/home/mjarnold/auxin/ARFDSynInt,10 -ARFDSynInt,/home/mjarnold/auxin/ARFDSynInt/.claude/worktrees/auxre-spacing,3 -ARFDSynInt,/home/mjarnold/figrepro,2 -PhelipancheTranscriptomics,/home/mjarnold/PhelipancheTranscriptomics,2 -PhelipancheTranscriptomics,/mnt/c/Users/a2b32/PhelipancheTranscriptomics,20 -_default,/tmp,6 -ad-hoc,/tmp,6 -agrigen,/home/mjarnold,13 -agrigen,/home/mjarnold/.claude/jobs/beb3f8b0/tmp/freshclone/backend,4 -agrigen,/home/mjarnold/agrigen,13 -agrigen,/home/mjarnold/agrigen/.claude/worktrees/functional-calibration,2 -agrigen,/home/mjarnold/agrigen/.claude/worktrees/functional-calibration/backend,127 -agrigen,/home/mjarnold/agrigen/.claude/worktrees/oncombo-gen/backend,2 -agrigen,/home/mjarnold/agrigen/.claude/worktrees/phase16-photo-level-gate/backend,11 -agrigen,/home/mjarnold/agrigen/.worktrees/phase11-step1c-solanum/backend,6 -agrigen,/home/mjarnold/agrigen/.worktrees/phase11-step1d-solanum/backend,3 -agrigen,/home/mjarnold/agrigen/.worktrees/phase7b2-fagus,2 -agrigen,/home/mjarnold/agrigen/.worktrees/phase7b2-fagus/backend,8 -agrigen,/home/mjarnold/agrigen/.worktrees/phase7b3-pinus/backend,6 -agrigen,/home/mjarnold/agrigen/.worktrees/phase7b3-rerun/backend,5 -agrigen,/home/mjarnold/agrigen/.worktrees/phase7b5-arabidopsis/backend,4 -agrigen,/home/mjarnold/agrigen/.worktrees/phase7b5-solanum-rerun/backend,3 -agrigen,/home/mjarnold/agrigen/backend,72 -agrigen,/home/mjarnold/agrigen_census,9 -agrigen,/home/mjarnold/hunyuan3d,3 -agrigen,/home/mjarnold/threestudio,3 -agrigen,/home/mjarnold/trellis2,11 -agrigen,/tmp,2 -agrigen-backfill,/home/mjarnold/agrigen/backend,2 -agrigen-hunyuan,/home/mjarnold/hunyuan3d,161 -agrigen-instantmesh,/home/mjarnold/instantmesh,22 -agrigen-phase10,/home/mjarnold/agrigen/backend,1 -agrigen-phase8,/home/mjarnold/agrigen/.worktrees/phase8/backend,1 -agrigen-phase8,/home/mjarnold/agrigen/backend,7 -agrigen-phase9,/home/mjarnold/hunyuan3d,2 -agrigen-phase9,/home/mjarnold/hunyuan3d/Hunyuan3D-2.1,1 -agrigen-phase9,/home/mjarnold/hunyuan3d/Hunyuan3D-2.1/hy3dshape,2 -agrigen-trellis,/home/mjarnold/trellis2,1190 -agrigen-trellis2,/home/mjarnold/trellis2/TRELLIS.2,2 -agrigen-viewcensus,/home/mjarnold/agrigen/backend,4 -ancestry,/home/mjarnold/ancestry,1 -ancestry-ho,/home/mjarnold/ancestry-ho,10 -antgame,/home/mjarnold/antgame,16 -antgame,/home/mjarnold/antgame-d2perf/head,3 -antgame,/home/mjarnold/antgame-d2perf/pre,1 -antgame,/mnt/c/Users/a2b32/antgame,61 -antgame,/tmp/antgame-m35x,3 -arf,/home/mjarnold/auxin/arf_promoter_analysis,9 -arf-dimer,/home/mjarnold/auxin/arf_promoter_analysis,4 -arf-dimer-g1,/home/mjarnold/auxin/arf_promoter_analysis/.claude/worktrees/arf-path1-binding-residual,4 -arf-iaa,/home/mjarnold/auxin/arf_promoter_analysis/.claude/worktrees/arf-path1-binding-residual,8 -arf-interp,/home/mjarnold/auxin/arf_promoter_analysis,1 -arf-promoter,/home/mjarnold/auxin/arf_promoter_analysis,29 -arf-promoter,/home/mjarnold/auxin/arf_promoter_analysis/.claude/worktrees/arf-sw3-crossspecies,6 -arf-promoter,/mnt/f/auxin/arf_promoter_analysis,1 -arf-promoter,/tmp,12 -arf-structural,/home/mjarnold,2 -arf6,/home/mjarnold/auxin/arf_promoter_analysis,2 -arf_phelipanche_fm,/home/mjarnold/auxin/arf_phelipanche_fm,6 -arf_promoter,/home/mjarnold/auxin/.wt-stage1,2 -arf_promoter_analysis,/home/mjarnold/.claude/jobs/bec2e0bd/tmp,2 -arf_promoter_analysis,/home/mjarnold/auxin/arf_promoter_analysis,15 -arf_promoter_analysis,/home/mjarnold/auxin/arf_promoter_analysis/.claude/worktrees/arf-path1-binding-residual,1 -arf_promoter_analysis,/home/mjarnold/auxin/arf_promoter_analysis/.claude/worktrees/arf-sw3-crossspecies,8 -arf_promoter_analysis,/home/mjarnold/auxin/arf_promoter_analysis/.claude/worktrees/omnigenome-arf-panel,3 -arf_promoter_analysis,/home/mjarnold/auxin/arf_promoter_analysis/.claude/worktrees/repro-gate-baseline-refresh,1 -arfd,/home/mjarnold/auxin/ARFD-Joseph,1 -arfdsynint,/home/mjarnold/auxin/ARFDSynInt,1 -bio3d-arena,/home/mjarnold/bio3d-arena,15 -bio3d-arena,/home/mjarnold/bio3d-arena/.claude/worktrees/bio3d-arena-mvp,4 -bio3d-arena,/home/mjarnold/bio3d-arena/.claude/worktrees/design-parity-3,6 -bio3d-arena,/home/mjarnold/bio3d-arena/.claude/worktrees/scoping-go-public,1 -biomancy,/home/mjarnold/biomancy-art,3 -biomancy,/mnt/c/Users/a2b32/Documents/My Games/Terraria/tModLoader/ModSources/Biomancy,4 -clinical-validation,/home/mjarnold/auxin/clinical_validation,1 -convergent_grammar,/home/mjarnold/auxin/arf_phelipanche_fm,7 -convergent_mimicry,/home/mjarnold/auxin/phelipanche-fm,2 -demo,/home/mjarnold/jobd,1 -demo,/tmp,8 -deploy-smoke,/home/mjarnold/jobd-worker,1 -dreamer-chassis,/home/mjarnold/dreamer-chassis,18 -dreamer-chassis,/tmp,1 -effector_host,/home/mjarnold/auxin/phelipanche-fm,11 -effector_host,/tmp/pc_boltz,1 -emoji-world,/home/mjarnold/emoji-world,5 -eo-biosim,/home/mjarnold/differland-spike,1 -fm-induce,/home/mjarnold/auxin/phelipanche-fm.wt-induce,1 -handscale,/home/mjarnold/handscale_tmp,1 -homelab,/tmp,3 -host_grammar,/home/mjarnold/auxin/arf_phelipanche_fm,4 -host_grammar,/home/mjarnold/auxin/phelipanche-fm,8 -host_mimicry,/home/mjarnold/auxin/phelipanche-fm,1 -hunyuan3d,/home/mjarnold/hunyuan3d/Hunyuan3D-2.1/hy3dshape,4 -jepagame,/home/mjarnold/.claude/jobs/2e34aa59/tmp/master-check,2 -jepagame,/home/mjarnold/jepagame,121 -jepagame,/home/mjarnold/jepagame-embodiment-v8-desktop,4 -jepagame,/home/mjarnold/jepagame-hostsplit,7 -jepagame,/home/mjarnold/jepagame-openevo,4 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/cluster-scoped-selection,58 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/ecology-positive-control,38 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/embodiment-v8,87 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/master-test-debt,6 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/phase21i-arrival-pilot,1 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/phase21j-run,2 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/phase22-run,3 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar11-polish,1 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar11.1-steering,12 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar11.2-experiment,20 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar11.2-prep,1 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar6.4-impl,2 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar6.5.y-rm-spatial-diag,1 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar7.1-active-cell-shape,3 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar7.2-t1-intercalation,1 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar7.3-convergent-extension,18 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar7.5-speciation,7 -jepagame,/home/mjarnold/jepagame/.claude/worktrees/pillar8.1-polarized-secretion,75 -jepagame,/home/mjarnold/jepagame/.worktrees/pillar1l-emerge-turing-spots,6 -jepagame,/home/mjarnold/jobd,1 -jepagame,/tmp,4 -jepagame,/tmp/jepa446,1 -jepagame,/tmp/jepagame-2a-l4,1 -jepagame-2a,/tmp/jepagame-2a-l4,1 -jepagame-2c-l4,/home/mjarnold/jepagame,3 -jepagame-2c-sweep,/home/mjarnold/jepagame,1 -jepagame-pillar1n5,/home/mjarnold/jepagame,2 -jepagame-xcheck,/home/mjarnold/jepagame-xcheck,3 -jobd,/tmp,11 -jobd,/tmp/definitely-not-here-9f3a,1 -jobd-cutover-test,/tmp,1 -jobd-mcp-fieldtest,/tmp,1 -jobd-mcp-live-test,/tmp,27 -jobd-mcp-smoke,/tmp,1 -jobd-self,/home/mjarnold,3 -jobd-smoke,/home/mjarnold/jobd,3 -jobd-smoke,/tmp,5 -kai2d,/tmp/kai2d_boltz,1 -latency-test,/tmp,1 -letmeplay,/home/mjarnold/letmeplay,1 -letmeplay,/home/mjarnold/letmeplay/experiments/shadow-decoder,4 -mcp-fieldtest,/tmp,4 -mcp-fieldtest-2,/tmp,7 -mcp-fieldtest-3,/tmp,3 -modeldna,/mnt/c/Users/a2b32/repo2,42 -modeldna,/mnt/c/Users/a2b32/repo2/.claude/worktrees/phylo-real-stage1,4 -music-visualizer,/home/mjarnold/music-visualizer/.claude/worktrees/dreamscape-service-plan,1 -mycohet-fm,/home/mjarnold/mycohet-fm,3 -ops,/home/mjarnold/jobd,1 -ops,/tmp,5 -orchid-clip,/home/mjarnold/orchid-sdxl,6 -orchid-clip,/home/mjarnold/orchid_clip_v8_release,2 -orchid-fusion,/home/mjarnold/orchid-sdxl,9 -orchid-sdxl,/home/mjarnold,3 -orchid-sdxl,/home/mjarnold/orchid-data,17 -orchid-sdxl,/home/mjarnold/orchid-sdxl,234 -orchid-sdxl,/home/mjarnold/orchid-sdxl/space_render,2 -orchid-sdxl,/home/mjarnold/orchid_clip_geo_g0,3 -orchid-sdxl,/home/mjarnold/orchid_clip_v8_release,2 -orchid-sdxl,/mnt/c/Users/a2b32,1 -orchid-sdxl,/mnt/c/Users/a2b32/orchid-sdxl,3 -orchid-sdxl,/mnt/d/nonexistent,1 -orchid-sdxl,/tmp,41 -orchid-sdxl,/tmp/orchid-seeds/space,1 -orchid-sdxl-geo,/home/mjarnold/orchid-sdxl,26 -orchid-sdxl-stage4b,/home/mjarnold/orchid-sdxl,11 -orchid-stage2,/home/mjarnold/orchid-sdxl,15 -outgroup_control,/home/mjarnold/auxin/phelipanche-fm,7 -p,/nonexistent/zzz,1 -pax6-validation,/home/mjarnold/auxin/pax6_validation,1 -phase12b,/home/mjarnold/trellis2,8 -phase8,/home/mjarnold/agrigen/backend,8 -phase9,/home/mjarnold/hunyuan3d/Hunyuan3D-2.1/hy3dshape,1 -phelipanche,/home/mjarnold/PhelipancheTranscriptomics,61 -phelipanche,/mnt/c/Users/a2b32/PhelipancheTranscriptomics,24 -phelipanche,/mnt/c/Users/a2b32/PhelipancheTranscriptomics/.claude/worktrees/audit-fixes-2026-06-10,2 -phelipanche-fm,/home/mjarnold/auxin/arf_phelipanche_fm,18 -phelipanche-fm,/home/mjarnold/auxin/phelipanche-fm,52 -phelipanche-fm,/home/mjarnold/cal_underbaseline,4 -pillar1k,/home/mjarnold/jepagame,5 -pillar1l-emerge,/home/mjarnold/jepagame-1l-sweep,12 -pillar1l-emerge,/home/mjarnold/jepagame/.worktrees/pillar1l-emerge-turing-spots,4 -pillar1n6-sweep,/home/mjarnold/jepagame/scratch/pillar1n6-sweep,1 -pillar2a1_sweep,/home/mjarnold/jepagame,27 -pillar2b,/home/mjarnold/jepagame,1 -pillar2c5,/home/mjarnold/jepagame-pillar2c5,1 -pillar2d-cycle,/home/mjarnold/jepagame/.worktrees/pillar2d-cycle,5 -pillar2e,/home/mjarnold/jepagame,2 -pillar2x,/home/mjarnold/jepagame,4 -pillar3a,/home/mjarnold/jepagame-pillar3a,3 -pillar3b,/home/mjarnold/jepagame-pillar3b,2 -pillar4_1,/home/mjarnold/jepagame,16 -pillar4_1_5,/home/mjarnold/jepagame,3 -plant-genomics-mcp,/home/mjarnold/plant-genomics-mcp,16 -plant-genomics-mcp,/home/mjarnold/plant-genomics-mcp/.claude/worktrees/ensembl-seq-region,9 -plant-world-model,/home/mjarnold/plant-world-model/.claude/worktrees/pwm-v1,5 -plant-world-model,/home/mjarnold/pwm-p4,2 -plant-world-model,/tmp/pwm_color,2 -plantdreamer,/home/mjarnold/threestudio,2 -pollination,/home/mjarnold/.claude/jobs/ef845e50/tmp/pheno-run,2 -pollination,/home/mjarnold/.claude/jobs/ef845e50/tmp/selfing-run,2 -pollination,/home/mjarnold/.claude/jobs/ef845e50/tmp/spatial-run,3 -pollination,/home/mjarnold/pollination-runs/pheno-run,4 -pollination,/home/mjarnold/pollination-runs/selfing-run,1 -pollination,/home/mjarnold/pollination-runs/spatial-run,4 -pollination-morphology,/home/mjarnold/pm-conserved,1 -pollination-morphology,/home/mjarnold/pm-empty-time,1 -pollination-morphology,/home/mjarnold/pm-pairing,1 -pollination-morphology,/home/mjarnold/sim-zoo/.claude/worktrees/auditable-cell/_pm,6 -project-a,/tmp,3 -prompt2brick,/home/mjarnold/p2b-lora,5 -prompt2brick,/home/mjarnold/prompt2brick/.claude/worktrees/agent-a4f7aeea896be7294,2 -proteingym-replication,/home/mjarnold/auxin/proteingym_replication,4 -pwm-v1,/home/mjarnold/plant-world-model/.claude/worktrees/pwm-v1,7 -pwm-v1,/home/mjarnold/pwm-v1-run,3 -sanity,/tmp,1 -scbench-armB,/home/mjarnold/eval/scbench,2 -scbench-armB1,/home/mjarnold/eval/scbench,2 -scbench-armB1-retry,/home/mjarnold/eval/scbench,1 -scbench-armB2,/home/mjarnold/eval/scbench,1 -scbench-armB3,/home/mjarnold/eval/scbench,1 -scbench-armBN,/home/mjarnold/eval/scbench,1 -scifig-stylize,/home/mjarnold/scifig-probe,6 -scifig-stylize,/home/mjarnold/scifig-stylize,2 -scifig-stylize,/tmp/scifig-probe,2 -smoke,/tmp,5 -ss-1l-emerge,/home/mjarnold/jepagame/.worktrees/pillar1l-emerge-turing-spots,2 -stack_ab,/home/mjarnold/eval/stack_ab,12 -stackhealth,/home/mjarnold/skillbenchmark,6 -stage2-ship,/home/mjarnold/orchid-sdxl,13 -stage7-gbif-drain,/home/mjarnold/orchid-sdxl,10 -stage8-specimen-tower,/home/mjarnold/orchid-sdxl,11 -synthetic-souls,/home/mjarnold/jepagame,2 -tau2-airline-armBA,/home/mjarnold/eval/tau2_airline,1 -tau2-armB3,/home/mjarnold/eval/tau2_airline,1 -tau2-armBN,/home/mjarnold/eval/tau2_airline,1 -test,/tmp,1 -test-42,/tmp,1 -test-43,/tmp,2 -tier-s-smoke,/home/mjarnold/r-renders,2 -tier1-test,/tmp,1 -tir1-affinity,/home/mjarnold/auxin/arf_promoter_analysis/.claude/worktrees/arf-path1-binding-residual,14 -v6,/home/mjarnold/auxin/arf_promoter_analysis,2 -vision-audit-2026-05-14,/home/mjarnold,2 -vision-audit-2026-05-14,/home/mjarnold/.claude/scripts/vision_bench,12 -vision-bench,/home/mjarnold/.claude/scripts/vision_bench,3 -vision_bench,/home/mjarnold/.claude/scripts/vision_bench,3 -vus_concordance,/home/mjarnold/auxin/vus_concordance,2 +project,cwd,n +zeta,/home/user/lab/zeta,27 +zeta,/home/user/lab/zeta,10 +epsilon,/home/user/.claude/jobs/85a955cb/tmp/figrepro,3 +epsilon,/home/user/lab/epsilon,10 +epsilon,/home/user/lab/epsilon/.claude/worktrees/auxre-spacing,3 +epsilon,/home/user/figrepro,2 +KappaTranscriptomics,/home/user/KappaTranscriptomics,2 +KappaTranscriptomics,/mnt/c/Users/user/KappaTranscriptomics,20 +_default,/tmp,6 +ad-hoc,/tmp,6 +alpha,/home/user,13 +alpha,/home/user/.claude/jobs/beb3f8b0/tmp/freshclone/backend,4 +alpha,/home/user/alpha,13 +alpha,/home/user/alpha/.claude/worktrees/functional-calibration,2 +alpha,/home/user/alpha/.claude/worktrees/functional-calibration/backend,127 +alpha,/home/user/alpha/.claude/worktrees/oncombo-gen/backend,2 +alpha,/home/user/alpha/.claude/worktrees/phase16-photo-level-gate/backend,11 +alpha,/home/user/alpha/.worktrees/phase11-step1c-solanum/backend,6 +alpha,/home/user/alpha/.worktrees/phase11-step1d-solanum/backend,3 +alpha,/home/user/alpha/.worktrees/phase7b2-fagus,2 +alpha,/home/user/alpha/.worktrees/phase7b2-fagus/backend,8 +alpha,/home/user/alpha/.worktrees/phase7b3-pinus/backend,6 +alpha,/home/user/alpha/.worktrees/phase7b3-rerun/backend,5 +alpha,/home/user/alpha/.worktrees/phase7b5-arabidopsis/backend,4 +alpha,/home/user/alpha/.worktrees/phase7b5-solanum-rerun/backend,3 +alpha,/home/user/alpha/backend,72 +alpha,/home/user/alpha_census,9 +alpha,/home/user/hunyuan3d,3 +alpha,/home/user/threestudio,3 +alpha,/home/user/trellis2,11 +alpha,/tmp,2 +alpha-backfill,/home/user/alpha/backend,2 +alpha-hunyuan,/home/user/hunyuan3d,161 +alpha-instantmesh,/home/user/instantmesh,22 +alpha-phase10,/home/user/alpha/backend,1 +alpha-phase8,/home/user/alpha/.worktrees/phase8/backend,1 +alpha-phase8,/home/user/alpha/backend,7 +alpha-phase9,/home/user/hunyuan3d,2 +alpha-phase9,/home/user/hunyuan3d/Hunyuan3D-2.1,1 +alpha-phase9,/home/user/hunyuan3d/Hunyuan3D-2.1/hy3dshape,2 +alpha-trellis,/home/user/trellis2,1190 +alpha-trellis2,/home/user/trellis2/TRELLIS.2,2 +alpha-viewcensus,/home/user/alpha/backend,4 +phi,/home/user/phi,1 +phi-ho,/home/user/phi-ho,10 +eta,/home/user/eta,16 +eta,/home/user/eta-d2perf/head,3 +eta,/home/user/eta-d2perf/pre,1 +eta,/mnt/c/Users/user/eta,61 +eta,/tmp/eta-m35x,3 +arf,/home/user/lab/arf_promoter_analysis,9 +arf-dimer,/home/user/lab/arf_promoter_analysis,4 +arf-dimer-g1,/home/user/lab/arf_promoter_analysis/.claude/worktrees/arf-path1-binding-residual,4 +arf-iaa,/home/user/lab/arf_promoter_analysis/.claude/worktrees/arf-path1-binding-residual,8 +arf-interp,/home/user/lab/arf_promoter_analysis,1 +arf-promoter,/home/user/lab/arf_promoter_analysis,29 +arf-promoter,/home/user/lab/arf_promoter_analysis/.claude/worktrees/arf-sw3-crossspecies,6 +arf-promoter,/mnt/f/lab/arf_promoter_analysis,1 +arf-promoter,/tmp,12 +arf-structural,/home/user,2 +arf6,/home/user/lab/arf_promoter_analysis,2 +arf_kappa_fm,/home/user/lab/arf_kappa_fm,6 +arf_promoter,/home/user/lab/.wt-stage1,2 +arf_promoter_analysis,/home/user/.claude/jobs/bec2e0bd/tmp,2 +arf_promoter_analysis,/home/user/lab/arf_promoter_analysis,15 +arf_promoter_analysis,/home/user/lab/arf_promoter_analysis/.claude/worktrees/arf-path1-binding-residual,1 +arf_promoter_analysis,/home/user/lab/arf_promoter_analysis/.claude/worktrees/arf-sw3-crossspecies,8 +arf_promoter_analysis,/home/user/lab/arf_promoter_analysis/.claude/worktrees/omnigenome-arf-panel,3 +arf_promoter_analysis,/home/user/lab/arf_promoter_analysis/.claude/worktrees/repro-gate-baseline-refresh,1 +zeta,/home/user/lab/zeta,1 +epsilon,/home/user/lab/epsilon,1 +bio3d-arena,/home/user/bio3d-arena,15 +bio3d-arena,/home/user/bio3d-arena/.claude/worktrees/bio3d-arena-mvp,4 +bio3d-arena,/home/user/bio3d-arena/.claude/worktrees/design-parity-3,6 +bio3d-arena,/home/user/bio3d-arena/.claude/worktrees/scoping-go-public,1 +mu,/home/user/mu-art,3 +mu,/mnt/c/Users/user/ModSources/Mu,4 +chi,/home/user/lab/clinical_validation,1 +c_grammar,/home/user/lab/arf_kappa_fm,7 +c_mimicry,/home/user/lab/kappa-fm,2 +demo,/home/user/jobd,1 +demo,/tmp,8 +deploy-smoke,/home/user/jobd-worker,1 +delta,/home/user/delta,18 +delta,/tmp,1 +eff_host,/home/user/lab/kappa-fm,11 +eff_host,/tmp/pc_boltz,1 +emoji-world,/home/user/emoji-world,5 +sigma,/home/user/differland-spike,1 +fmi,/home/user/lab/kappa-fm.wt-induce,1 +hscale,/home/user/hscale_tmp,1 +homelab,/tmp,3 +h_grammar,/home/user/lab/arf_kappa_fm,4 +h_grammar,/home/user/lab/kappa-fm,8 +h_mimicry,/home/user/lab/kappa-fm,1 +hunyuan3d,/home/user/hunyuan3d/Hunyuan3D-2.1/hy3dshape,4 +beta,/home/user/.claude/jobs/2e34aa59/tmp/master-check,2 +beta,/home/user/beta,121 +beta,/home/user/beta-embodiment-v8-desktop,4 +beta,/home/user/beta-hostsplit,7 +beta,/home/user/beta-openevo,4 +beta,/home/user/beta/.claude/worktrees/cluster-scoped-selection,58 +beta,/home/user/beta/.claude/worktrees/ecology-positive-control,38 +beta,/home/user/beta/.claude/worktrees/embodiment-v8,87 +beta,/home/user/beta/.claude/worktrees/master-test-debt,6 +beta,/home/user/beta/.claude/worktrees/phase21i-arrival-pilot,1 +beta,/home/user/beta/.claude/worktrees/phase21j-run,2 +beta,/home/user/beta/.claude/worktrees/phase22-run,3 +beta,/home/user/beta/.claude/worktrees/pillar11-polish,1 +beta,/home/user/beta/.claude/worktrees/pillar11.1-steering,12 +beta,/home/user/beta/.claude/worktrees/pillar11.2-experiment,20 +beta,/home/user/beta/.claude/worktrees/pillar11.2-prep,1 +beta,/home/user/beta/.claude/worktrees/pillar6.4-impl,2 +beta,/home/user/beta/.claude/worktrees/pillar6.5.y-rm-spatial-diag,1 +beta,/home/user/beta/.claude/worktrees/pillar7.1-active-cell-shape,3 +beta,/home/user/beta/.claude/worktrees/pillar7.2-t1-intercalation,1 +beta,/home/user/beta/.claude/worktrees/pillar7.3-convergent-extension,18 +beta,/home/user/beta/.claude/worktrees/pillar7.5-speciation,7 +beta,/home/user/beta/.claude/worktrees/pillar8.1-polarized-secretion,75 +beta,/home/user/beta/.worktrees/pillar1l-emerge-turing-spots,6 +beta,/home/user/jobd,1 +beta,/tmp,4 +beta,/tmp/beta446,1 +beta,/tmp/beta-2a-l4,1 +beta-2a,/tmp/beta-2a-l4,1 +beta-2c-l4,/home/user/beta,3 +beta-2c-sweep,/home/user/beta,1 +beta-pillar1n5,/home/user/beta,2 +beta-xcheck,/home/user/beta-xcheck,3 +jobd,/tmp,11 +jobd,/tmp/definitely-not-here-9f3a,1 +jobd-cutover-test,/tmp,1 +jobd-mcp-fieldtest,/tmp,1 +jobd-mcp-live-test,/tmp,27 +jobd-mcp-smoke,/tmp,1 +jobd-self,/home/user,3 +jobd-smoke,/home/user/jobd,3 +jobd-smoke,/tmp,5 +k2d,/tmp/k2d_boltz,1 +latency-test,/tmp,1 +theta,/home/user/theta,1 +theta,/home/user/theta/experiments/shadow-decoder,4 +mcp-fieldtest,/tmp,4 +mcp-fieldtest-2,/tmp,7 +mcp-fieldtest-3,/tmp,3 +mdna,/mnt/c/Users/user/repo2,42 +mdna,/mnt/c/Users/user/repo2/.claude/worktrees/phylo-real-stage1,4 +pi-viz,/home/user/pi-viz/.claude/worktrees/dreamscape-service-plan,1 +iota-fm,/home/user/iota-fm,3 +ops,/home/user/jobd,1 +ops,/tmp,5 +orchid-clip,/home/user/gamma,6 +orchid-clip,/home/user/orchid_clip_v8_release,2 +gamma-fusion,/home/user/gamma,9 +gamma,/home/user,3 +gamma,/home/user/gamma-data,17 +gamma,/home/user/gamma,234 +gamma,/home/user/gamma/space_render,2 +gamma,/home/user/orchid_clip_geo_g0,3 +gamma,/home/user/orchid_clip_v8_release,2 +gamma,/mnt/c/Users/user,1 +gamma,/mnt/c/Users/user/gamma,3 +gamma,/mnt/d/nonexistent,1 +gamma,/tmp,41 +gamma,/tmp/gamma-seeds/space,1 +gamma-geo,/home/user/gamma,26 +gamma-stage4b,/home/user/gamma,11 +gamma-stage2,/home/user/gamma,15 +og_control,/home/user/lab/kappa-fm,7 +p,/nonexistent/zzz,1 +p6v,/home/user/lab/pax6_validation,1 +phase12b,/home/user/trellis2,8 +phase8,/home/user/alpha/backend,8 +phase9,/home/user/hunyuan3d/Hunyuan3D-2.1/hy3dshape,1 +kappa,/home/user/KappaTranscriptomics,61 +kappa,/mnt/c/Users/user/KappaTranscriptomics,24 +kappa,/mnt/c/Users/user/KappaTranscriptomics/.claude/worktrees/audit-fixes-2026-06-10,2 +kappa-fm,/home/user/lab/arf_kappa_fm,18 +kappa-fm,/home/user/lab/kappa-fm,52 +kappa-fm,/home/user/cal_underbaseline,4 +pillar1k,/home/user/beta,5 +pillar1l-emerge,/home/user/beta-1l-sweep,12 +pillar1l-emerge,/home/user/beta/.worktrees/pillar1l-emerge-turing-spots,4 +pillar1n6-sweep,/home/user/beta/scratch/pillar1n6-sweep,1 +pillar2a1_sweep,/home/user/beta,27 +pillar2b,/home/user/beta,1 +pillar2c5,/home/user/beta-pillar2c5,1 +pillar2d-cycle,/home/user/beta/.worktrees/pillar2d-cycle,5 +pillar2e,/home/user/beta,2 +pillar2x,/home/user/beta,4 +pillar3a,/home/user/beta-pillar3a,3 +pillar3b,/home/user/beta-pillar3b,2 +pillar4_1,/home/user/beta,16 +pillar4_1_5,/home/user/beta,3 +plant-genomics-mcp,/home/user/plant-genomics-mcp,16 +plant-genomics-mcp,/home/user/plant-genomics-mcp/.claude/worktrees/ensembl-seq-region,9 +xi,/home/user/xi/.claude/worktrees/upsilon-v1,5 +xi,/home/user/upsilon-p4,2 +xi,/tmp/upsilon_color,2 +xi-dreamer,/home/user/threestudio,2 +omicron,/home/user/.claude/jobs/ef845e50/tmp/pheno-run,2 +omicron,/home/user/.claude/jobs/ef845e50/tmp/selfing-run,2 +omicron,/home/user/.claude/jobs/ef845e50/tmp/spatial-run,3 +omicron,/home/user/omicron-runs/pheno-run,4 +omicron,/home/user/omicron-runs/selfing-run,1 +omicron,/home/user/omicron-runs/spatial-run,4 +omicron-morphology,/home/user/pm-conserved,1 +omicron-morphology,/home/user/pm-empty-time,1 +omicron-morphology,/home/user/pm-pairing,1 +omicron-morphology,/home/user/szoo/.claude/worktrees/auditable-cell/_pm,6 +project-a,/tmp,3 +rho,/home/user/rho-lora,5 +rho,/home/user/rho/.claude/worktrees/agent-a4f7aeea896be7294,2 +omega,/home/user/lab/proteingym_replication,4 +upsilon-v1,/home/user/xi/.claude/worktrees/upsilon-v1,7 +upsilon-v1,/home/user/upsilon-v1-run,3 +sanity,/tmp,1 +scbench-armB,/home/user/eval/scbench,2 +scbench-armB1,/home/user/eval/scbench,2 +scbench-armB1-retry,/home/user/eval/scbench,1 +scbench-armB2,/home/user/eval/scbench,1 +scbench-armB3,/home/user/eval/scbench,1 +scbench-armBN,/home/user/eval/scbench,1 +tau-stylize,/home/user/tau-probe,6 +tau-stylize,/home/user/tau-stylize,2 +tau-stylize,/tmp/tau-probe,2 +smoke,/tmp,5 +ss-1l-emerge,/home/user/beta/.worktrees/pillar1l-emerge-turing-spots,2 +sab,/home/user/eval/sab,12 +lambda,/home/user/sbench,6 +stage2-ship,/home/user/gamma,13 +stage7-gbif-drain,/home/user/gamma,10 +stage8-specimen-tower,/home/user/gamma,11 +ssouls,/home/user/beta,2 +tau2-airline-armBA,/home/user/eval/tau2_airline,1 +tau2-armB3,/home/user/eval/tau2_airline,1 +tau2-armBN,/home/user/eval/tau2_airline,1 +test,/tmp,1 +test-42,/tmp,1 +test-43,/tmp,2 +tier-s-smoke,/home/user/r-renders,2 +tier1-test,/tmp,1 +t1a,/home/user/lab/arf_promoter_analysis/.claude/worktrees/arf-path1-binding-residual,14 +v6,/home/user/lab/arf_promoter_analysis,2 +vision-audit-2026-05-14,/home/user,2 +vision-audit-2026-05-14,/home/user/.claude/scripts/vision_bench,12 +vision-bench,/home/user/.claude/scripts/vision_bench,3 +vision_bench,/home/user/.claude/scripts/vision_bench,3 +psi,/home/user/lab/psi,2 diff --git a/tests/test_cli.py b/tests/test_cli.py index 3430e24..307c1e5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -382,7 +382,7 @@ def test_submit_identity_note_goes_to_stderr(monkeypatch): line like every other one in this function, all of which go to stderr.""" r = _submit_with_response( monkeypatch, - {"id": 7, "project": "jepagame", "project_label": "pillar2a1_sweep"}, + {"id": 7, "project": "beta", "project_label": "pillar2a1_sweep"}, ) assert r.exit_code == 0, r.output # The whole point: stdout stays machine-readable. @@ -414,29 +414,27 @@ def test_submit_identity_note_states_only_what_it_can_observe(monkeypatch): def test_submit_folded_spelling_does_not_claim_the_cwd_supplied_it(monkeypatch): - """`Orchid_SDXL` -> `orchid-sdxl` is case/`-`_`_` FOLDING by + """`GAMMA` -> `gamma` is case/`-`_`_` FOLDING by `canonical_project_name`; cwd was never consulted. Saying "identity from cwd" here sends an operator debugging a surprise priority to read `roots:`, which had nothing to do with it.""" r = _submit_with_response( monkeypatch, - {"id": 8, "project": "orchid-sdxl", "project_label": "Orchid_SDXL"}, + {"id": 8, "project": "gamma", "project_label": "GAMMA"}, ) assert r.exit_code == 0, r.output assert json.loads(r.stdout)["id"] == 8 - assert "Orchid_SDXL" in r.stderr + assert "GAMMA" in r.stderr assert "cwd" not in r.stderr def test_submit_prints_no_note_when_the_typed_name_was_used_verbatim(monkeypatch): """Positive control: no substitution, no line — so the two tests above are reading a message that is actually conditional.""" - r = _submit_with_response( - monkeypatch, {"id": 9, "project": "jepagame", "project_label": "jepagame"} - ) + r = _submit_with_response(monkeypatch, {"id": 9, "project": "beta", "project_label": "beta"}) assert r.exit_code == 0, r.output assert json.loads(r.stdout)["id"] == 9 - assert "jepagame" not in r.stderr + assert "beta" not in r.stderr def test_submit_eta_banner_prints_p50_p90_when_history(monkeypatch): diff --git a/tests/test_corpus_replay.py b/tests/test_corpus_replay.py index 318493b..93b911e 100644 --- a/tests/test_corpus_replay.py +++ b/tests/test_corpus_replay.py @@ -62,17 +62,17 @@ # Every root this feature ships with, mirroring config/projects.yaml. An # earlier version of this table listed three of the six while still claiming # to mirror the shipped config, which quietly narrowed arm 1 from 214 jobs to -# 179: agrigen fell through `ROOTS.get(name, [])` to no roots at all, so the +# 179: alpha fell through `ROOTS.get(name, [])` to no roots at all, so the # replay could not have caught a regression in it, and the second root of -# jepagame and of orchid-sdxl went unexercised too. (agrigen's own tree holds -# 287 corpus jobs, but 264 of them are typed `agrigen` exactly and so are +# beta and of gamma went unexercised too. (alpha's own tree holds +# 287 corpus jobs, but 264 of them are typed `alpha` exactly and so are # rule-1 business; 23 are the rule-2 recoveries at stake here.) Add a root # here whenever one is added there. ROOTS = { - "agrigen": ["/home/mjarnold/agrigen"], - "jepagame": ["/home/mjarnold/jepagame", "/home/mjarnold/jepagame-1l-sweep"], - "orchid-sdxl": ["/home/mjarnold/orchid-sdxl", "/home/mjarnold/orchid-data"], - "dreamer-chassis": ["/home/mjarnold/dreamer-chassis"], + "alpha": ["/home/user/alpha"], + "beta": ["/home/user/beta", "/home/user/beta-1l-sweep"], + "gamma": ["/home/user/gamma", "/home/user/gamma-data"], + "delta": ["/home/user/delta"], } @@ -98,10 +98,10 @@ def corpus(): def projects(): p = {"_default": ProjectEntry(priority=40)} for name, prio in [ - ("jepagame", 78), - ("orchid-sdxl", 60), - ("dreamer-chassis", 60), - ("agrigen", 80), + ("beta", 78), + ("gamma", 60), + ("delta", 60), + ("alpha", 80), ]: p[name] = ProjectEntry(priority=prio, roots=ROOTS.get(name, [])) return p @@ -186,5 +186,5 @@ def test_no_job_is_assigned_an_identity_from_a_scratch_directory(corpus, project projects' work, so an identity derived from either would be a confident wrong answer -- worse than the _default it replaced.""" for _typed, cwd, _n in corpus: - if cwd.rstrip("/") in ("/tmp", "/home/mjarnold", ""): + if cwd.rstrip("/") in ("/tmp", "/home/user", ""): assert project_from_cwd(projects, cwd) is None, f"{cwd} claimed by a root" diff --git a/tests/test_no_private_paths.py b/tests/test_no_private_paths.py new file mode 100644 index 0000000..fe535b2 --- /dev/null +++ b/tests/test_no_private_paths.py @@ -0,0 +1,65 @@ +"""The tracked tree carries no private paths, hosts, addresses or session trailers. + +Written after the 2026-08-19 scrub was undone twelve days later by ordinary +feature work: a scrub is a state, not a commit, and nothing was checking the +state. Patterns are assembled from fragments so this file passes its own scan. +""" + +from __future__ import annotations + +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +FORBIDDEN = [ + "/home/" + "mjarnold", + "/Users/" + "a2b32", + "100.113." + "204.41", + "100.91." + "179.102", + "tail" + "86d19d", + "mjarnold" + "gt76", + "Claude-" + "Session:", + "noreply@" + "anthropic.com", +] + + +def scan(paths: list[Path]) -> list[str]: + hits: list[str] = [] + needles = [n.encode() for n in FORBIDDEN] + for p in paths: + try: + data = p.read_bytes() + except (IsADirectoryError, FileNotFoundError): + continue + for i, line in enumerate(data.split(b"\n"), 1): + for n in needles: + if n in line: + hits.append( + f"{p.relative_to(ROOT) if p.is_relative_to(ROOT) else p}:{i}: {n.decode()}" + ) + return hits + + +def _tracked() -> list[Path]: + out = subprocess.run( + ["git", "ls-files", "-z"], cwd=ROOT, capture_output=True, check=True + ).stdout + return [ROOT / f.decode() for f in out.split(b"\0") if f] + + +def test_tracked_tree_has_no_private_paths(): + files = _tracked() + assert Path(__file__) in files, "this guard must itself be tracked" + hits = scan(files) + assert hits == [], "private content is tracked:\n" + "\n".join(hits) + + +def test_scanner_reports_every_planted_hit(tmp_path): + """Positive control: the scanner is worth exactly what it can catch.""" + planted = tmp_path / "planted.bin" + planted.write_bytes(b"\x89PNG\n" + b"\n".join(n.encode() for n in FORBIDDEN) + b"\n") + hits = scan([planted]) + found = {h.split(": ", 1)[1] for h in hits} + assert found == set(FORBIDDEN), sorted(set(FORBIDDEN) - found) + assert all(h.split(":")[1].isdigit() for h in hits) diff --git a/tests/test_projects_yaml.py b/tests/test_projects_yaml.py index f8ae2a1..7a29989 100644 --- a/tests/test_projects_yaml.py +++ b/tests/test_projects_yaml.py @@ -353,14 +353,14 @@ def test_roots_are_parsed_and_normalized(tmp_path): p = tmp_path / "projects.yaml" p.write_text( "projects:\n" - " jepagame:\n" + " beta:\n" " priority: 78\n" " roots:\n" - " - /home/mjarnold/jepagame/\n" - " - /srv/jepagame\n" + " - /home/user/beta/\n" + " - /srv/beta\n" ) projects = load_projects(p) - assert projects["jepagame"].roots == ["/home/mjarnold/jepagame", "/srv/jepagame"] + assert projects["beta"].roots == ["/home/user/beta", "/srv/beta"] def test_a_project_without_roots_gets_an_empty_list(tmp_path): @@ -378,10 +378,10 @@ def test_a_project_without_roots_gets_an_empty_list(tmp_path): [ ("roots: [relative/path]", "not absolute"), ("roots: [17]", "not a string"), - ("roots: /home/mjarnold/x", "not a list"), + ("roots: /home/user/x", "not a list"), ("roots: ['/']", "filesystem root captures every job"), - ("roots: ['/home/mjarnold/x/../y']", "parent-dir component"), - ("roots: ['/home/mjarnold/..']", "parent-dir component"), + ("roots: ['/home/user/x/../y']", "parent-dir component"), + ("roots: ['/home/user/..']", "parent-dir component"), ], ) def test_a_bad_root_is_a_load_error_not_a_silent_drop(tmp_path, bad, why): @@ -389,7 +389,7 @@ def test_a_bad_root_is_a_load_error_not_a_silent_drop(tmp_path, bad, why): present to the operator as 'the feature just doesn't work on my machine', so roots are validated loudly instead.""" p = tmp_path / "projects.yaml" - p.write_text(f"projects:\n jepagame:\n priority: 78\n {bad}\n") + p.write_text(f"projects:\n beta:\n priority: 78\n {bad}\n") with pytest.raises(ValueError, match="roots"): load_projects(p) @@ -421,9 +421,45 @@ def test_the_shipped_roots_resolve_as_intended(): prefix-sharing sibling takes none. """ projects = load_projects(_REPO_ROOT / "config" / "projects.yaml") - hit = project_from_cwd(projects, "/home/mjarnold/jepagame/sweeps/a") + hit = project_from_cwd(projects, "/home/user/beta/sweeps/a") assert hit is not None, "a path under a shipped root must resolve" name, root = hit - assert name == "jepagame" - assert _path_is_within("/home/mjarnold/jepagame/sweeps/a", root) - assert project_from_cwd(projects, "/home/mjarnold/jepagame2") is None + assert name == "beta" + assert _path_is_within("/home/user/beta/sweeps/a", root) + assert project_from_cwd(projects, "/home/user/beta2") is None + + +def test_local_overlay_replaces_roots_and_adds_projects(tmp_path): + """`projects.local.yaml` beside projects.yaml overrides the tracked entry + wholesale and can add a project; names it does not declare are untouched, + and `_default` keeps the tracked priority.""" + from jobd.config import load_effective_projects + + (tmp_path / "projects.yaml").write_text( + "projects:\n _default: {priority: 40}\n" + " alpha: {priority: 80, roots: [/srv/example/alpha]}\n" + " beta: {priority: 70}\n" + ) + (tmp_path / "projects.local.yaml").write_text( + "projects:\n alpha: {priority: 80, roots: [/home/me/alpha]}\n private-x: {priority: 55}\n" + ) + eff, base = load_effective_projects(tmp_path / "projects.yaml", tmp_path / "overrides.yaml") + assert [str(r) for r in eff["alpha"].roots] == ["/home/me/alpha"] + assert eff["private-x"].priority == 55 + assert eff["beta"].priority == 70 + assert eff["_default"].priority == 40 + assert base["private-x"] == 55 + + +def test_no_local_overlay_is_a_noop(tmp_path): + """Negative control for the test above: with no local file the effective + table is exactly the tracked one.""" + from jobd.config import load_effective_projects, load_projects + + (tmp_path / "projects.yaml").write_text( + "projects:\n alpha: {priority: 80, roots: [/srv/example/alpha]}\n" + ) + eff, _ = load_effective_projects(tmp_path / "projects.yaml", tmp_path / "overrides.yaml") + tracked = load_projects(tmp_path / "projects.yaml") + assert set(eff) == set(tracked) + assert [str(r) for r in eff["alpha"].roots] == ["/srv/example/alpha"] diff --git a/tests/test_submit_resolve_agreement.py b/tests/test_submit_resolve_agreement.py index 07f2781..935e50c 100644 --- a/tests/test_submit_resolve_agreement.py +++ b/tests/test_submit_resolve_agreement.py @@ -122,7 +122,7 @@ def test_submit_matches_resolve(client, project, profile, extra, expected_host_p # Project names are free text typed at submit time and were matched with a bare # `name in projects`, so a REGISTERED project silently lost its priority to a # differently-typed name. Measured on the live broker 2026-08-30: jobs submitted -# as `ARFDSynInt` ran at _default 40 while `arfdsynint` sat deliberately +# as `epsilon` ran at _default 40 while `epsilon` sat deliberately # registered at 65 — a difference of case alone, with no error raised and no way # to notice except by reading a warning nothing consumed. @@ -197,8 +197,8 @@ def test_a_genuinely_new_project_keeps_the_name_its_owner_chose(client): def test_a_suffix_difference_is_not_treated_as_the_same_project(client): - """Folding is case and -/_ ONLY, deliberately. `phelipanche` must not be - folded onto `phelipanche-fm` — that differs by a suffix, and equating them + """Folding is case and -/_ ONLY, deliberately. `kappa` must not be + folded onto `kappa-fm` — that differs by a suffix, and equating them would run work at another project's priority, which is the bug this matching exists to end, inverted.""" _submit(client, "pinned") diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 8ff6024..3e30321 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -24,12 +24,12 @@ def rooted_app(tmp_path, sample_profiles_yaml, sample_classifier_yaml): projects = tmp_path / "rooted-projects.yaml" projects.write_text( "projects:\n" - " jepagame:\n" + " beta:\n" " priority: 78\n" - " roots: ['/home/mjarnold/jepagame']\n" - " orchid-sdxl:\n" + " roots: ['/home/user/beta']\n" + " gamma:\n" " priority: 60\n" - " roots: ['/home/mjarnold/orchid-sdxl']\n" + " roots: ['/home/user/gamma']\n" " _default: { priority: 40 }\n" ) return build_app( diff --git a/tests/unit/test_cwd_identity_event.py b/tests/unit/test_cwd_identity_event.py index 38a30e9..8f96c1b 100644 --- a/tests/unit/test_cwd_identity_event.py +++ b/tests/unit/test_cwd_identity_event.py @@ -24,7 +24,7 @@ def test_the_event_is_emitted_with_the_label_and_the_root(rooted_logs): "/submit", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) @@ -36,9 +36,9 @@ def test_the_event_is_emitted_with_the_label_and_the_root(rooted_logs): ] hits = [e for e in rows if e["event"] == "cwd_identity_applied"] assert len(hits) == 1, f"expected exactly one, got {[e['event'] for e in rows]}" - assert hits[0]["project"] == "jepagame" + assert hits[0]["project"] == "beta" assert hits[0]["payload"]["project_label"] == "pillar2a1_sweep" - assert hits[0]["payload"]["matched_root"] == "/home/mjarnold/jepagame" + assert hits[0]["payload"]["matched_root"] == "/home/user/beta" def test_no_event_when_the_typed_name_was_already_registered(rooted_logs): @@ -49,7 +49,7 @@ def test_no_event_when_the_typed_name_was_already_registered(rooted_logs): client, logs_dir = rooted_logs r = client.post( "/submit", - json={"cmd": ["true"], "cwd": "/home/mjarnold/jepagame", "project": "jepagame"}, + json={"cmd": ["true"], "cwd": "/home/user/beta", "project": "beta"}, ) assert r.status_code == 200, r.text rows = [ diff --git a/tests/unit/test_cwd_identity_resolution.py b/tests/unit/test_cwd_identity_resolution.py index 48dbc90..9d3ca1c 100644 --- a/tests/unit/test_cwd_identity_resolution.py +++ b/tests/unit/test_cwd_identity_resolution.py @@ -17,18 +17,16 @@ def _req(project: str, cwd: str) -> JobSubmit: def _projects() -> dict[str, ProjectEntry]: return { "_default": ProjectEntry(priority=40), - "jepagame": ProjectEntry(priority=78, roots=["/home/mjarnold/jepagame"]), - "orchid-sdxl": ProjectEntry(priority=60, roots=["/home/mjarnold/orchid-sdxl"]), + "beta": ProjectEntry(priority=78, roots=["/home/user/beta"]), + "gamma": ProjectEntry(priority=60, roots=["/home/user/gamma"]), } def test_rule1_a_registered_name_wins_over_a_conflicting_cwd(): - """THE invariant. Submitted as orchid-sdxl from inside jepagame's tree: + """THE invariant. Submitted as gamma from inside beta's tree: the typed, registered name must win, so no correct submit is repriced.""" - eff = resolve_effective_config( - _req("orchid-sdxl", "/home/mjarnold/jepagame/sweeps"), _projects(), None - ) - assert eff.project == "orchid-sdxl" + eff = resolve_effective_config(_req("gamma", "/home/user/beta/sweeps"), _projects(), None) + assert eff.project == "gamma" assert eff.priority.value == 60 assert eff.matched_root is None @@ -36,21 +34,19 @@ def test_rule1_a_registered_name_wins_over_a_conflicting_cwd(): def test_rule1_still_wins_when_the_name_needed_folding(): """A name reaching rule 1 only via case/`-_` folding is still a rule-1 hit and must not be overridden by cwd.""" - eff = resolve_effective_config( - _req("Orchid_SDXL", "/home/mjarnold/jepagame"), _projects(), None - ) - assert eff.project == "orchid-sdxl" + eff = resolve_effective_config(_req("GAMMA", "/home/user/beta"), _projects(), None) + assert eff.project == "gamma" assert eff.matched_root is None - assert eff.project_label == "Orchid_SDXL" + assert eff.project_label == "GAMMA" def test_rule2_an_unregistered_label_takes_its_identity_from_cwd(): eff = resolve_effective_config( - _req("pillar2a1_sweep", "/home/mjarnold/jepagame/sweeps"), _projects(), None + _req("pillar2a1_sweep", "/home/user/beta/sweeps"), _projects(), None ) - assert eff.project == "jepagame" + assert eff.project == "beta" assert eff.priority.value == 78 - assert eff.matched_root == "/home/mjarnold/jepagame" + assert eff.matched_root == "/home/user/beta" assert eff.project_label == "pillar2a1_sweep" assert eff.unknown_project_warning is None @@ -58,10 +54,8 @@ def test_rule2_an_unregistered_label_takes_its_identity_from_cwd(): def test_rule2_supplies_project_defaults_too_not_just_priority(): """Identity means the whole entry, not only the number.""" projects = _projects() - projects["jepagame"].defaults.max_wall_s = 3600 - eff = resolve_effective_config( - _req("pillar2a1_sweep", "/home/mjarnold/jepagame"), projects, None - ) + projects["beta"].defaults.max_wall_s = 3600 + eff = resolve_effective_config(_req("pillar2a1_sweep", "/home/user/beta"), projects, None) assert eff.max_wall_s.value == 3600 assert eff.max_wall_s.source == "project_default" @@ -79,8 +73,8 @@ def test_rule3_an_unmatched_job_still_warns_as_before(): def test_the_typed_label_is_preserved_in_every_branch(): projects = _projects() for name, cwd in [ - ("orchid-sdxl", "/home/mjarnold/jepagame"), - ("pillar2a1_sweep", "/home/mjarnold/jepagame"), + ("gamma", "/home/user/beta"), + ("pillar2a1_sweep", "/home/user/beta"), ("whatever", "/tmp"), ]: eff = resolve_effective_config(_req(name, cwd), projects, None) diff --git a/tests/unit/test_cwd_identity_resolve_api.py b/tests/unit/test_cwd_identity_resolve_api.py index eec54c6..c28b8bf 100644 --- a/tests/unit/test_cwd_identity_resolve_api.py +++ b/tests/unit/test_cwd_identity_resolve_api.py @@ -5,20 +5,20 @@ def test_resolve_reports_the_root_that_supplied_the_identity(rooted_client): "/resolve", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) body = r.json() - assert body["project"] == "jepagame" + assert body["project"] == "beta" assert body["project_label"] == "pillar2a1_sweep" - assert body["matched_root"] == "/home/mjarnold/jepagame" + assert body["matched_root"] == "/home/user/beta" def test_resolve_reports_no_root_for_a_registered_name(rooted_client): r = rooted_client.post( "/resolve", - json={"cmd": ["true"], "cwd": "/home/mjarnold/jepagame", "project": "jepagame"}, + json={"cmd": ["true"], "cwd": "/home/user/beta", "project": "beta"}, ) assert r.json()["matched_root"] is None @@ -51,11 +51,11 @@ def test_explain_shows_the_root_and_the_typed_label(rooted_client, monkeypatch): r = _explain( monkeypatch, rooted_client, - ["--project", "pillar2a1_sweep", "--cwd", "/home/mjarnold/jepagame/sweeps"], + ["--project", "pillar2a1_sweep", "--cwd", "/home/user/beta/sweeps"], ) assert r.exit_code == 0, r.output assert "pillar2a1_sweep" in r.output - assert "/home/mjarnold/jepagame" in r.output + assert "/home/user/beta" in r.output def test_explain_says_nothing_about_roots_when_cwd_was_not_consulted(rooted_client, monkeypatch): @@ -64,8 +64,8 @@ def test_explain_says_nothing_about_roots_when_cwd_was_not_consulted(rooted_clie r = _explain( monkeypatch, rooted_client, - ["--project", "jepagame", "--cwd", "/home/mjarnold/jepagame"], + ["--project", "beta", "--cwd", "/home/user/beta"], ) assert r.exit_code == 0, r.output - assert "resolved config for project jepagame" in r.output + assert "resolved config for project beta" in r.output assert "root" not in r.output diff --git a/tests/unit/test_list_filter_matches_either_name.py b/tests/unit/test_list_filter_matches_either_name.py index 3556990..23484ea 100644 --- a/tests/unit/test_list_filter_matches_either_name.py +++ b/tests/unit/test_list_filter_matches_either_name.py @@ -11,13 +11,13 @@ def test_filtering_by_the_label_finds_the_job(rooted_client): "/submit", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) rows = rooted_client.get("/jobs", params={"project": "pillar2a1_sweep"}).json() assert len(rows) == 1 - assert rows[0]["project"] == "jepagame" + assert rows[0]["project"] == "beta" def test_filtering_by_the_identity_finds_the_same_job(rooted_client): @@ -25,11 +25,11 @@ def test_filtering_by_the_identity_finds_the_same_job(rooted_client): "/submit", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) - rows = rooted_client.get("/jobs", params={"project": "jepagame"}).json() + rows = rooted_client.get("/jobs", params={"project": "beta"}).json() assert len(rows) == 1 @@ -40,8 +40,8 @@ def test_an_unrelated_project_filter_still_matches_nothing(rooted_client): "/submit", json={ "cmd": ["true"], - "cwd": "/home/mjarnold/jepagame/sweeps", + "cwd": "/home/user/beta/sweeps", "project": "pillar2a1_sweep", }, ) - assert rooted_client.get("/jobs", params={"project": "orchid-sdxl"}).json() == [] + assert rooted_client.get("/jobs", params={"project": "gamma"}).json() == [] diff --git a/tests/unit/test_project_from_cwd.py b/tests/unit/test_project_from_cwd.py index 59a8b33..cc44619 100644 --- a/tests/unit/test_project_from_cwd.py +++ b/tests/unit/test_project_from_cwd.py @@ -1,8 +1,8 @@ """cwd -> project identity matching. The boundary case is the whole point of this module. A `str.startswith` root -match is the obvious implementation and it is wrong: `/home/mjarnold/jepagame2` -starts with `/home/mjarnold/jepagame`, so a sibling project would silently +match is the obvious implementation and it is wrong: `/home/user/beta2` +starts with `/home/user/beta`, so a sibling project would silently inherit its neighbour's priority -- the exact failure class this feature exists to end, pointing the other way. """ @@ -18,43 +18,43 @@ def _projects(**roots_by_name): def test_a_cwd_inside_a_root_resolves_to_that_project(): - projects = _projects(jepagame=["/home/mjarnold/jepagame"]) - assert project_from_cwd(projects, "/home/mjarnold/jepagame/sweeps/a") == ( - "jepagame", - "/home/mjarnold/jepagame", + projects = _projects(beta=["/home/user/beta"]) + assert project_from_cwd(projects, "/home/user/beta/sweeps/a") == ( + "beta", + "/home/user/beta", ) def test_the_root_itself_matches(): - projects = _projects(jepagame=["/home/mjarnold/jepagame"]) - assert project_from_cwd(projects, "/home/mjarnold/jepagame") == ( - "jepagame", - "/home/mjarnold/jepagame", + projects = _projects(beta=["/home/user/beta"]) + assert project_from_cwd(projects, "/home/user/beta") == ( + "beta", + "/home/user/beta", ) def test_a_sibling_directory_sharing_a_name_prefix_does_not_match(): """The regression this module exists to prevent.""" - projects = _projects(jepagame=["/home/mjarnold/jepagame"]) - assert project_from_cwd(projects, "/home/mjarnold/jepagame2") is None - assert project_from_cwd(projects, "/home/mjarnold/jepagame-scratch") is None + projects = _projects(beta=["/home/user/beta"]) + assert project_from_cwd(projects, "/home/user/beta2") is None + assert project_from_cwd(projects, "/home/user/beta-scratch") is None def test_the_deepest_root_wins(): """Nesting is legitimate: a sub-project living inside a parent's tree must take its own identity, not the parent's.""" projects = _projects( - jepagame=["/home/mjarnold/jepagame"], - jepagame_xcheck=["/home/mjarnold/jepagame/xcheck"], + beta=["/home/user/beta"], + beta_xcheck=["/home/user/beta/xcheck"], ) - assert project_from_cwd(projects, "/home/mjarnold/jepagame/xcheck/run1") == ( - "jepagame-xcheck", - "/home/mjarnold/jepagame/xcheck", + assert project_from_cwd(projects, "/home/user/beta/xcheck/run1") == ( + "beta-xcheck", + "/home/user/beta/xcheck", ) def test_an_unmatched_cwd_returns_none(): - projects = _projects(jepagame=["/home/mjarnold/jepagame"]) + projects = _projects(beta=["/home/user/beta"]) assert project_from_cwd(projects, "/tmp") is None @@ -62,7 +62,7 @@ def test_a_project_with_no_roots_never_matches(): """Positive control for the opt-in: roots are opt-in, and a project that declares none must not be reachable by cwd at all.""" projects = {"_default": ProjectEntry(priority=40), "plain": ProjectEntry(priority=60)} - assert project_from_cwd(projects, "/home/mjarnold/plain") is None + assert project_from_cwd(projects, "/home/user/plain") is None def test_two_projects_claiming_the_same_depth_yield_no_identity(caplog): @@ -70,30 +70,30 @@ def test_two_projects_claiming_the_same_depth_yield_no_identity(caplog): canonical_project_name's posture on an ambiguous fold (config.py:355). Picking one would run someone's jobs at a neighbour's priority.""" projects = _projects( - alpha=["/home/mjarnold/shared"], - beta=["/home/mjarnold/shared"], + alpha=["/home/user/shared"], + beta=["/home/user/shared"], ) with caplog.at_level("WARNING"): - assert project_from_cwd(projects, "/home/mjarnold/shared/x") is None + assert project_from_cwd(projects, "/home/user/shared/x") is None assert "alpha" in caplog.text and "beta" in caplog.text def test_an_ambiguous_shallow_root_still_loses_to_a_deeper_unambiguous_one(): """Ambiguity at depth N must not poison a clear winner at depth N+1.""" projects = _projects( - alpha=["/home/mjarnold/shared"], - beta=["/home/mjarnold/shared"], - gamma=["/home/mjarnold/shared/g"], + alpha=["/home/user/shared"], + beta=["/home/user/shared"], + gamma=["/home/user/shared/g"], ) - assert project_from_cwd(projects, "/home/mjarnold/shared/g/run") == ( + assert project_from_cwd(projects, "/home/user/shared/g/run") == ( "gamma", - "/home/mjarnold/shared/g", + "/home/user/shared/g", ) def test_default_is_never_returned_as_an_identity(): - projects = {"_default": ProjectEntry(priority=40, roots=["/home/mjarnold"])} - assert project_from_cwd(projects, "/home/mjarnold/anything") is None + projects = {"_default": ProjectEntry(priority=40, roots=["/home/user"])} + assert project_from_cwd(projects, "/home/user/anything") is None # --- `..` in the submitted cwd ------------------------------------------ @@ -101,7 +101,7 @@ def test_default_is_never_returned_as_an_identity(): # `--cwd` is a free-text CLI flag and `JobSubmit.cwd` is a bare `str` with no # normalization, so a caller can submit a path whose textual components say one # thing and whose meaning says another. Comparing raw components made a job -# actually running in /tmp price at jepagame's priority. Collapsing `..` +# actually running in /tmp price at beta's priority. Collapsing `..` # LEXICALLY is the fix: the broker has no access to the worker's filesystem, so # resolving symlinks is both wrong here and unavailable. @@ -109,27 +109,27 @@ def test_default_is_never_returned_as_an_identity(): def _rooted(): """The two shipped roots the `..` cases traverse between.""" projects = {"_default": ProjectEntry(priority=40)} - projects["jepagame"] = ProjectEntry(priority=78, roots=["/home/mjarnold/jepagame"]) - projects["orchid-sdxl"] = ProjectEntry(priority=60, roots=["/home/mjarnold/orchid-sdxl"]) + projects["beta"] = ProjectEntry(priority=78, roots=["/home/user/beta"]) + projects["gamma"] = ProjectEntry(priority=60, roots=["/home/user/gamma"]) return projects def test_dotdot_out_of_a_root_and_into_another_takes_the_other_projects_identity(): - """`/home/mjarnold/jepagame/../orchid-sdxl` IS orchid-sdxl. Component-wise - matching on un-normalized parts read the leading `jepagame` component and - handed the job jepagame's 78.""" - assert project_from_cwd(_rooted(), "/home/mjarnold/jepagame/../orchid-sdxl") == ( - "orchid-sdxl", - "/home/mjarnold/orchid-sdxl", + """`/home/user/beta/../gamma` IS gamma. Component-wise + matching on un-normalized parts read the leading `beta` component and + handed the job beta's 78.""" + assert project_from_cwd(_rooted(), "/home/user/beta/../gamma") == ( + "gamma", + "/home/user/gamma", ) def test_dotdot_onto_a_prefix_sibling_does_not_match(): """The sibling-prefix regression, reachable through `..`: this path means - `/home/mjarnold/jepagame2`, which the direct spelling already refuses.""" - assert project_from_cwd(_rooted(), "/home/mjarnold/jepagame/../jepagame2") is None + `/home/user/beta2`, which the direct spelling already refuses.""" + assert project_from_cwd(_rooted(), "/home/user/beta/../beta2") is None def test_dotdot_escaping_the_home_tree_entirely_does_not_match(): - """The worst case: a job really running in /tmp, priced at jepagame's 78.""" - assert project_from_cwd(_rooted(), "/home/mjarnold/jepagame/../../tmp") is None + """The worst case: a job really running in /tmp, priced at beta's 78.""" + assert project_from_cwd(_rooted(), "/home/user/beta/../../tmp") is None