Files
msd-core/tests/agent-tracked-source-rule.test.cjs
Tom Boucher a84f756303 fix(#3078): sweep all-spent ack fragments on next, name the collision remedy (#3823)
* fix(#3078): sweep all-spent ack fragments on next, name the collision remedy

`guard-no-ack-on-next` only ever watched the legacy tests/emitted-drift-ack.json.
#2914 exempted the fragment directory on the premise that a persisting fragment
"cannot conflict with any other PR". Fragments do not share a FILE, but they do
share a PATH KEY SPACE, and a path claimed by two sources is a hard failure in
the same script -- so a fully-spent fragment on next owns keys it can no longer
gate, and the next PR to grow one of those paths can declare it neither there
(spent) nor in its own fragment (duplicate). Measured at the sweep: 45 fragments
owning 403 paths, up from 13/272 at triage 19 days earlier.

- `assertNoAllSpentFragments` fails a fragment only when EVERY surviving entry is
  spent against the copy at HEAD^, so a partially spent fragment -- and the
  re-arm-by-appending route #2639/#2993 ship on -- keeps working.
- `ackProse` duplicates the gate's zero-width/whitespace stripping across the
  scripts-ship/tests-do-not line, bounded by a prose-parity test.
- The guard job's checkout takes fetch-depth: 2; at depth 1 HEAD^ is absent and
  every fragment reads as brand-new, i.e. the guard passes vacuously.
- The duplicate-ack error now names both resolutions, since the guard is
  post-merge by design and cannot stop the colliding PR.
- All 45 spent fragments deleted, 0000-legacy-migration.json included, and the
  three tests that pinned its permanence corrected.

Verification is the remote runner (gsd-test), not a local suite.

Closes #3078

* fix(#3078): make the prose-parity test two-sided, cover the git seam, base on the pre-push tip

Three review findings, all fixed:

- The parity test was a tautology: it checked ACK_INVISIBLE against a
  hardcoded list matching its own definition, never against the gate. The
  gate's INVISIBLE and its reason normalizer (hoisted out of diffEmitted as
  normalizeAckReason) are now exported for that sole purpose, and the test
  sweeps 0x00-0xFFFF against both surfaces. Mutation-checked: adding a
  codepoint to one side and not the other now fails.
- resolveBaseRef, readFragmentAtRef and assertUsableBaseRef had zero direct
  coverage -- the tests reimplemented the git reads in a local helper, so the
  ls-tree-vs-show discrimination, the root-commit fallback and the
  option-injection guard were never executed. All are exported and tested
  against real temp repositories now, plus an end-to-end --base-ref subprocess.
- HEAD^ is not 'the state of next before this push'. The default branch allows
  REBASE merges, so one push can carry N commits, and a 2-commit rebase-merge
  whose first commit adds a fragment would be told to git rm it on the very
  push that introduced it. CI now passes github.event.before via --base-ref and
  fetches it explicitly; HEAD^ remains only the local fallback.

Also adds the safe.directory guard every other git call in this repo carries
(#2767), and stops naming the deleted migration fragment by filename in
CONTEXT.md, which tripped lint-removed-but-needed.

Refs #3078

* fix(#3078): keep the fragment directory alive after the sweep empties it

Sweeping every fragment leaves the directory untracked, and check-glossary-refs
then fails: CONTEXT.md references tests/emitted-drift-acks, which no longer
exists. The empty directory IS the intended steady state, so it has to survive
its own remedy.

Adds tests/emitted-drift-acks/README.md documenting the create/use/delete
lifecycle where a contributor actually meets it, matching the existing
tests/qa/smell-acks/README.md precedent. Every reader filters on .json, so the
README is invisible to the gate.

Also sweeps #3809's ack fragment, which the rebase onto origin/next brought in
and the new guard immediately reported as all-spent -- its own remedy applied.

Refs #3078

* fix(#3078): guard the added tests' git calls, drop a second fragment-existence pin

Both defects surfaced by the remote runner (linux-node24, 4/37445 failed).

- The new --base-ref E2E test ran `git rev-parse HEAD` against the checkout
  without the #2767 safe.directory guard. The runner mounts the repo at a path
  owned by another uid, so git refused every operation there with 'detected
  dubious ownership'. Every git call the new tests make now names its own
  specific directory as safe, via one local helper, mirroring safeDirArgs in
  helpers/emitted-runtime.cjs.
- tests/agent-tracked-source-rule.test.cjs pinned the existence and contents of
  the 3645 and 3409 ack fragments. That is a merged PR's paperwork, not live
  behavior: once the growth is in next's baseline the acks are spent and this
  PR's guard sweeps them. The third assertion pinned the hand-appended
  workaround for the exact collision #3078 removes. Deleted; #3645's real
  protection is the two behavioral tests above it, untouched.

Also restores #3809's ack fragment, which merged one commit before this branch.
Deleting an ack in the same window as its introducing PR races any consumer
whose baseline predates it -- the runner's container proved it, resolving
origin/next to 8ed105c8a where the file is still 13847. The backlog sweep is
this PR's scope; that fragment is left for the guard's own first run.

Adds the rule to the fragment README so the class stops recurring.

Refs #3078

* test(#3078): derive the E2E guard expectation from the fragment inventory

The --base-ref E2E test asserted exit 0 while passing the checkout's own HEAD
as the base ref. HEAD-as-base makes every present fragment byte-identical to
itself, so all of them are trivially all-spent and the guard correctly exits 1.
The test only ever passed because the directory happened to be empty when it
was written; restoring #3809's fragment made it fail. The script was right and
the test was wrong.

The degenerate base ref is kept deliberately -- it is what makes 'spent'
trivially true and therefore deterministic -- but the expectation is now
derived from listFragmentFiles() at runtime: zero fragments means exit 0 and
the no-survivors line, N fragments means exit 1 with every name and its git rm.
Proven state-independent by running the suite with the fragment present, with
the directory emptied, and with it restored.

The option-shaped --base-ref rejection is split into its own test, unchanged.

Refs #3078

* chore(#3078): backfill PR number into the changeset fragment (pr:0 -> pr:3823)

---------

Co-authored-by: sim <sim@local>
2026-08-24 15:24:30 -04:00

94 lines
5.3 KiB
JavaScript

/**
* #3645: gsd-planner and gsd-pattern-mapper must write only git-TRACKED
* source paths into PLAN.md / PATTERNS.md — never a gitignored install/
* runtime mirror (e.g. <root>/.gsd/capabilities/<id>/... synced from a
* plugin's tracked tree). Executors that trust a mirror path edit a copy
* whose changes die on the next sync; the wrong path also self-propagates
* across phases because pattern-mapper builds on prior phases' docs.
*
* Enforcement points: gsd-pattern-mapper.md carries the gate inline (its
* size tier has headroom); the PLANNER agent file is frozen under a
* 49152-LF-char cap asserted by four other suites, so the planner-side rule
* is projected onto its spawn contract in gsd-core/workflows/plan-phase.md
* (the #3297 precedent for requirements the spawned agent must honor).
*
* Shipped-content contract rows: the agent/workflow text IS the product the
* runtime loads, so asserting its contract lines tests the deployed behavior.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
const WORKFLOWS_DIR = path.join(__dirname, '..', 'gsd-core', 'workflows');
describe('#3645 — agents write only git-tracked source paths', () => {
// allow-test-rule: source-text-is-the-product (#3645)
// The workflow prompt block below IS the runtime instruction shipped to
// every plan-phase run; testing its content tests the deployed contract.
const planPhase = fs.readFileSync(path.join(WORKFLOWS_DIR, 'plan-phase.md'), 'utf8');
// allow-test-rule: source-text-is-the-product (#3645)
// The agent gate text IS the runtime instruction; testing it tests the
// deployed contract — if the tracked-source gate is absent, the agent
// does not enforce it.
const mapper = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-pattern-mapper.md'), 'utf8');
test('planner spawn contract carries the #3645 tracked-source rule', () => {
const block = planPhase.split('<tracked_source_paths>')[1]?.split('</tracked_source_paths>')[0];
assert.ok(block, 'plan-phase.md must carry the <tracked_source_paths> block in the planner spawn prompt (#3645)');
assert.ok(/files_modified/.test(block) && /must_haves/.test(block),
'the block must govern files_modified and must_haves paths');
assert.ok(block.includes('git ls-files'),
'the block must instruct git ls-files verification (#3645)');
assert.ok(/GSD_SOURCE_MIRROR_SENTINEL|\.gsd\/capabilities/.test(block),
'the block must name the gitignored install-mirror shape it rejects');
assert.ok(/plugins\//.test(block),
'the block must point at tracked plugin-source fallback locations');
});
test('planner spawn contract re-verifies inherited PATTERNS.md paths (#3645)', () => {
const block = planPhase.split('<tracked_source_paths>')[1]?.split('</tracked_source_paths>')[0];
assert.ok(/PATTERNS_PATH/.test(block) && /inherit/.test(block),
'the block must cover paths inherited from {PATTERNS_PATH} and prior phases');
});
test('gsd-pattern-mapper emits only tracked analog paths (#3645)', () => {
assert.ok(mapper.includes('git ls-files'),
'gsd-pattern-mapper.md must verify analog paths via git ls-files (#3645)');
assert.ok(mapper.includes('gitignored install/runtime mirror'),
'the mapper must name the gitignored-mirror rejection explicitly (#3645)');
assert.ok(mapper.includes('never emit mirror paths'),
'PATTERNS.md output must be required to never carry mirror paths (#3645)');
assert.ok(/plugins\//.test(mapper) && /capabilities\//.test(mapper),
'the mapper must name tracked-origin fallback locations (#3645)');
});
test('the frozen planner agent file is untouched by #3645', () => {
// The planner is pinned under a 49152-LF-char cap by four suites; the
// rule lives in its spawn contract instead. Guard the freeze: #3645
// must not have grown the agent file past its baseline.
const src = fs.readFileSync(path.join(AGENTS_DIR, 'gsd-planner.md'), 'utf8');
const lf = src.replace(/\r\n/g, '\n').length;
assert.ok(lf < 49152, `gsd-planner.md is ${lf} LF chars — must stay < 49152 (#3645 keeps the planner frozen; enforcement lives in plan-phase.md)`);
assert.ok(!src.includes('Tracked-source'),
'the rule belongs in the spawn contract, not the frozen agent file (#3645)');
});
// A prior version of this suite pinned the EXISTENCE and CONTENTS of the `3645` and
// `3409` emitted-drift-acks fragments. An ack is scoped to the diff that introduced
// it (#2789's ack-lifecycle law): once #3645 merged and its growth is in `next`'s
// baseline, neither fragment gates anything — both are spent, and #3078's
// `guard-no-ack-on-next` sweeps them. A test may therefore never pin a fragment's
// existence or its prose; a fragment that is correctly swept would fail the pinning
// test for a reason that has nothing to do with the behavior it was meant to protect.
// #3645's actual protection is the two behavioral tests above — the mapper emitting
// only tracked analog paths, and the frozen planner file staying untouched — which
// this change leaves exactly as they were. The growth itself is protected by `next`'s
// emitted baseline (the differential attribution check), not by the spent fragment.
});