* test(#4544): failing-first rollback-coverage tests for codex install
* test(#4544): scope the rollback suite to its own requires
The appended describe used bare describe/test/os/cleanup and the folded
block's runCodexInstall — none visible at file scope, so the whole test
file failed to load. Wrap it in its own block with local requires and a
local harness copy, matching the folded-block idiom.
* test(#4544): import beforeEach/afterTest hooks into the suite scope
* fix(#4544): restore manifest-tracked files and hooks/ on codex rollback
restoreCodexSnapshot and _codexPreConfigRollback knew only the five
#3245 targets (config.toml, hooks.json, skills/gsd-*, agents/gsd-*,
VERSION). A Codex install also writes hooks/, gsd-core/CHANGELOG.md,
gsd-core/.gsd-runtime, scripts/changeset|lib + the standalone scripts,
and rewrites gsd-file-manifest.json -- none were captured, so any
rollback left the new payload in place for all of them.
The pre-install capture now records every path the PRIOR install's own
gsd-file-manifest.json lists (bytes when present, absence-marker when
not, so a path deleted between installs is re-deleted rather than
resurrected), the manifest file itself, and the whole hooks/ tree --
wholesale, because the Codex manifest deliberately omits hooks/ and
hooks/ is shared space, so restore returns user files that predated the
install and drops everything the failed install staged. Both rollback
paths share one restore closure; malformed or missing prior state
degrades to today's behavior.
Known residual, documented: files the FAILED install adds under
manifest-tracked dirs survive a rollback that fires before the new
manifest is written (they are named by no prior state). The five
original targets and hooks/ have no residual.
* test(#4544): align malformed-manifest fixtures with pre-install-state semantics
Row 7 seeded VERSION and then asserted its absence -- but a seeded
VERSION is pre-install state the fix must restore, not remove. Row 8
asserted a pre-existing array-shaped manifest must not survive, when
restoring those exact bytes IS the contract. Both were fixture bugs;
the probe-verified installer behavior was correct.
* docs(#4544): add Fixed changeset for codex rollback coverage
* fix(#4544): harden snapshot per adversarial review — minimal mode, symlinks, clean installs
Review (three independent passes) found five defects and one coverage
gap in the first cut; all fixed:
- BLOCKER: the capture gate is off in minimal mode but the restore call
was not, so a minimal-mode rollback wholesale-deleted the user's
entire hooks/ directory (empirically confirmed by the reviewer). The
restore now consults a captured flag: no snapshot means do nothing.
- MAJOR: the hooks/ walk followed file symlinks — a repo-shipped
.codex/hooks symlink to a FIFO would hang the installer, to
/dev/zero exhaust memory, or to private data copy that data into the
snapshot. The walk lstats every entry and captures only true regular
files; anything else marks the capture incomplete.
- Incomplete captures now downgrade the restore to per-file: put back
what was captured, remove only the names GSD itself stages (the
hoisted CODEX_HOOKS_TO_COPY set + CommonJS marker), never wholesale-
delete a tree the snapshot did not fully see. GSD-owned removal runs
before the restore so a name in both sets keeps its pre-install
bytes.
- A pre-existing hooks FILE (not directory) is left alone instead of
deleted.
- readInstallManifest now rejects a manifest whose files field is a
JSON array (typeof [] === 'object'), which previously produced
numeric-key paths.
- Clean FIRST installs: with no prior manifest nothing recorded the
payload, so a failed clean install rolled back to a half-written
tree. Capture now enumerates the same source directories the
installer copies plus the two standalone files (CHANGELOG.md from
the repo root, generated .gsd-runtime) and records absence — a
failed clean install now rolls back to actually nothing.
Tests: minimal-mode preservation regression, symlink never-followed
regression, helpers.cjs temp dirs, the injected-failure message is
asserted, residue assertions made unconditional.
* test(#4544): pin symlink-downgrade semantics the final run exposed
The symlink itself marks the capture incomplete, so the restore takes
the per-file downgrade — which preserves uncaptured pre-install state
(the link) rather than wholesale-dropping it. The probe run verified
exactly this; the assertion guessed the wholesale branch. Pin the
verified behavior: referent untouched, link preserved and resolving,
no leak.
* docs(#4544): backfill changeset PR number
---------
Co-authored-by: sim <sim@local>