Files
msd-core/tests/fix-2657-untrack-compiled-artifacts.test.cjs
Tom Boucher 07de60523c fix(#2657): untrack the nine ADR-457 migration-gap compiled artifacts (#3011)
* test(#2657): add failing-first regression for tracked bin/lib compiled artifacts

Nine gsd-core/bin/lib/*.cjs artifacts are tracked in git despite having
src/*.cts sources, violating ADR-457's build-at-publish contract. This
regression test asserts the ADR-457 end state (untracked, gitignored,
empty-set reported by the #2656 sync guard) and fails until the tracking
is fixed.

* fix(#2657): untrack the nine ADR-457 migration-gap compiled artifacts

Nine gsd-core/bin/lib/*.cjs artifacts (api-coverage, assumption-delta,
claude-orchestration, claude-orchestration-command-router, external-job,
markdown-table, runtime-artifact-install-plan, state-transition,
write-set) were tracked in git despite each having a matching src/*.cts
source, letting the committed bytes drift silently from source (#2653
demonstrated this for api-coverage.cjs).

Seven had no .gitignore entry at all; two (markdown-table.cjs,
write-set.cjs) had a pattern added by #2248 but were never
git rm --cached. Both gaps produce the same tracked-file symptom.

Untracks all nine and adds the seven missing .gitignore entries next to
their two siblings, reaching ADR-457's end state: bin/lib/*.cjs is a
gitignored build artifact built via prepare/pretest/prepublishOnly, never
checked-in source of truth. The #2656 artifact-sync guard is
regime-agnostic by design and needed no code change; it now reports the
empty-set end state.

* test(#2657): consolidate repeated still-tracked/unmatched assertion shape

Code-review finding (Standards axis, Duplicated Code): the three
'none of the nine should still be in bad state X' checks shared an
identical filter-then-assert-empty shape. Extracted assertNoneStillBad()
as a shared helper; behavior is unchanged.

* fix(#2657): make the .gitignore-match assertion existence-independent

The regression test's check-ignore assertion used --no-index, which
locally exercises the pattern correctly but was reported failing on
gsd-test's fresh shallow clone. Switched to plain 'git check-ignore -q'
(no --no-index): verified via a real git worktree checkout at both
origin/next (fails: all nine report not-ignored, since check-ignore
correctly special-cases the still-tracked pre-fix state) and this
branch's tip (passes: all nine report ignored). Plain check-ignore is
also semantically stronger than --no-index here, since it honors the
'a tracked path is never reported ignored' rule that --no-index
bypasses -- exactly the property under test for the two paths whose
.gitignore pattern predates this fix (#2248) but were never untracked.

Also reconciled the .gitignore comment: it previously read 'these
seven' beside seven new lines with no indication of the other two (of
nine total) that already had a pattern from #2248. Annotated both
groups so the count is unambiguous at the point of the diff.

* test(#2657): make every git invocation self-diagnosing

b6f915bc0 failed in the runner with a shape that turned out not to be
about .gitignore content or the merge: two of the five failures in this
file were 'Command failed' / 'Got unwanted exception' -- git itself
erroring, not answering. The old code used execFileSync + try/catch,
which conflates 'git said no' with 'git could not run' -- both looked
like the same negative result to the test, exactly the failure mode
that produced 'unmatched: <all nine>' twice on two different
assertions for two different reasons.

Switched every git invocation to spawnSync (never throws) and made
every assertion check the exit status explicitly before interpreting
output:
  - git ls-files: must exit 0, or the assertion fails loud with cwd,
    exit status, and stderr instead of silently reading an error as
    'nothing tracked'.
  - git check-ignore -q: only exit 0 (ignored) and exit 1 (not
    ignored) are legitimate answers per check-ignore(1); any other
    status is now a thrown infrastructure failure, never read as
    'not ignored'.
  - trackedCompiledArtifacts(): a thrown error is now reported as
    what it is (its internal git call failed), not swallowed into a
    'still tracked' verdict.
  - the sync-guard subprocess check now reports cwd/stderr/stdout on
    a non-zero exit instead of a bare doesNotThrow.

This is a genuine, independent test defect (a test that reads a
failed command's empty output as a meaningful answer can pass or fail
for the wrong reason) as well as the mechanism for finally surfacing
why b6f915bc0 failed in the runner: the next run's assertion messages
will show the resolved cwd and git's actual stderr instead of an
opaque 'unmatched: <all nine>'.

* fix(#2657): trust the repo root for git calls under dubious-ownership

Root cause of the failing runner verdict, harvested from the
diagnostics commit: every git invocation in the container exits 128
with 'fatal: detected dubious ownership in repository at /work' --
the checkout there is owned by a different uid than the process
running the tests, and git refuses to operate at all. The old
assertions read that hard failure as 'not ignored' / 'still tracked',
producing the all-nine symptom seen on both b6f915bc0 (plain
check-ignore) and 34052f836 (--no-index). Nothing was ever wrong with
the untracking, the .gitignore content, or the merge -- confirmed by
exhaustive local reproduction (git worktree, real shallow clone, the
runner's exact clone+checkout+merge sequence from its own Go source)
that could never surface the bug because this machine owns its own
checkouts.

Fixed at both git() call sites in this exact seam by passing
'-c safe.directory=<repo root>' per-invocation (never written to any
config file, so trust is scoped to the single call):
  - tests/fix-2657-untrack-compiled-artifacts.test.cjs
  - scripts/lint-compiled-artifact-sync.cjs -- a SHIPPED script with
    the identical defect (its own git ls-files failed the same way in
    the same run), which would fail identically for any containerized
    CI lane whose checkout uid differs from the running user, not just
    this branch. Folded in under the no-defer rule rather than filed
    separately, since it sits in the exact tracked-compiled-artifact
    guard this issue is about.

The status-code guards added in 31858818e stay in place -- they are
what turned an unexplainable 'unmatched: <all nine>' into a one-line
diagnosis, and they must keep any future infrastructure fault from
silently reading as a substantive result.

* chore(#2657): backfill changeset PR number to 3011

* chore(#2657): backfill changeset PR number to 3011

---------

Co-authored-by: sim <sim@local>
2026-08-02 21:22:43 -04:00

186 lines
8.3 KiB
JavaScript

'use strict';
/**
* Regression test for #2657.
*
* Nine compiled `.cjs` artifacts under gsd-core/bin/lib/ were tracked in git
* despite each having a matching src/*.cts source, violating ADR-457's
* build-at-publish contract ("bin/lib/*.cjs" must be a gitignored build
* artifact, never checked-in source of truth). A tracked compiled artifact
* can silently drift from its source without anyone noticing — #2653
* demonstrated exactly this for api-coverage.cjs, which shipped four days
* behind its .cts with CI green throughout.
*
* This asserts the ADR-457 end state for all nine: none tracked, all
* gitignored, and the regime-agnostic sync guard (added in #2656,
* scripts/lint-compiled-artifact-sync.cjs) reports the empty tracked set.
*
* Two of the nine (markdown-table.cjs, write-set.cjs) already had a
* .gitignore pattern before this fix (added by #2248) but were never
* `git rm --cached`; the other seven had no .gitignore pattern at all. Both
* gaps produce the same `git ls-files` symptom, so both are covered by the
* same assertions here.
*
* ── Diagnostics discipline ────────────────────────────────────────────────
* Every git invocation below uses `spawnSync` (never throws) and every
* assertion explicitly checks the exit status BEFORE interpreting output.
* A git command that errors (bad cwd, dubious-ownership refusal, missing
* binary, anything) must never be silently read as a legitimate "not
* ignored" / "still tracked" answer — that conflates "the property does not
* hold" with "I could not determine whether the property holds", which is a
* distinct defect from the bug this file guards against. On any failure,
* the assertion message includes the resolved cwd, exit status, and stderr,
* so a red run is self-diagnosing without a second round-trip.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { trackedCompiledArtifacts } = require('../scripts/lint-compiled-artifact-sync.cjs');
const REPO_ROOT = path.join(__dirname, '..');
const LIB_DIR = 'gsd-core/bin/lib';
const NINE_ARTIFACTS = [
'api-coverage.cjs',
'assumption-delta.cjs',
'claude-orchestration-command-router.cjs',
'claude-orchestration.cjs',
'external-job.cjs',
'markdown-table.cjs',
'runtime-artifact-install-plan.cjs',
'state-transition.cjs',
'write-set.cjs',
].map((name) => `${LIB_DIR}/${name}`);
/**
* Run a command via spawnSync (never throws) and return the raw result.
* Throws immediately, with full context, only on a genuine spawn failure
* (binary not found, etc.) — a condition no caller here can meaningfully
* interpret as a match/no-match answer.
*/
function run(cmd, args, opts) {
const result = spawnSync(cmd, args, { cwd: REPO_ROOT, encoding: 'utf8', ...opts });
if (result.error) {
throw new Error(
`${cmd} ${args.join(' ')} failed to spawn (cwd=${REPO_ROOT}): ${result.error.message}`,
);
}
return result;
}
/** Render a failed command's full context for an assertion message. */
function describeFailure(cmd, args, result) {
return (
`${cmd} ${args.join(' ')} (cwd=${REPO_ROOT}) exited ${result.status}` +
(result.signal ? ` (signal ${result.signal})` : '') +
`\n stderr: ${(result.stderr || '(empty)').trim()}` +
`\n stdout: ${(result.stdout || '(empty)').trim()}`
);
}
function git(args) {
// -c safe.directory=REPO_ROOT: containerized CI checkouts are frequently
// owned by a different uid than the one running node --test, and git
// refuses to operate at all on such a repo ("detected dubious ownership")
// unless explicitly trusted. Scoped per-invocation (not written to any
// config file), matching the same fix applied to
// scripts/lint-compiled-artifact-sync.cjs's own git() helper, which has
// the identical defect (#2657 diagnostic run: `git ls-files` there failed
// with the same "dubious ownership" fatal in the runner).
return run('git', ['-c', `safe.directory=${REPO_ROOT}`, ...args]);
}
/** `git ls-files <LIB_DIR>`, asserting success before trusting the output. */
function trackedLibFiles() {
const args = ['ls-files', LIB_DIR];
const result = git(args);
assert.equal(
result.status,
0,
`git ls-files must exit 0 before its output can be trusted as "nothing tracked":\n${describeFailure('git', args, result)}`,
);
return new Set(result.stdout.split('\n').filter(Boolean));
}
/**
* `git check-ignore -q <path>` has exactly two legitimate outcomes: exit 0
* (ignored) and exit 1 (not ignored) — check-ignore(1). Any other exit code
* or a signal is an infrastructure failure, not a "not ignored" answer, and
* must not be conflated with one.
*/
function isIgnored(artifactPath) {
const args = ['check-ignore', '-q', artifactPath];
const result = git(args);
if (result.status === 0) return true;
if (result.status === 1) return false;
throw new Error(
`git check-ignore for ${artifactPath} returned neither a match (0) nor a legitimate ` +
`no-match (1) exit code — this is an infrastructure failure, not evidence the path ` +
`is unignored:\n${describeFailure('git', args, result)}`,
);
}
// Shared shape for both "none of the nine should still be in state X" checks
// below: derive the still-bad subset via `isBad`, then assert it's empty.
function assertNoneStillBad(isBad, failureLabel) {
const stillBad = NINE_ARTIFACTS.filter(isBad);
assert.deepEqual(
stillBad,
[],
`expected none of the nine ${failureLabel}; still: ${stillBad.join(', ') || '(none)'}`,
);
}
describe('fix-2657: compiled .cjs artifacts are gitignored, not tracked (ADR-457)', () => {
test('none of the nine ADR-457 migration-gap artifacts are tracked by git', () => {
const tracked = trackedLibFiles();
assertNoneStillBad((p) => tracked.has(p), 'to be tracked');
});
test('every one of the nine paths is ignored per git', () => {
// Deliberately WITHOUT --no-index: git-check-ignore(1) operates on the
// pathname alone and does not require the file to exist on disk (true
// both with and without --no-index — this repo's gsd-test runner checks
// out a fresh shallow clone per sha, where an untracked, gitignored path
// exists as a pattern match only, never as a file on disk). Plain
// check-ignore is preferred here over --no-index specifically because it
// also honors git's "a still-TRACKED path is never reported ignored"
// rule (check-ignore(1)) — which is exactly the property under test: a
// path that still matches a .gitignore pattern while ALSO remaining
// tracked (the pre-fix state for two of the nine, whose pattern
// predates this fix per #2248) must still read as "not ignored," the
// same as the seven with no pattern at all. --no-index would blur that
// distinction by reporting the two as ignored regardless of tracking.
assertNoneStillBad((p) => !isIgnored(p), 'to be reported not-ignored by git');
});
test('trackedCompiledArtifacts() reports the ADR-457 empty-set end state for the nine', () => {
let pairs;
try {
pairs = trackedCompiledArtifacts();
} catch (err) {
// trackedCompiledArtifacts() (scripts/lint-compiled-artifact-sync.cjs)
// wraps its OWN internal `git ls-files gsd-core/bin/lib` call, with cwd
// resolved from that script's own __dirname (should equal REPO_ROOT
// here regardless of caller). A throw means THAT invocation failed —
// not that any artifact is still tracked. Surface it, don't mask it.
assert.fail(
`trackedCompiledArtifacts() threw instead of returning a result — this indicates its ` +
`internal git invocation failed, not that any of the nine is still tracked:\n` +
`${err && err.stack ? err.stack : err}`,
);
}
const stillPresentArtifacts = new Set(pairs.map((p) => p.artifact));
assertNoneStillBad((p) => stillPresentArtifacts.has(p), 'to appear in trackedCompiledArtifacts()');
});
test('lint-compiled-artifact-sync exits 0 with nothing left to check', () => {
const args = [path.join(REPO_ROOT, 'scripts', 'lint-compiled-artifact-sync.cjs')];
const result = run(process.execPath, args);
assert.equal(result.status, 0, describeFailure(process.execPath, args, result));
});
});