Files
msd-core/tests/lint-vendored-deps-manifest.test.cjs
Tom Boucher 523be34133 fix(#4282): register PATTERNS.md as a canonical .planning/ artifact (#4618)
* test(#4282): prove PATTERNS.md is unrecognized by the artifact registry

Regression test only, no fix yet: CANONICAL_EXACT in src/artifacts.cts was
never updated when workflows/graduation.md started writing .planning/
PATTERNS.md, same omission class as the already-fixed #3224 (WINDOWS.md).

Expected RED on this commit (src/artifacts.cts is unchanged).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4282): register PATTERNS.md as a canonical .planning/ artifact

CANONICAL_EXACT in src/artifacts.cts was never updated when
workflows/graduation.md started writing .planning/PATTERNS.md for the
`patterns` graduation-target category -- same omission class as the
already-fixed #3224 (WINDOWS.md). validate.health's W019 falsely flagged it
as unrecognized on every repo that has run the graduation scan.

Also backfilled 5 other pre-existing stale rows in
gsd-core/templates/README.md's artifact table (WINDOWS.md, STATE-ARCHIVE.md,
milestone.lock, state.json, skill-manifest.json) that were already in the
source registry but missing from the docs table -- found while fixing this
exact drift class, cheap to close alongside it.

RED proven on f3dd791fb8cda18196803e7144ce20e506d6490b (test-only commit,
gsd-test outcome:failed, exactly the new PATTERNS.md test failing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4282): fix stale function name in skill-manifest.json comment

Review finding: both the source comment and the new docs row said
"routeSkillManifest" -- no such symbol exists (verified via Memtrace); the
actual function is cmdSkillManifest (src/init.cts). Copied verbatim from a
pre-existing comment, not introduced by this PR, but cheap to fix alongside.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4282): add changeset fragment

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4282): backfill changeset PR number

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: isolate lint-vendored-deps-manifest.test.cjs's fixRow tests from the real vendor file

Genuine, pre-existing defect found and fixed per this repo's no-defer policy
(discovered while investigating a real CI failure during this PR's own
merge attempt, user-directed investigation -- not deferred to a separate
issue since it was actively blocking work and root-caused with concrete
evidence, not speculation).

Root cause: fixRow(row) (scripts/lint-vendored-deps.cjs) unconditionally
does fs.copyFileSync(upstreamCjs, vendoredCjs) as its first line. All three
tests in the #4573 describe block called fixRow(row) with the REAL js-yaml
row, so all three wrote to the real, shared gsd-core/bin/lib/vendor/
js-yaml.cjs -- a file other test files' require() calls can read at any
moment, since node --test runs files concurrently in this repo.
fs.copyFileSync's write is not atomic against a concurrent reader on every
filesystem; a concurrent require() elsewhere caught the file mid-overwrite
and read a truncated file, crashing an entirely unrelated test
(m9-statelock-write-error-orphan.test.cjs) with a SyntaxError.

Confirmed via two real CI log fetches, not assumed: the exact same shard
grouping (same 308 files) ran clean ~90 minutes earlier during PR #4615's
own final merge CI, with the identical #3660 reap-fix code already present
-- ruling out a deterministic connection to that change and confirming a
genuine, non-deterministic timing race in this pre-existing test design.

Fix: all three tests now redirect row.vendoredCjs to a private os.tmpdir()
path via a cloned row object before calling fixRow, so the real vendored
file is never touched. upstreamCjs stays pointed at the real node_modules
copy (read-only, safe to share).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: also isolate fixRow's package.json pin-rewrite from the real file

Review finding (major) on the previous race-condition fix: fixRow's pin
-rewrite path still hardcoded path.join(ROOT, 'package.json'), so the third
#4573 test still wrote the real, shared package.json -- read at module
top-level by dozens of other test files, the same concurrent-file race
class already fixed for the vendored .cjs copy.

