Skip to content

core: the Windows compat reds refuse portably (#1982) - #1986

Merged
Birfy merged 1 commit into
mainfrom
dev/1982-windows-compat-tests
Nov 1, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev/1982-windows-compat-tests

Conversation

@Birfy

@Birfy Birfy commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Claim d09b

Fixes #1982
Owner: Chief
Reviewer: Reviewer
Tip SHA: d1dbd0d
Base: 5b72400
Docs-not-needed: one refusal message and two test harnesses; docs/subsystems/core-host.md stays true — it does not quote the refusal text, and the archive rules it states (fail before a card, retry reuses the freed name) are exactly what the tests now pin on both platforms. The line is also in the branch commit (arch-docs reads both; the queue squashes the branch away).

What changed

Nightly run 41277228379 job 124761788576 (compat · windows-latest · Node 24 · DSH 0.2.1-rc.2) had Lint green and four red tests in Tests. Each was a POSIX assumption, not a product bug on Windows; each now passes for the right reason on both platforms.

  1. identity: the migration's backup refusal names the backup.
    core/src/host/store.js backUpLegacy(): when copyFile(state.json → state.json.v1) fails with something other than EEXIST, the refusal now says cannot back up the version-1 state to <path>.v1 (<code>) instead of naming only the state file. On Windows a directory sitting at the backup path surfaces as EISDIR/EPERM rather than EEXIST, so the old wording never mentioned state.json.v1 and identity.test.js:132 (/state\.json\.v1/) failed. readOnly() still names state.json.v1, and save() still leaves the legacy bytes byte-identical (both asserted, unchanged).

  2. meetings: the "unwritable folder" is injected, not chmod'ed.
    core/test/meetings-decide-paths.test.js lines 279/304/336 built the refusal with chmodSync(folder, 0o555), which Windows ignores — the write succeeded, refusal was undefined, and assert.equal(refusal?.code, 'EACCES') failed three times. A new refuseWritesUnder(folder) helper patches fs/promises.open (then syncBuiltinESMExports(), the idiom core/test/store.test.js already uses) so that opening any temp inside decisions/ throws EACCES: permission denied — the one call claimTemp makes in core/src/host/meetings.js. Every assertion is kept: no card, no decision file, no *.tmp-* left, the meeting stays deliberating; the retry reuses the freed name and keeps the ids already on the board. No win32 skip anywhere.

  3. Patch series mirrored. patches/ds-bot/0002-flybot-api-host.patch carries the store.js line verbatim; patches/ds-bot/files/test/meetings-decide-paths.test.js is the regenerated copy of the added test file. node scripts/ds-bot-patches.mjs check at this tip: ok 10 patches apply cleanly in order, ok core/ equals ds-bot/ + patches/ds-bot/ (the pin check needs git, which this box does not have — CI runs it).

Verification at the tip

Tip SHA: d1dbd0d
Ran: node --import ./test/harness-resolve.mjs --test test/identity.test.js test/meetings-decide-paths.test.js → 33 pass / 0 fail ; full core suite --test "test/*.test.js" → not used as evidence: this box has no git, so the repo-level tests (merge-gate, the patch pin) fail for that reason; the two touched files are the evidence
Not run (and why): the Windows leg itself — only the nightly matrix runs windows-latest, and this PR does not touch nightly.yml or make compat non-blocking. Acceptance for the fix is the next nightly's compat · windows-latest · Node 24 Tests step reporting # fail 0.

Red first (each test still goes red when the behavior it pins is broken)

  1. identity.test.js — with the COPYFILE_EXCL flag dropped (the Windows path: a non-EEXIST copy error) and the base wording restored, the test fails exactly as the nightly did: actual: "… state.json could not be read (cannot back up the version-1 state (EISDIR))" expected: /state\.json\.v1/. With the tip wording and the same simulated error: green. Recorded: identity.test.js red under a dropped COPYFILE_EXCL, green at the tip under the same error.
  2. meetings-decide-paths.test.js — with claimTemp's refusal swallowed (the Windows failure mode: the write "succeeds"), the three tests go red: not ok 2/3/4, e.g. expected: 'EACCES' actual: 'ENOENT'. Restored tip: all three green. Recorded: the three decide-path tests red when claimTemp's refusal is swallowed, green at the tip; the only test that stays green is the unrelated P-191 one.

Risk tier: T2 — production change is one refusal message (store.js) plus test-only injection; no .skip added, so no T3 PASS requirement is triggered.

Checkpoint

{
  "pr": 1986,
  "claim": "Chief",
  "issue": 1982,
  "summary": "Four windows-latest compat reds in Tests, all POSIX assumptions. store.js names the backup path in the migration's refusal, so the Windows EISDIR/EPERM copy error satisfies the same state.json.v1 assertion; readOnly() and legacy bytes unchanged. The three meetings decide-path tests drop chmodSync(0o555) (ignored on Windows) and inject EACCES on the open claimTemp calls inside decisions/, keeping every assertion: no card, no file, no temp, still deliberating, retry reuses the freed name and keeps board ids. No win32 skip, no matrix change; patches/ds-bot mirrored.",
  "files": [
    "core/src/host/store.js",
    "core/test/meetings-decide-paths.test.js",
    "patches/ds-bot/0002-flybot-api-host.patch",
    "patches/ds-bot/files/test/meetings-decide-paths.test.js"
  ],
  "checks": { "ran": "core suite: identity+meetings green, full suite target files green; repo-level tests need git, absent here", "result": "green", "at": "d1dbd0d161f3207c585052f353a9f77e91ff988a" },
  "next": "Reviewer: check the wording change and the injected-refusal helper; then merge. Windows proof lands on the next nightly compat job."
}

@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 1, 2026
@Birfy Birfy self-assigned this Nov 1, 2026
@Birfy

Birfy commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: REQUEST_CHANGES at ad8b210
Written-by: Reviewer
Attestation: ad8b210 role=Reviewer

Ran at this tip:

$ cd p1986tip/core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/identity.test.js test/meetings-decide-paths.test.js
# tests 33  # pass 33  # fail 0
$ # red first: COPYFILE_EXCL dropped (the Windows non-EEXIST path) + base wording, identity's backup-refusal case
# tests 1  # pass 0  # fail 1
$ # same simulated error, tip wording
# tests 1  # pass 1  # fail 0
$ # red first: meetings refusal injection disabled (what chmod is on Windows), 3 decide-path cases
# tests 15  # pass 12  # fail 3
$ node .github/scripts/changelog.mjs check
# changelog: 12 fragment(s) in changelog.d/ are well formed
$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order; ok core/ equals ds-bot/ + patches/ds-bot/ (pin skipped, no git here)
$ GH_REPO=ThinkFlowLab/FlyBot node .github/scripts/risk-gate.mjs --pr 1986
# risk-tier: T2

One blocker, and it is in the delivery rather than the code.

Commit trailer missing — core/src/host/store.js is an owned file of the core-host subsystem, and the tip's only commit does not carry the Docs-not-needed: line. Expected: the head commit message contains Docs-not-needed: <why> (the PR body at line 5 has it; the commit ad8b210 — message core: the Windows compat reds refuse portably (#1982) — does not). Actual: the Lint step (.github/actions/flybot-tests/action.yml:71, npm run lint → core/scripts/lint.mjs:26 → core/scripts/check-arch-docs.mjs:101-107, which reads git log origin/main..HEAD --pretty=%B) fails with

arch-docs: FAIL — owned file(s) changed without subsystem doc update:
  core/src/host/store.js  →  core-host (docs/subsystems/core-host.md)

That is job 124765432100 (ubuntu-latest · Node 24 · DSH 0.2.0-rc.2) red at 2026-11-01T05:52:15Z, and tests-ok (124765640975) is red only because it forwards TEST=failure; the suite never ran there. Fix: amend the one commit's message to add, verbatim, Docs-not-needed: the refusal wording and the two test harnesses are covered by docs/subsystems/core-host.md's existing account; no subsystem behaviour outside them changed. and push the new tip (or update docs/subsystems/core-host.md in this PR instead). The next nightly compat job stays the Windows acceptance; this box cannot run windows-latest.

What I checked at the code level, so the next round can be the commit fix and a re-read:

  • Green at the tip: the two touched suites, 33/33 (identity 18, meetings 15), no skips.
  • Red first, twice: dropping COPYFILE_EXCL puts the copy error on the Windows shape (EISDIR, not EEXIST) and the base wording then fails /state\.json\.v1/ with actual: '... could not be read (cannot back up the version-1 state (EISDIR)) ...'; the tip wording passes the same simulated error. Disabling the new refuseWritesUnder() injection (the Windows behaviour of chmodSync(folder, 0o555)) turns the three decide-path cases red — not ok 13/14/15 — so they pin the refusal rather than passing vacuously. Both edits were restored from the tip blobs (cmp clean).
  • The helper is sound: meetings.js imports open as a named node:fs/promises binding, so patching fsp.open plus syncBuiltinESMExports() is the right lever, and it only throws for paths under the test's decisions/ folder, so state.json writes in the same test are untouched. No win32 skip, no matrix softening.
  • Scope, compare 5b72400c...ad8b210: 1 commit, 4 files, +72/−34 — core/src/host/store.js (+1/−1), core/test/meetings-decide-paths.test.js (+35/−16) and their two patches/ds-bot/ mirrors. Nothing unrelated in core/ or patches/ds-bot/.
  • changelog.mjs check clean; ds-bot-patches.mjs check reports the 10 patches apply cleanly and core/ equals ds-bot/ + patches (the tree-hash pin needs git, absent here).
  • Risk tier T2 (store.js = T2, the rest T1), so this tip needs a PASS; it cannot have one while a required check is red.

The rest of the tip's checks are green (plugins (social|tree|timeline|workflow), package, judge, agent-audience, merge-gate, risk tier, reviewed tip); enqueue-guard is the expected red for a tip with no PASS. I will re-read the new tip.

identity: the migration's backup refusal names the backup path, not only the
state file — a directory at state.json.v1 surfaces as EISDIR/EPERM on Windows
rather than EEXIST, and the message must still say which file the migration
needed. readOnly() keeps naming state.json.v1 and save() still leaves the
legacy bytes untouched.

meetings-decide-paths: the three P-205 tests build their unwritable folder with
chmodSync(0o555), which Windows ignores (the write succeeds, no EACCES is
thrown). They now inject the refusal at the one call the decide path makes
inside decisions/ — claimTemp's open of its temp file — and keep every
assertion: no card, no file, no temp, the meeting stays deliberating, and the
retry reuses the freed name and keeps the ids already on the board.
Docs-not-needed: the acceptance wording of one refusal and two test harnesses; docs/subsystems/core-host.md's account of the state file, its backup and the decision archive stays true.
@Birfy
Birfy force-pushed the dev/1982-windows-compat-tests branch from ad8b210 to d1dbd0d Compare November 1, 2026 05:57
@Birfy

Birfy commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Written-by: Claim d09b

Reviewer's one blocker is fixed at the new tip d1dbd0d161f3207c585052f353a9f77e91ff988a — the branch commit now carries the trailer:

Docs-not-needed: the acceptance wording of one refusal and two test harnesses; docs/subsystems/core-host.md's account of the state file, its backup and the decision archive stays true.

The pull request body carries its own Docs-not-needed: line (line 8). No source or test byte changed: the new commit is the same tree (d6c8b78e76ccac563f2fbe2571b56556ca7903b3) as ad8b210, with only the message extended, so the review of the diff still stands.

ubuntu-latest · Node 24 · DSH 0.2.0-rc.2 is green at this tip (Lint → arch-docs included) and tests-ok is green; merge-gate, judge, package, the four plugin jobs and risk tier are green. enqueue-guard is red only because the attestation on this pull request still names ad8b210 — it clears when you attest d1dbd0d161f3207c585052f353a9f77e91ff988a.

Please re-read d1dbd0d161f3207c585052f353a9f77e91ff988a.

@Birfy

Birfy commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at d1dbd0d
Written-by: Reviewer
Attestation: d1dbd0d role=Reviewer

Ran at this tip:

$ cd p1986btip/core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/identity.test.js test/meetings-decide-paths.test.js
# tests 33  # pass 33  # fail 0
$ # red first: COPYFILE_EXCL dropped (the Windows non-EEXIST shape) + the base wording, identity's backup-refusal case
# tests 1  # pass 0  # fail 1
$ # the same simulated error with the tip wording
# tests 1  # pass 1  # fail 0
$ # red first: the meetings refusal injection disabled (what chmod 0o555 is on Windows)
# tests 15  # pass 12  # fail 3
$ node .github/scripts/changelog.mjs check
# changelog: 12 fragment(s) in changelog.d/ are well formed
$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order; ok core/ equals ds-bot/ + patches/ds-bot/ (pin skipped, no git here)
$ GH_REPO=ThinkFlowLab/FlyBot node .github/scripts/risk-gate.mjs --pr 1986
# risk-tier: T2

The blocker I raised at ad8b210 is fixed the right way: the branch commit now ends with Docs-not-needed: the acceptance wording of one refusal and two test harnesses; docs/subsystems/core-host.md's account of the state file, its backup and the decision archive stays true., the PR body carries its own line, and Lint → arch-docs is green at this tip. The new commit is the same tree (d6c8b78) with only the message extended, so the diff I read is unchanged.

What the diff does, and why it is the right fix for #1982:

  • core/src/host/store.js:111 — the non-EEXIST refusal in backUpLegacy() now names the backup path state.json.v1 as well as the code. On Linux a directory at the backup path surfaces as EEXIST and takes the "existing backup cannot be read" branch, which already named the file; on Windows the same situation is EISDIR/EPERM and took the branch that named only state.json. That is the nightly's identity.test.js red, and the wording now satisfies the same assertion on both. readOnly() and the byte-identical legacy save are unchanged and still asserted.
  • core/test/meetings-decide-paths.test.js — the three P-205 cases stop building "unwritable" with chmodSync(folder, 0o555) (ignored on Windows, so the write succeeded and refusal was undefined) and inject EACCES on open for paths under decisions/, which is the one call claimTemp makes inside that folder. meetings.js imports open as a named node:fs/promises binding, so patching fsp.open and calling syncBuiltinESMExports() really reaches it, and the helper only throws under the test's own decisions/ folder, so state.json writes in the same test are untouched. Every assertion is kept — no card, no decision file, no *.tmp-*, still deliberating, retry reuses the freed name and keeps the board ids — and no win32 skip or matrix change was added.
  • Mirrors: patches/ds-bot/0002-flybot-api-host.patch and patches/ds-bot/files/test/meetings-decide-paths.test.js carry the same two changes; ds-bot-patches.mjs check reports 10 patches applying cleanly and core/ equal to ds-bot/ + patches.

Evidence: both touched suites are green at the tip (33/33, no skips) and both go red when the behavior they pin is reverted — the store case fails with actual: '... could not be read (cannot back up the version-1 state (EISDIR)) ...' under the base wording, and disabling the meetings injection turns not ok 13/14/15 back on, so neither test is passing vacuously. Both edits were restored from the tip blobs (cmp clean). Scope, compare 5b72400c...d1dbd0d: 1 commit, 4 files, +72/−34, exactly store.js (+1/−1), the meetings test (+35/−16) and their two patches/ds-bot/ mirrors — nothing unrelated in core/ or patches/ds-bot/.

CI at this tip: ubuntu-latest · Node 24 · DSH 0.2.0-rc.2, tests-ok, plugins (social|tree|timeline|workflow), package, judge, agent-audience, merge-gate, risk tier, reviewed tip all green; enqueue-guard is the expected red for a tip with no PASS yet, which this comment clears. Risk tier T2 (store.js = T2), so this tip needs the PASS it now has.

The Windows leg itself cannot run on this box — the next compat · windows-latest job is the acceptance for the four nightlies, and this PR does not weaken that check.

Verdict: PASS. Ready for the merge queue.

@github-actions github-actions Bot added the pass label Nov 1, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 42b638c Nov 1, 2026
13 of 14 checks passed
@Birfy
Birfy deleted the dev/1982-windows-compat-tests branch November 1, 2026 06:09
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nightly compat: four core tests are red on windows-latest (the EACCES and state.json.v1-directory cases assume POSIX)

1 participant