Files
msd-core/tests
Tom Boucher 7efd9032ee fix(#4544): cover hooks/, scripts/, CHANGELOG and the manifest in codex rollback (#4760)
* 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>
2026-09-15 02:37:18 -04:00
..