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.
154 lines
6.4 KiB
JavaScript
154 lines
6.4 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Regression test for #2944 — 85140eac8 "refresh the predicate index staled
|
|
* by a merge race on next" (docs/CONTEXT-INDEX.json) shipped with zero
|
|
* behavioral test coverage.
|
|
*
|
|
* docs/CONTEXT-INDEX.json is a committed generated artifact guarded by
|
|
* `scripts/gen-context-index.cjs --check` in `lint:generated-sync`. A
|
|
* concurrent-merge race landed one PR's CONTEXT.md alongside another PR's
|
|
* index; each PR was green alone (lint only runs on the PR's own diff), the
|
|
* combination was red, and it only surfaced on the NEXT PR to run `lint:ci`.
|
|
* This file puts the same drift check inside the test suite (which
|
|
* `msd-test` runs on every PR), so a future racing merge fails here directly
|
|
* instead of waiting for a downstream PR to trip over `lint:generated-sync`.
|
|
*
|
|
* The example/production parser parity guard (the OTHER defect #2944
|
|
* introduced regression coverage for) lives in
|
|
* scripts/lint-example-parser-parity.cjs, wired into `npm run lint:ci` — NOT
|
|
* here. ADR-1671 ("Dynamic context management platform",
|
|
* docs/adr/1671-dynamic-context-management-platform.md:102) places
|
|
* examples/dynamic-context-management/ deliberately outside four surfaces:
|
|
* the build (src/ -> bin/lib/), the npm package files[], the installer, and
|
|
* the CI test suite (tests/) — this file. A `require()` of the example from
|
|
* inside tests/ would violate that fourth exclusion directly; a lint script
|
|
* that only reads both modules from a repo-root script does not.
|
|
*/
|
|
|
|
process.env.MSD_TEST_MODE = '1';
|
|
|
|
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 helpers = require('./helpers.cjs');
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
const LIB_DIR = path.join(REPO_ROOT, 'msd-core', 'bin', 'lib');
|
|
const PROD_PREDICATES_PATH = path.join(LIB_DIR, 'context-predicates.cjs');
|
|
const CONTEXT_PATH = path.join(REPO_ROOT, 'CONTEXT.md');
|
|
const INDEX_PATH = path.join(REPO_ROOT, 'docs', 'CONTEXT-INDEX.json');
|
|
|
|
const prodPredicates = require(PROD_PREDICATES_PATH);
|
|
|
|
function readContextMarkdown() {
|
|
return fs.readFileSync(CONTEXT_PATH, 'utf8');
|
|
}
|
|
|
|
function readCommittedIndex() {
|
|
return JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8'));
|
|
}
|
|
|
|
/**
|
|
* Diff two ContextIndex-shaped `{predicates:[{id,klass,value}]}` objects by
|
|
* id, returning human-readable divergence strings naming the exact
|
|
* predicate id(s) involved — so a failure reads as an actionable list, never
|
|
* "objects differ".
|
|
*/
|
|
function diffPredicatesById(leftPredicates, rightPredicates, leftLabel, rightLabel) {
|
|
const leftMap = new Map(leftPredicates.map((p) => [p.id, p]));
|
|
const rightMap = new Map(rightPredicates.map((p) => [p.id, p]));
|
|
const allIds = new Set([...leftMap.keys(), ...rightMap.keys()]);
|
|
const diffs = [];
|
|
for (const id of Array.from(allIds).sort()) {
|
|
const l = leftMap.get(id);
|
|
const r = rightMap.get(id);
|
|
if (l && !r) {
|
|
diffs.push(`${id}: present in ${leftLabel} but absent from ${rightLabel}`);
|
|
} else if (!l && r) {
|
|
diffs.push(`${id}: present in ${rightLabel} but absent from ${leftLabel}`);
|
|
} else if (l.value !== r.value) {
|
|
diffs.push(
|
|
`${id}: value diverged (${leftLabel}=${JSON.stringify(l.value)}, ${rightLabel}=${JSON.stringify(r.value)})`,
|
|
);
|
|
} else if (l.klass !== r.klass) {
|
|
diffs.push(
|
|
`${id}: klass diverged (${leftLabel}=${JSON.stringify(l.klass)}, ${rightLabel}=${JSON.stringify(r.klass)})`,
|
|
);
|
|
}
|
|
}
|
|
return diffs;
|
|
}
|
|
|
|
// ─── docs/CONTEXT-INDEX.json vs. a fresh CONTEXT.md parse ────────────────────
|
|
|
|
describe('docs/CONTEXT-INDEX.json sync with CONTEXT.md (#2944 concurrent-merge regression)', () => {
|
|
test('committed index matches a fresh parse of the real CONTEXT.md, predicate-by-predicate', () => {
|
|
const { parsePredicates, buildIndex } = prodPredicates;
|
|
const markdown = readContextMarkdown();
|
|
const { predicates } = parsePredicates(markdown);
|
|
const fresh = buildIndex(predicates);
|
|
const committed = readCommittedIndex();
|
|
|
|
const diffs = diffPredicatesById(
|
|
committed.predicates,
|
|
fresh.predicates,
|
|
'committed docs/CONTEXT-INDEX.json',
|
|
'fresh CONTEXT.md parse',
|
|
);
|
|
assert.deepEqual(
|
|
diffs,
|
|
[],
|
|
'docs/CONTEXT-INDEX.json has drifted from CONTEXT.md for predicate id(s) ' +
|
|
'(run `node scripts/gen-context-index.cjs --write` to refresh):\n' +
|
|
diffs.join('\n'),
|
|
);
|
|
assert.equal(
|
|
committed.count,
|
|
fresh.count,
|
|
`predicate count diverged: committed=${committed.count} fresh=${fresh.count}`,
|
|
);
|
|
assert.deepEqual(
|
|
committed.classes,
|
|
fresh.classes,
|
|
'class-count map diverged between committed index and fresh CONTEXT.md parse:\n' +
|
|
`committed=${JSON.stringify(committed.classes)}\nfresh=${JSON.stringify(fresh.classes)}`,
|
|
);
|
|
});
|
|
|
|
test('divergence detector is not vacuous: catches a mutated index in a throwaway temp copy (never the real artifact)', (t) => {
|
|
const { parsePredicates, buildIndex } = prodPredicates;
|
|
const markdown = readContextMarkdown();
|
|
const { predicates } = parsePredicates(markdown);
|
|
const fresh = buildIndex(predicates);
|
|
|
|
// Mutate a COPY of the fresh index in a fresh temp dir. docs/CONTEXT-
|
|
// INDEX.json itself is never opened for write anywhere in this file.
|
|
const mutated = JSON.parse(JSON.stringify(fresh));
|
|
const target =
|
|
mutated.predicates.find((p) => p.id === 'RULESET.WORKFLOW_SIZE_BUDGET') || mutated.predicates[0];
|
|
target.value = target.value + ' MUTATED-FOR-TEST';
|
|
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'context-index-sync-'));
|
|
t.after(() => helpers.cleanup(tmpDir));
|
|
const tmpIndexPath = path.join(tmpDir, 'CONTEXT-INDEX.json');
|
|
fs.writeFileSync(tmpIndexPath, JSON.stringify(mutated, null, 2) + '\n', 'utf8');
|
|
const mutatedCommitted = JSON.parse(fs.readFileSync(tmpIndexPath, 'utf8'));
|
|
|
|
const diffs = diffPredicatesById(
|
|
mutatedCommitted.predicates,
|
|
fresh.predicates,
|
|
'mutated temp-copy index',
|
|
'fresh CONTEXT.md parse',
|
|
);
|
|
|
|
assert.ok(diffs.length > 0, 'expected the mutated temp copy to diverge from the fresh parse');
|
|
assert.ok(
|
|
diffs.some((d) => d.startsWith(`${target.id}:`)),
|
|
`expected the diff to name ${target.id}; got:\n${diffs.join('\n')}`,
|
|
);
|
|
});
|
|
});
|