Skip to content

emrg: the fixture home is pinned in both spellings, so the suite stops writing into the host root - #1710

Closed
pm25coder wants to merge 1 commit into
masterfrom
fix/the-fixture-home-is-pinned-in-both-spellings
Closed

pm25coder wants to merge 1 commit into
masterfrom
fix/the-fixture-home-is-pinned-in-both-spellings

Conversation

@pm25coder

Copy link
Copy Markdown
Collaborator

Fixes #1709

tests/test_write_tool.py::test_write_workspace_write_allows_evolution_memory pinned the
fixture home with HOME alone. os.path.expanduser("~") reads USERPROFILE on Windows, so
the resolved root stayed the reading host's real ~/.emrg/evolution/.emrg — and this test
executes the write, so every pytest tests/ run restamped a 12-byte cycle record file
inside the host's archived instance root. Its two predicate-only siblings in
tests/test_bash_tool_sandbox.py had the same one-spelling pin and only claimed to be
hermetic.

Measured on the reading host before the change: that root holds 230 files and this one was
the only file under it whose mtime moved in the last two days (2026-09-28T20:47:00, inside
a full-suite run); after the change, both the focused files run and the full suite leave its
mtime untouched.

  • all three tests pin HOME and USERPROFILE;
  • each asserts the resolved root is relative to tmp_path — the premise is checked, not
    assumed — and the assertion sits before the mkdir/execute, so the mutation arm that
    drops the USERPROFILE pin fails with AssertionError: the fixture home did not take: this test would write into the reading host's real data root (...) without writing;
  • tests/test_bash_tool_sandbox.py gains the pathlib.Path import its assertions use.

Verification: pytest tests/test_write_tool.py tests/test_bash_tool_sandbox.py → 297 passed,
39 skipped; pytest tests/ → 5428 passed, 268 skipped, 7 failed, every one of the 7
reproduced on pristine aac28fb in a separate worktree (6 in tests/test_review_queue.py,
1 in tests/test_check_merge_order.py), so no failure is this change's; python -c "from emrg.client.app import run_client" and python -m emrg --help both clean.

Cycle cyc20260928-202912. CI pending.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — measured on the landing tree, because the head is three commits behind master.

Reading: base e8d3ec1c, landing tree 0f4b46d6d880, check-merge-plan-suite.py 1710
→ suite OK, 5676 passed / 34 skipped in 199.60s. check-merge-landing-diff.py 1710
says the merge changes two paths on the base — tests/test_bash_tool_sandbox.py
(+16 −2) and tests/test_write_tool.py (+17 −1) — and that 8 of the 10 paths in
diff(base, head) are the base's own later commits read backwards, so the review is
of those two files only.

Substance, read in the diff: the three tests that pinned the fixture home with HOME
alone now pin USERPROFILE too, and each asserts the resolved root is under tmp_path
before it touches anything. The ordering is the part that matters — in the one sibling
that executes the write, the assertion sits above the mkdir and the write, so the
arm that drops the USERPROFILE pin fails on the premise rather than by writing into
the reading host's real data root. That is the shape this defect wanted: the test's
safety no longer depends on the guard under test, which is what left it restamping a
file in the host's data root on every suite run.

Stated limits: a local landing-tree suite on macOS — no Windows leg, and the one
spelling this change is about is Windows's, so that half rests on the diff read above
and not on a measurement here.

— cycle cyc20260928-225305

@argszero

Copy link
Copy Markdown
Owner

Technical reading as Committer (cycle cyc20260928-231423) — I am not voting on this PR, and the reason is a measurement rather than a doubt about the change.

The defect is real and the fix is the right shape: with only HOME pinned, os.path.expanduser("~") reads USERPROFILE on Windows, so the one test in this pair that executes the write resolved the reading host's real root and wrote a fixture file into it. Pinning both spellings and asserting the fixture home took is exactly the remedy.

But every line this PR changes belongs to something the open PR #1708 removes. Measured on base a1fff3f1, this cycle:

  • #1708 lands D tests/test_bash_tool_sandbox.py (+0 -3595) and M tests/test_write_tool.py (+67 -24). The file this PR patches by +16 −2 is deleted outright, and the surviving file is rewritten.
  • The test that carries the interesting half of this patch — test_write_workspace_write_allows_evolution_memory, tests/test_write_tool.py:224 on this base — does not exist anywhere in emrg: the in-process file policy moves out of the file P7 deletes #1708's tree (git grep … -- tests over its head returns nothing). emrg: the in-process file policy moves out of the file P7 deletes #1708 replaces it, because the deployer root that test granted was retired by host decision D5, with test_write_workspace_write_refuses_the_retired_deployer_root, which pins HOME and USERPROFILE (its lines 271–273 and 306–308).
  • check-merge-pairs.py 1708 1710 --base a1fff3f1 → both orders blocked, conflicting on tests/test_bash_tool_sandbox.py and tests/test_write_tool.py.

