Files
msd-core/tests/lint-compiled-artifact-sync.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

236 lines
11 KiB
JavaScript

'use strict';
/**
* Regression test for #2657.
*
* Nine compiled `.cjs` artifacts under msd-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 { runGit, runNode, OUTCOME } = require('./helpers/process-seam.cjs');
const { toLegacyResult } = require('./helpers/git-fixture.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const { trackedCompiledArtifacts } = require('../scripts/lint-compiled-artifact-sync.cjs');
const REPO_ROOT = path.join(__dirname, '..');
const LIB_DIR = 'msd-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 the process seam (never throws) and return a legacy
* `{status, stdout, stderr, signal}` shape. `cmd` is either `'git'` (routed
* through `runGit`) or `process.execPath` (routed through `runNode`) — the
* only two callers below. 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 options = { cwd: REPO_ROOT, timeoutMs: PROBE_TIMEOUT_MS, ...opts };
const result = cmd === 'git' ? runGit(args, options) : runNode(args, options);
if (result.outcome === OUTCOME.SPAWN_FAILED) {
throw new Error(
`${cmd} ${args.join(' ')} failed to spawn (cwd=${REPO_ROOT}): ${result.stderr || result.code}`,
);
}
return { ...toLegacyResult(result), signal: result.signal };
}
/** 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 msd-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 msd-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', () => {
// #4093 CI: this spawn is NOT the PROBE class the shared default below
// describes. The script's "nothing left to check" path still runs a FULL
// `tsc -p tsconfig.build.json` compile to a throwaway outDir whenever any
// compiled artifact remains tracked, and under CI shard load that compile
// can exceed even a generous budget, dying to a SIGTERM with empty piped
// stdout (observed on ubuntu shard 1/3 both pre- and post-#4488). Per
// helpers/timeouts.cjs's own rule, a call site that genuinely differs from
// its class — "a real `tsc` compile" — keeps its own local constant with
// its own justifying comment; see tests/ensure-runtime-build.test.cjs's
// BUILD_TIMEOUT_MS for the other instance. 60s is that same class, sized
// for the cold-cache CI case.
//
// #4488: as of this fix the tracked set is fully empty (see the
// "fully-empty ADR-457 end state" test above) — trackedCompiledArtifacts()
// returning [] means main() short-circuits at its own line ~98 BEFORE ever
// invoking `compileToTemp()`, so this spawn no longer runs a real tsc
// compile at all. The 60s budget above stays as a guard for whenever a
// future module reintroduces a tracked artifact (re-widening the migration
// gap this whole file exists to catch), not because this run needs it now.
const TSC_COMPILE_TIMEOUT_MS = 60000;
const args = [path.join(REPO_ROOT, 'scripts', 'lint-compiled-artifact-sync.cjs')];
const result = run(process.execPath, args, { timeoutMs: TSC_COMPILE_TIMEOUT_MS });
assert.equal(result.status, 0, describeFailure(process.execPath, args, result));
});
test('trackedCompiledArtifacts() reports the fully-empty ADR-457 end state (#4488)', () => {
// #4488 CI: tdd-red-evidence.cjs (introduced by #3770/PR #4279, AFTER the
// original "nine" this file's other tests pin) was never gitignored —
// a tenth, later instance of the exact #2657/#2653 migration-gap defect
// class, incidentally surfaced by a #4488 CI run timing out on the real
// tsc compile this test's sibling above must run while ANY artifact stays
// tracked. Generic (not tied to the closed NINE_ARTIFACTS list above) so
// it also catches any future module that lands compiled-but-tracked.
let pairs;
try {
pairs = trackedCompiledArtifacts();
} catch (err) {
assert.fail(
`trackedCompiledArtifacts() threw instead of returning a result:\n` +
`${err && err.stack ? err.stack : err}`,
);
}
assert.deepEqual(
pairs,
[],
`expected zero tracked compiled artifacts (ADR-457 end state); still tracked: ` +
`${pairs.map((p) => p.artifact).join(', ') || '(none)'}`,
);
});
});