Commit Graph

2 Commits

Author SHA1 Message Date
Tom Boucher
9ee6d54cc3 fix(#4306): extend fault-injection fd-swallow fix across the whole suite (#4308)
* fix(#4306): forward real bytes through io.test.cjs's fault-injection mocks

The bug #1008 fault-injection tests mock fs.writeSync scoped only by file
descriptor. On their "success" arms (the retry-after-EAGAIN/EINTR call, and
the short-write simulation) they fabricated a return byte count without ever
calling the real writeSync -- the bytes went into a local array and nowhere
else.

node:test's process-isolation runner (default on Node >= 22) reads each test
file's own stdout to parse its child-to-parent result protocol. If the
runner's own reporter write for an adjacent test lands on fd 1 while one of
these mocks is installed, that write was silently swallowed instead of
reaching the real pipe -- observed in CI as "Unable to deserialize cloned
data" (a corrupted/truncated byte stream on the parent's read side), not a
thrown exception.

Every "success" arm now forwards the real bytes to orig()/restore() instead
of fabricating a return value, so anything else sharing the fd during the
mocked window still gets its bytes delivered for real. writeAllSync (the
only production caller reaching this mock) always passes a Buffer, so the
forwarded calls use the buffer-form fs.writeSync overload unambiguously.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4306): extend fault-injection fd-swallow fix across the whole suite

The originally-fixed instance (tests/io.test.cjs) was one occurrence of a
copy-pasted defect: mocked fs.writeSync arms fabricated a return byte count
without ever forwarding the call to the real fs.writeSync, silently
discarding bytes. Under node:test's process-isolated runner, the parent
reads the child's real stdout to parse v8-serialized report frames
interleaved with plain output (confirmed against node's own
lib/internal/test_runner/runner.js and a matching upstream issue,
nodejs/node#64061) — a swallowed write on that fd corrupts the parent's
parse ("Unable to deserialize cloned data").

Adds a shared, safe capture helper to tests/helpers.cjs, captureFdSync(fd,
fn): it always forwards every write to the real fs.writeSync first, then
records only the observed fd's bytes, sliced by the real return count (not
the requested length), decoded once via Buffer.concat so a short write
can't split a multi-byte codepoint across two decodes.

17 test files migrate their local copy of the unsafe mock to this shared
helper. tests/worktree-base-ref.test.cjs keeps a narrower in-place fix
instead (it needs to record every fd a write touched, which the shared
helper doesn't expose).

tests/io.test.cjs gets two follow-up correctness fixes on top of the
already-committed forwarding fix: the EAGAIN/EINTR/short-write arms now
derive their recorded chunk from the real return count everywhere
(including the string-form overload), and the short-write test no longer
forces a Buffer-shaped truncation call onto a string-form write that could
land on the same fd.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 23:54:02 -04:00
Tom Boucher
57437071e7 test(#3244): codex smoke test across the emit, validate and repair seam (#3298)
* test(#3244): end-to-end codex smoke test across the emit and validate seam

Test-only. The only place in this epic where the emitter (Phase 1) and
the validator (Phase 2) meet the same bytes.

Every phase so far tested its own half against fixtures it authored, and
per #2371 a fixture written by the gate's own author can only confirm
what that author already believed. Rows 7 and 10 feed a REAL emitted
tree to Phase 2's checkCodexModelPosture: if the emitter and the
validator disagree about what "clean" means, nothing else in the suite
can see it.

Scope corrected from the epic, which scoped this to model_profile:
inherit. That profile is the one this epic does NOT change —
readGsdRuntimeProfileResolver returns null for it, so a Codex install
under inherit omitted the model before Phase 1 too, and a test scoped
only to it would pass identically before and after the change it exists
to prove. Row 1 uses `balanced`, the default and the path that actually
lost its pin; inherit is row 4, the unchanged control.

Honest classification, because this is a smoke test and claiming
otherwise would be false: NO row here is red-first. Phases 1 and 2 are
merged and working, so every row passes today. Rows 1-6 and 9 are
regression guards; rows 7 and 10 are cross-phase integration. Their
value is that nothing else can catch the drift they cover, not that they
are red now. The RED checkpoint is therefore skipped deliberately for
this phase rather than spent proving a tautology.

Rows 8 and 11 — the same cross-checks against Phase 3's repairer — are
deliberately ABSENT, not forgotten. Phase 3 (#3243) is still in CI and
its module does not exist on next yet. They land once it merges; writing
them against a surface that does not exist would have meant inventing
the assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#3244): close the emit-validate-repair loop with rows 8 and 11

These were deliberately absent from the previous commit because Phase 3
had not merged; writing them against a module that did not exist would
have meant inventing the assertion. It has merged, so they land now.

Row 8: a freshly-installed tree reports ZERO changes from the sync's dry
run. If a brand-new install needs repair, the emitter and the repairer
disagree about what a correct file looks like.

Row 11 is the sharpest row in the phase: after installing with an
explicit real-Codex model_overrides pin, the sync reports that agent
skipped rather than synced, and the file is byte-identical afterwards.
An over-eager stripper that removes any `model` line passes every Phase
3 unit test and fails only here.

Byte-identity is asserted by comparing file CONTENTS, not mtime — a
rewrite with identical bytes still moves mtime, so an mtime check would
pass exactly the implementation this row exists to catch.

With these, all four cross-phase rows are in place, and this suite is
the only place in the epic where the emitter, the validator and the
repairer meet the same bytes. Every phase before this tested its own
half against fixtures it authored, and per #2371 those can only confirm
what their author already believed.

Both rows are cross-phase integration and pass today, like the rest of
this suite. No row here is red-first and none is described as such.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(#3244): extract withAgentsDir instead of try/finally in test bodies

Review finding. Rows 7 and 10 put try/finally directly inside the test
callback to save and restore GSD_AGENTS_DIR. CONTRIBUTING prohibits that
in test bodies — it masks failures — and permits it only inside
standalone helpers. The phase's own test matrix restated the rule and it
was violated anyway.

lint:ci does not catch this; it is a prose standard, which is precisely
why it survived to review.

It was also inconsistent with this file's own conventions:
runCodexInstall, runEffortSyncDryRun and captureStderr already factor
save/restore into helpers. withAgentsDir now follows the same shape and
both sites use it.

No assertion changed — both rows still drive the real
checkCodexModelPosture against the real installed tree, which is the
whole point of them. Swept the rest of the file: the only other try
blocks are inside the two pre-existing helpers and were already
compliant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 02:11:57 -04:00