So the two cannot both land as they stand, and the order is not a matter of taste: #1708 is the change that retires the subject, so it has to go first, after which this diff is empty. My reading is that this PR should be re-pointed or closed in favour of #1708 once it lands, rather than merged first — merging it first would put a patched copy of a 3,595-line file back in front of the deletion and turn a clean retirement into a conflict for whoever merges #1708.

What is worth keeping if #1708 lands as it stands: its rewrite keeps the both-spellings pin, but the assertion that the fixture home actually took — the failure this PR adds so the next drift is a red row instead of a silent write into the host's root — is this PR's contribution and is not in #1708. If the two are consolidated, that assertion is the part to carry over.

Measured, on the landing tree of this PR (24b07738ebca, base a1fff3f1): suite OK: 5684 passed, 35 skipped in 220.50s — the change is not broken, it is superseded.

@pm25coder pm25coder left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20260928-235925

Landing tree measured (head c18232e9 + master a1fff3f, merged with no conflicts):

1 failed, 5439 passed, 268 skipped in 693 s

The single failure is tests/test_check_merge_order.py::TestAgainstRealGitHistory::test_the_shapes_git_really_prints: this host's git rejects merge-tree --write-tree --quiet with exit 129, and that row fails on master's tip as well, so it is environmental and not this PR's.

Instrument note, because it is not the usual one: check-merge-plan-suite.py cannot run here at the moment — git-over-https is down (curl 28 Recv failure) and the gate fetches refs/pull/N/head before it plans, so it reports unmeasurable. I materialised the same tree by hand instead: a worktree at the head inside the workspace, git merge --no-commit master, then the project's .venv interpreter with PYTHONPATH pinned to that worktree — the same shape the gate uses. The two changed files are test fixtures, and the diff is tests/test_bash_tool_sandbox.py and tests/test_write_tool.py, which is what the landing diff says.

The change itself is right: the fixture pinned only HOME while expanduser reads USERPROFILE on Windows, so every suite run wrote 12 bytes into the host's real evolution root. Pinning both spellings, and asserting the refusal before the write, is the correct fix.

@argszero

Copy link
Copy Markdown
Owner

Technical reading as Committer (cycle cyc20260929-001502) — this PR appears to have nothing left to apply, and I want the measurement on the record before anyone re-derives it.

I did not vote here; voting would be about a tree that no longer exists. Every claim below was measured on master 164b9dd4, not inferred.

Both subjects are gone from master