Adds an optional pkgRoot parameter (defaults to the real ROOT) threaded
through readPinState/checkRow/fixRow -- fully backward-compatible, every
existing call site (the CLI --fix path, any other caller) is unaffected
since the default is unchanged. The pin-rewrite test now builds an isolated
temp root (its own package.json + node_modules/js-yaml/package.json) and
passes it explicitly, so the real package.json is never touched either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: use helpers.cleanup instead of raw fs.rmSync in test cleanup

CI caught it: local/no-raw-rmsync-in-tests flagged the three t.after temp-dir
cleanup calls added for the fixRow isolation fix. helpers.cleanup() carries
the Windows-EBUSY retry budget (maxRetries/retryDelay) that raw fs.rmSync
lacks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 22:46:18 -04:00

389 lines
19 KiB
JavaScript

'use strict';
/**
* ADR-3473 §8.1 (#3881, phase test-matrix §G — packaging).
*
* scripts/lint-vendored-deps.cjs used to be a single hand-rolled check hardcoded to
* `re2js`; #3881 generalized it to a table-driven VENDORED manifest so adding js-yaml did
* not need a second hardcoded block. This suite pins:
* G1 the js-yaml row's byte-compare actually matches node_modules today.
* G2 all four checks the original hand-rolled re2js guard ran (see
* scripts/lint-vendored-deps.cjs's `checkRow`: .cjs drift, .d.cts drift, src/vendor/
* twin drift, version-pin drift) still fire, asserted against re2js's CURRENT
* behavior — not re-derived from the new manifest, which would validate the
* refactor against its own output and prove nothing about drift.
* G3 the hand-authored js-yaml type twin (no upstream to compare against) is
* deliberately excluded from the byte-compare rather than silently skipped by
* accident, and is pinned by a direct assertion on its declared surface instead.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const {
VENDORED,
compareFiles,
checkRow,
stripRangeOperator,
pinOperatorPrefix,
fixRow,
declaredValueExports,
checkHandAuthoredTwin,
resolvePath,
} = require('../scripts/lint-vendored-deps.cjs');
const REPO_ROOT = path.join(__dirname, '..');
function jsYamlRow() {
const row = VENDORED.find((r) => r.name === 'js-yaml');
assert.ok(row, 'expected a js-yaml row in VENDORED');
return row;
}
function re2jsRow() {
const row = VENDORED.find((r) => r.name === 're2js');
assert.ok(row, 'expected a re2js row in VENDORED');
return row;
}
describe('resolvePath: absolute-input safety does not change repo-relative resolution', () => {
test('a repo-relative input still resolves under REPO_ROOT (unchanged behavior)', () => {
const row = jsYamlRow();
assert.equal(resolvePath(row.vendoredCjs), path.join(REPO_ROOT, row.vendoredCjs));
});
test('an absolute input is returned as-is, not re-joined onto REPO_ROOT', () => {
const absPath = path.join(os.tmpdir(), 'resolve-path-absolute-sensor.cjs');
assert.equal(resolvePath(absPath), absPath);
// Sensor: confirm this is not vacuous — path.join(ROOT, absPath) (the
// pre-fix behavior) would NOT equal absPath, since joining an absolute
// second segment onto ROOT is not the identity operation on either
// POSIX or Windows.
assert.notEqual(path.join(REPO_ROOT, absPath), absPath);
});
});
describe('G1: vendored js-yaml matches node_modules via the generalized manifest', () => {
test('the js-yaml row byte-compares clean against node_modules today', () => {
const findings = checkRow(jsYamlRow());
assert.deepEqual(findings, [], `unexpected drift findings for js-yaml: ${JSON.stringify(findings)}`);
});
test('sensor: compareFiles is not vacuous — it reports drift against a deliberately mutated copy', () => {
const row = jsYamlRow();
const upstreamAbs = path.join(REPO_ROOT, row.upstreamCjs);
const tmpFile = path.join(os.tmpdir(), `js-yaml-mutated-${process.pid}-${Date.now()}.cjs`);
const original = fs.readFileSync(upstreamAbs, 'utf8');
fs.writeFileSync(tmpFile, `${original}\n// mutated for test\n`);
try {
// tmpFile is passed ABSOLUTE, not relativized against REPO_ROOT — a
// temp dir can live on a different drive than the repo checkout
// (observed on windows-latest CI), where path.relative() cannot
// express a relative traversal and silently returns the absolute
// path unchanged, defeating compareFiles's `path.join(ROOT, rel)`
// resolution. compareFiles/resolvePath must accept an absolute path
// as-is regardless of platform or drive.
const drift = compareFiles(row.vendoredCjs, tmpFile);
assert.ok(drift, 'expected compareFiles to report drift against a mutated copy, got null');
assert.ok(drift.includes('!='), `expected a byte-length mismatch description, got: ${drift}`);
} finally {
fs.unlinkSync(tmpFile);
}
});
});
describe('G2: all four original re2js checks still fire after the manifest refactor', () => {
test('fresh state: re2js has zero findings today (sanity baseline before mutating)', () => {
const findings = checkRow(re2jsRow());
assert.deepEqual(findings, [], `expected re2js to be fresh; findings: ${JSON.stringify(findings)}`);
});
test('check 1 (cjs drift) fires against a mutated vendoredCjs copy', () => {
const row = re2jsRow();
const vendoredAbs = path.join(REPO_ROOT, row.vendoredCjs);
const tmpFile = path.join(os.tmpdir(), `re2js-vendored-mutated-${process.pid}-${Date.now()}.cjs`);
fs.writeFileSync(tmpFile, `${fs.readFileSync(vendoredAbs, 'utf8')}\n// mutated`);
try {
// Absolute tmpFile, not relativized — see the js-yaml sensor test above
// for why: cross-drive path.relative() on Windows returns the absolute
// path unchanged, which is exactly the shape that must still resolve.
const mutatedRow = { ...row, vendoredCjs: tmpFile };
const findings = checkRow(mutatedRow);
assert.ok(
findings.some((f) => f.includes('!=')),
`expected a cjs-drift finding, got: ${JSON.stringify(findings)}`,
);
} finally {
fs.unlinkSync(tmpFile);
}
});
test('check 2 (d.cts drift) fires against a mutated vendoredDts copy', () => {
const row = re2jsRow();
assert.ok(row.vendoredDts, 're2js is expected to carry a vendoredDts for this check to apply');
const vendoredDtsAbs = path.join(REPO_ROOT, row.vendoredDts);
const tmpFile = path.join(os.tmpdir(), `re2js-dts-mutated-${process.pid}-${Date.now()}.d.cts`);
fs.writeFileSync(tmpFile, `${fs.readFileSync(vendoredDtsAbs, 'utf8')}\n// mutated`);
try {
// Absolute tmpFile — same cross-drive rationale as above.
const mutatedRow = { ...row, vendoredDts: tmpFile };
const findings = checkRow(mutatedRow);
assert.ok(
findings.some((f) => f.includes('!=')),
`expected a d.cts-drift finding, got: ${JSON.stringify(findings)}`,
);
} finally {
fs.unlinkSync(tmpFile);
}
});
test('check 3 (src/vendor twin drift) fires against a mutated srcTwin copy', () => {
const row = re2jsRow();
assert.ok(row.srcTwin, 're2js is expected to carry a srcTwin for this check to apply');
const srcTwinAbs = path.join(REPO_ROOT, row.srcTwin);
const tmpFile = path.join(os.tmpdir(), `re2js-srctwin-mutated-${process.pid}-${Date.now()}.d.cts`);
fs.writeFileSync(tmpFile, `${fs.readFileSync(srcTwinAbs, 'utf8')}\n// mutated`);
try {
// Absolute tmpFile — same cross-drive rationale as above.
const mutatedRow = { ...row, srcTwin: tmpFile };
const findings = checkRow(mutatedRow);
assert.ok(
findings.some((f) => f.includes('!=')),
`expected a src/vendor-twin-drift finding, got: ${JSON.stringify(findings)}`,
);
} finally {
fs.unlinkSync(tmpFile);
}
});
test('check 4 (version-pin drift) fires when the row name has no package.json pin', () => {
const row = re2jsRow();
const mutatedRow = { ...row, name: 'a-package-that-is-not-pinned-anywhere' };
const findings = checkRow(mutatedRow);
assert.ok(
findings.some((f) => f.includes('devDependencies') && f.includes('is missing')),
`expected a missing-pin finding, got: ${JSON.stringify(findings)}`,
);
});
test('check 4 (version-pin drift): stripRangeOperator mismatch is what the real check compares', () => {
// checkRow reads package.json/node_modules directly and cannot be redirected, so this
// exercises the exact comparison predicate checkRow applies
// (stripRangeOperator(pinned) !== installed.version) against a synthetic mismatch,
// proving the predicate itself can fail rather than only ever reading true.
assert.equal(stripRangeOperator('^5.9.9') === '5.9.0', false, 'a genuine version mismatch must not compare equal');
assert.equal(stripRangeOperator('^5.9.0') === '5.9.0', true, 'a matching version must compare equal');
});
});
describe('G3: the hand-authored js-yaml type twin is excluded from byte-compare, and pinned by test', () => {
test('the js-yaml row is declared hand-authored with no upstream twin to compare', () => {
const row = jsYamlRow();
assert.equal(row.twinKind, 'hand-authored');
assert.equal(row.upstreamDts, null, 'js-yaml ships no upstream .d.ts to compare against');
assert.equal(row.vendoredDts, null, 'there is no bin-side .d.cts twin for js-yaml');
assert.equal(row.srcTwin, 'src/vendor/js-yaml.d.cts');
});
test('sensor: a byte-compare-neutral mutation (e.g. a trailing comment) produces NO drift finding — the BYTE-COMPARE exclusion is real, not accidental', () => {
const row = jsYamlRow();
const srcTwinAbs = path.join(REPO_ROOT, row.srcTwin);
const original = fs.readFileSync(srcTwinAbs, 'utf8');
fs.writeFileSync(srcTwinAbs, `${original}\n// mutated for test — must not be flagged\n`);
try {
const findings = checkRow(row);
assert.deepEqual(
findings,
[],
`hand-authored twin must be excluded from byte-compare; unexpected findings: ${JSON.stringify(findings)}`,
);
} finally {
fs.writeFileSync(srcTwinAbs, original);
}
});
test('#3881 review, finding 4: srcTwin is NOT dead for a hand-authored row — a declared export the runtime does not have IS caught', () => {
// Before the fix, `srcTwin` was read only inside the `twinKind === 'upstream-verbatim'`
// branch; for a hand-authored row nothing ever consulted it, which is exactly how the
// js-yaml.d.cts docblock could drift from runtime reality (finding 2) unnoticed.
const row = jsYamlRow();
const srcTwinAbs = path.join(REPO_ROOT, row.srcTwin);
const original = fs.readFileSync(srcTwinAbs, 'utf8');
fs.writeFileSync(
srcTwinAbs,
`${original}\nexport function thisExportDoesNotExistAtRuntime(): void;\n`,
);
try {
const findings = checkRow(row);
assert.ok(
findings.some((f) => f.includes('thisExportDoesNotExistAtRuntime')),
`expected a declared-export-not-at-runtime finding, got: ${JSON.stringify(findings)}`,
);
} finally {
fs.writeFileSync(srcTwinAbs, original);
}
});
test('checkHandAuthoredTwin: fresh state is clean for the real js-yaml twin', () => {
assert.deepEqual(checkHandAuthoredTwin(jsYamlRow()), []);
});
test('declaredValueExports: extracts function/const/class exports, ignores type/interface exports', () => {
const src = [
'export interface Foo { x: number; }',
'export type Bar = string;',
'export function realFn(): void;',
'export const REAL_CONST: string;',
'export class RealClass {}',
].join('\n');
assert.deepEqual(declaredValueExports(src), ['realFn', 'REAL_CONST', 'RealClass']);
});
test('contrast: the SAME mutation on an upstream-verbatim row (re2js) IS caught — proving the exclusion is deliberate', () => {
const row = re2jsRow();
const srcTwinAbs = path.join(REPO_ROOT, row.srcTwin);
const original = fs.readFileSync(srcTwinAbs, 'utf8');
fs.writeFileSync(srcTwinAbs, `${original}\n// mutated for test — must be flagged\n`);
try {
const findings = checkRow(row);
assert.ok(findings.length > 0, 'expected the upstream-verbatim row to catch the same mutation the hand-authored row ignores');
} finally {
fs.writeFileSync(srcTwinAbs, original);
}
});
test("js-yaml.d.cts's declared surface is pinned (no upstream to byte-diff, so pin by contract instead)", () => {
const content = fs.readFileSync(path.join(REPO_ROOT, 'src/vendor/js-yaml.d.cts'), 'utf8');
// The declared surface is deliberately narrow (ADR-3473 §8.1: only what the FAILSAFE
// read/write path needs). Pin each declared export by name.
assert.match(content, /export function load\(/, 'load export missing');
assert.match(content, /export function dump\(/, 'dump export missing');
assert.match(content, /export const FAILSAFE_SCHEMA:/, 'FAILSAFE_SCHEMA export missing');
assert.match(content, /export class YAMLException/, 'YAMLException export missing');
// Deliberately NOT declared — anchors/aliases/custom types/loadAll are unreachable
// from typed code through this twin (the security posture this twin encodes). Check
// for an actual export statement, not just the word (which legitimately appears in
// this file's own prose explaining the exclusion).
assert.doesNotMatch(content, /export function loadAll\(/, 'loadAll must stay undeclared per the narrowed surface');
});
});
describe('#4573: pinOperatorPrefix / fixRow — mechanical --fix for Dependabot-range vendor drift', () => {
test('pinOperatorPrefix extracts the leading range-operator token, or "" for an exact pin', () => {
assert.equal(pinOperatorPrefix('^4.3.1'), '^');
assert.equal(pinOperatorPrefix('~4.3.1'), '~');
assert.equal(pinOperatorPrefix('4.3.1'), '');
assert.equal(pinOperatorPrefix('>=4.3.1'), '>=');
});
test('regression guard: stripRangeOperator is unchanged for the same inputs after the PIN_OPERATOR_RE refactor', () => {
assert.equal(stripRangeOperator('^4.3.1'), '4.3.1');
assert.equal(stripRangeOperator('~4.3.1'), '4.3.1');
assert.equal(stripRangeOperator('4.3.1'), '4.3.1');
assert.equal(stripRangeOperator('>=4.3.1'), '4.3.1');
});
test('fixRow resolves mechanical .cjs drift: mutated vendored js-yaml.cjs is byte-restored to match node_modules', (t) => {
// Operates on an ISOLATED TEMP COPY, never the real gsd-core/bin/lib/vendor/js-yaml.cjs.
// node --test runs files concurrently; the real file is `require()`-able by other test
// files at any moment, and fs.copyFileSync's write is not atomic against a concurrent
// reader on every filesystem. A prior version of this test wrote directly to the real
// file and a concurrent require() elsewhere caught it mid-overwrite, reading a truncated
// file and crashing an unrelated test with a SyntaxError -- confirmed via real CI logs,
// not a hypothetical. upstreamCjs stays pointed at the real node_modules copy (read-only,
// nothing writes to node_modules during tests, so sharing it is safe); only the
// destination is redirected to a private temp path.
const row = jsYamlRow();
const upstreamAbs = path.join(REPO_ROOT, row.upstreamCjs);
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lint-vendored-deps-fixrow-'));
const tempVendoredAbs = path.join(tmpDir, 'js-yaml.cjs');
t.after(() => cleanup(tmpDir));
const original = fs.readFileSync(upstreamAbs, 'utf8');
fs.writeFileSync(tempVendoredAbs, `${original}\n// mutated for test\n`);
const tempRow = { ...row, vendoredCjs: tempVendoredAbs };
const findings = fixRow(tempRow);
assert.deepEqual(findings, [], `expected fixRow to leave zero findings, got: ${JSON.stringify(findings)}`);
assert.ok(
fs.readFileSync(tempVendoredAbs).equals(fs.readFileSync(upstreamAbs)),
'expected the vendored .cjs to byte-equal node_modules/js-yaml/dist/js-yaml.js after fixRow',
);
});
test('fixRow does NOT mask a genuine hand-authored-twin incompatibility: a fake declared export still surfaces after --fix', (t) => {
// fixRow's first line unconditionally copies onto row.vendoredCjs regardless of what this
// test is exercising -- redirect it to a private temp path too, same reasoning as the
// preceding test (avoid ANY write to the real, shared, concurrently-`require()`-able
// gsd-core/bin/lib/vendor/js-yaml.cjs). srcTwin (a .d.cts type-only file, never require()'d
// at runtime) is mutated in place as before -- this test's actual subject.
const row = jsYamlRow();
const srcTwinAbs = path.join(REPO_ROOT, row.srcTwin);
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lint-vendored-deps-fixrow-'));
const tempVendoredAbs = path.join(tmpDir, 'js-yaml.cjs');
t.after(() => cleanup(tmpDir));
const tempRow = { ...row, vendoredCjs: tempVendoredAbs };
const original = fs.readFileSync(srcTwinAbs, 'utf8');
fs.writeFileSync(
srcTwinAbs,
`${original}\nexport function thisFixRowExportDoesNotExistAtRuntime(): void;\n`,
);
t.after(() => {
fs.writeFileSync(srcTwinAbs, original);
});
const findings = fixRow(tempRow);
assert.ok(
findings.some((f) => f.includes('thisFixRowExportDoesNotExistAtRuntime')),
`expected the hand-authored-twin finding to survive fixRow, got: ${JSON.stringify(findings)}`,
);
});
test('fixRow preserves the pin\'s original range-operator style when rewriting package.json', (t) => {
// Isolates BOTH real-file writes fixRow makes: the vendoredCjs copy (redirected to a temp
// path, same reasoning as the two tests above) AND the package.json pin rewrite this test
// specifically exercises (redirected via fixRow's pkgRoot parameter to an isolated temp
// root containing its own package.json + node_modules/js-yaml/package.json). Neither the
// real vendored .cjs nor the real package.json is touched -- both are readable at module
// top-level by other concurrently-running node --test files, the same race class already
// fixed for the vendored .cjs.
const row = jsYamlRow();
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lint-vendored-deps-fixrow-'));
t.after(() => cleanup(tmpDir));
const tempVendoredAbs = path.join(tmpDir, 'js-yaml.cjs');
const tempRow = { ...row, vendoredCjs: tempVendoredAbs };
const realInstalledPkgPath = path.join(REPO_ROOT, 'node_modules', 'js-yaml', 'package.json');
const installedVersion = JSON.parse(fs.readFileSync(realInstalledPkgPath, 'utf8')).version;
const tempPkgRoot = path.join(tmpDir, 'pkgroot');
const tempNodeModulesJsYamlDir = path.join(tempPkgRoot, 'node_modules', 'js-yaml');
fs.mkdirSync(tempNodeModulesJsYamlDir, { recursive: true });
const stalePin = installedVersion === '4.0.0' ? '~4.0.1' : '~4.0.0';
fs.writeFileSync(
path.join(tempPkgRoot, 'package.json'),
`${JSON.stringify({ devDependencies: { 'js-yaml': stalePin } }, null, 2)}\n`,
);
fs.writeFileSync(
path.join(tempNodeModulesJsYamlDir, 'package.json'),
`${JSON.stringify({ name: 'js-yaml', version: installedVersion }, null, 2)}\n`,
);
const findings = fixRow(tempRow, tempPkgRoot);
assert.deepEqual(findings, [], `expected fixRow to leave zero findings, got: ${JSON.stringify(findings)}`);
const after = JSON.parse(fs.readFileSync(path.join(tempPkgRoot, 'package.json'), 'utf8'));
const pinnedAfter = after.devDependencies['js-yaml'];
assert.equal(
pinnedAfter,
`~${installedVersion}`,
`expected fixRow to preserve the "~" range-operator style from the stale pin and correct the version to the installed one, got pin "${pinnedAfter}"`,
);
});
});