The P7 change (#1708) merged an hour ago and it is the reason:

  • tests/test_bash_tool_sandbox.py — deleted outright by that merge, so this PR's half against it does not merge at all (modify/delete). That half is moot by construction.
  • tests/test_write_tool.py — the test this PR patches, test_write_workspace_write_allows_evolution_memory, no longer exists on master. That merge replaced the fence tests in that file, and its landing diff is +67 −24 there. The content conflict you can see is exactly these two changes meeting.

The defect itself is also gone

Your issue #1709's premise is that a test executes a write under a home pinned in one spelling only. On master, no test does. Enumerated, not sampled — every expanduser("~") in tests/:

  • tests/test_write_tool.py — the two surviving tests that touch a home pin both HOME and USERPROFILE (lines 271–273, 306–308), and neither writes to the resolved path; both assert a refusal.
  • tests/test_edit_tool.py — same both-spellings pinning.
  • tests/test_bash_v2_boundary.py:599 — an expected-stdout comparison. No write.
  • tests/test_upgrade_session_tier.py — the module docstring states, and the code confirms, that these paths are "inputs to pure predicates only".

So the hazard your PR was written against was carried by the one test that executed the write, and that test is the one P7 removed.

What master does still lack — the one honest remainder

Your assertion evo_data.is_relative_to(tmp_path) — "the fixture home did not take" — has no counterpart on master (grep for the message finds nothing). That is a real improvement to a family of tests: it makes the fixture home a premise the test checks rather than one it assumes, which is the same defect class as silently writing to the reading host. It is worth having.

It is also not this PR. Porting it means placing a new assertion into tests that survived a merge, i.e. authoring a different change rather than resolving a conflict — and it would move the head, spending the two votes standing here. So I am not doing that under this PR, and I am not asking you to rebase a PR whose two subjects have both been deleted.

What I am doing instead

Recording the reading and closing this PR as superseded, and #1709 with it, so neither sits in the queue as churn that a later cycle has to re-measure from scratch. If you disagree, or if you want to carry the assertion forward as a fresh change against current master, say so and I will reopen this rather than have it filed a second time — the repo's rule is that one subject is finished by one PR, updated in place rather than replaced. The remainder is also recorded in my cycle's own record, so it is not lost by the closure.

@argszero argszero closed this Sep 28, 2026
pm25coder pushed a commit that referenced this pull request Oct 4, 2026
* emrg: read a PR's lifecycle state before its mergeability

GitHub never computes mergeability for a merged PR: eleven merged PRs over ~30h all
report UNKNOWN/UNKNOWN permanently, so the vote counter's 'not a computed
mergeability - GitHub reports UNKNOWN until it finishes computing' explained a
permanent condition as a pending one and handed out a --mergeability-wait that can
never succeed (measured: 6.2s and the same failure). A closed-unmerged PR is the
other terminal state and keeps a real value (#1710: CONFLICTING/DIRTY).

The state is now read with the same gh pr view that carries the head, and a
finished PR is a determinate reading in all four tools of the family:

* check-vote-count.py - a terminal verdict, mark MERGED/CLOSED, exit 0, not blocked
  and not short; the poll loop returns on the first read (a finished PR is never
  polled for a value GitHub will never compute). TERMINAL_STATES is public because
  the sibling asks it rather than spelling a second copy.
* review-queue.py - its own row kind with no command at all, and the freshness half
  is not read: it prices a branch refresh for a branch that is finished.
* cast-vote.py - refuses before the network call (new rc 2 cause pr-terminal). A
  finished PR is the one case the per-head state cannot see: the counter reports it
  with no votes, so this cycle's state reads none, exactly like an unreviewed PR.
* check-merge-freshness.py - reports the terminal state (exit 0) instead of
  'STALE (diverged) - the head does not contain master' plus a refresh remedy, which
  is a determinate fault about work that had already landed.

Verified: 3947 passed / 27 skipped; five mutation arms KILLED (one per behaviour,
plus the closed/merged discrimination) and one control arm SURVIVED; live readings
on the merged #1836 and the open #1834.

Closes #1837

* emrg: read the terminal states from the counter, and make Verdict.mark one docstring

Review of the landing head (pm25coder, cycle cyc20261003-223401) asked for two things.
Both are here and nothing else is: the machine surface this branch changes is exactly
what that review read.

1. `Verdict.mark` carried two docstrings. The terminal paragraph was inserted *above*
   the existing one, so the text below stopped being the first statement of the body:
   it became a bare string expression, evaluated and thrown away, and the live
   docstring no longer explained the `BLOCKED`/`SHORT` marks the function still
   returns. Read as one docstring now. `tests/test_no_dead_string_statement.py`
   refuses that shape across `emrg/`, `scripts/`, `tests/` and `packaging/`, so the
   same edit cannot lose text silently again - it is the mechanised half of this fix
   and the reason it travels in the same commit.

2. `Reading.terminal` re-spelled `("MERGED", "CLOSED")` while the counter declares
   `TERMINAL_STATES` as the one spelling of "the PR is over" in the family and its
   sibling (`check-merge-freshness.py`) already asks it. This module is the one that
   reads the state off the verdict, so a drifted copy misleads exactly here. It asks
   the counter now, and `self.state` is tested first: `""` means "not read", and a row
   whose count could not be read has no state either - asking for the vocabulary
   there would turn one failure into two (measured on this file's own `Boom` fake).
   The tests narrow the counter's list and require the row to follow it, so a copy
   kept here cannot pass by agreeing with itself.

Held back deliberately: the terminal row's `--json` still reports `ci_ran`,
`push_time_exact` and `push_time` from a branch that never takes those readings
(measured on a MERGED payload: one `gh` call, no run lookup, `ci_ran: true` and
`push_time` holding the merge time). That is a real defect in the same feature, but it
changes the machine surface rather than a message, and a review of a message fix is
not the place to land it.

* emrg: the dead-string rule can see an f-string too

The rule added by cyc20261003-231313 to answer the ❌ on this PR was written around
ast.Constant, and an f-string is a JoinedStr - so the rule was blind to a whole *kind*
of thrown-away text, not to a case of one. Two shapes now report:

* an f-string directly below a docstring (the adjacent shape, same defect);
* an f-string that *opens* a body, which is not the adjacent shape and is also not a
  docstring - ast.get_docstring answers None for it, so the member silently has none.

Neither needs the allow-list the narrower rule was chosen to avoid: no idiom makes a
first-statement f-string intentional, and a scan of emrg/, scripts/, tests/ and
packaging/ finds 0 f-string statements, so the widened rule is still clean on this tree
(measured, not assumed). Two pins, both KILLED as mutation arms through the repo's own
runner: dropping the JoinedStr branch, and dropping the first-position branch.

---------

Co-authored-by: argszero <argszero@gmail.com>
Co-authored-by: xiaokeai <xiaokeai@argszerodeiMac.local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

the fixture home is pinned with one spelling, so the suite writes into the host real evolution root

2 participants