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.
449 lines
22 KiB
JavaScript
449 lines
22 KiB
JavaScript
/**
|
|
* Tests for the prompt-layer plan/summary-COUNTING drift guard (epic #3180,
|
|
* ADR-3180 Decision 4(e)) — `scripts/lint-planning-prompt-drift.cjs`.
|
|
*
|
|
* Covers:
|
|
* - `findPromptDrift` — the per-line detection shape (a `*...PLAN.md` /
|
|
* `*...SUMMARY.md` set glob AND a counting operator on the same line),
|
|
* and its documented near-miss exclusions.
|
|
* - `diffAgainstBaseline` — the three ratchet invariants (known / fresh /
|
|
* stale), keyed on TEXT not line number, exercised on synthetic input.
|
|
* - `loadBaseline` / `scanRepo` against the real, committed repo state —
|
|
* the guard's actual contract.
|
|
*
|
|
* Uses fs.mkdtempSync directly for the one synthetic-tree fixture, matching
|
|
* the sibling drift-guard test suites' own drift-guard sections — cleaned
|
|
* up in `t.after()`, never a fixed path.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const drift = require('../scripts/lint-planning-prompt-drift.cjs');
|
|
const { findPromptDrift, scanRepo, loadBaseline, diffAgainstBaseline, toPosixRel, writeBaseline } = drift;
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
const { runNode } = require('./helpers/process-seam.cjs');
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..');
|
|
const DRIFT_SCRIPT = path.join(REPO_ROOT, 'scripts', 'lint-planning-prompt-drift.cjs');
|
|
|
|
// ─── POSITIVE ───────────────────────────────────────────────────────────
|
|
|
|
describe('findPromptDrift — positive detection', () => {
|
|
test('X=$(ls dir/*-PLAN.md 2>/dev/null | wc -l) is detected', () => {
|
|
const line = 'X=$(ls dir/*-PLAN.md 2>/dev/null | wc -l)';
|
|
const out = findPromptDrift(line, 'msd-core/workflows/fake.md');
|
|
assert.strictEqual(out.length, 1);
|
|
assert.strictEqual(out[0].found, '*-PLAN.md');
|
|
assert.strictEqual(out[0].text, line);
|
|
});
|
|
|
|
test('the *-SUMMARY.md variant is detected', () => {
|
|
const line = 'X=$(ls dir/*-SUMMARY.md 2>/dev/null | wc -l)';
|
|
const out = findPromptDrift(line, 'msd-core/workflows/fake.md');
|
|
assert.strictEqual(out.length, 1);
|
|
assert.strictEqual(out[0].found, '*-SUMMARY.md');
|
|
});
|
|
|
|
test('a grep -c variant is detected', () => {
|
|
const line = "Y=$(grep -cE '^' dir/*-PLAN.md)";
|
|
const out = findPromptDrift(line, 'msd-core/workflows/fake.md');
|
|
assert.strictEqual(out.length, 1);
|
|
assert.strictEqual(out[0].found, '*-PLAN.md');
|
|
});
|
|
});
|
|
|
|
// ─── NEGATIVE — each with a comment saying WHY it must not fire ──────────
|
|
|
|
describe('findPromptDrift — negative: documented near-misses', () => {
|
|
test('grep -cE task-heading count inside ONE NAMED plan (no glob) is NOT detected', () => {
|
|
// A real line in msd-core/workflows/execute-plan.md: it counts <task>
|
|
// elements INSIDE one already-named plan file — no `*` glob token
|
|
// anywhere near PLAN.md — so it is not a plan-COUNT re-derivation. A
|
|
// false positive here would redden lint:ci on an untouched file.
|
|
const line = "grep -cE '^\\s*<task[[:space:]>]' .planning/phases/[current-phase-dir]/{phase}-{plan}-PLAN.md";
|
|
const out = findPromptDrift(line, 'msd-core/workflows/execute-plan.md');
|
|
assert.deepStrictEqual(out, []);
|
|
});
|
|
|
|
test('a *-UAT.md count is NOT detected', () => {
|
|
// UAT artifacts are a different derivation this guard does not own —
|
|
// PLAN_SUMMARY_GLOB_RE requires the literal PLAN.md or SUMMARY.md
|
|
// suffix, which "UAT.md" never satisfies.
|
|
const line = 'X=$(ls dir/*-UAT.md 2>/dev/null | wc -l)';
|
|
const out = findPromptDrift(line, 'msd-core/workflows/fake.md');
|
|
assert.deepStrictEqual(out, []);
|
|
});
|
|
|
|
test('a line that globs plan files but does not count them is NOT detected', () => {
|
|
// Reading/iterating (cat, backup, cross-reference) over a *-PLAN.md
|
|
// glob without a counting operator is not this derivation — every
|
|
// non-counting *-PLAN.md/*-SUMMARY.md glob in plan-phase.md is exactly
|
|
// this shape and is deliberately left alone.
|
|
const line = 'cat dir/*-PLAN.md';
|
|
const out = findPromptDrift(line, 'msd-core/workflows/fake.md');
|
|
assert.deepStrictEqual(out, []);
|
|
});
|
|
});
|
|
|
|
// ─── RATCHET MECHANICS — diffAgainstBaseline on synthetic inputs ─────────
|
|
|
|
describe('diffAgainstBaseline — ratchet invariants (synthetic)', () => {
|
|
test('a violation whose (file, text) pair is in the baseline is KNOWN: neither fresh nor stale', () => {
|
|
const baseline = [{ file: 'a.md', text: 'X=$(ls *-PLAN.md 2>/dev/null | wc -l)' }];
|
|
const violations = [
|
|
{ file: 'a.md', line: 10, found: '*-PLAN.md', text: 'X=$(ls *-PLAN.md 2>/dev/null | wc -l)' },
|
|
];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(fresh, []);
|
|
assert.deepStrictEqual(stale, []);
|
|
});
|
|
|
|
test('a violation absent from the baseline is FRESH: fails', () => {
|
|
const baseline = [];
|
|
const violations = [
|
|
{ file: 'a.md', line: 1, found: '*-PLAN.md', text: 'Y=$(grep -c dir/*-PLAN.md)' },
|
|
];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.strictEqual(fresh.length, 1);
|
|
assert.strictEqual(fresh[0].text, 'Y=$(grep -c dir/*-PLAN.md)');
|
|
assert.deepStrictEqual(stale, []);
|
|
});
|
|
|
|
test('a baseline entry matching nothing this run is STALE: fails', () => {
|
|
const baseline = [{ file: 'a.md', text: 'X=$(ls *-PLAN.md 2>/dev/null | wc -l)' }];
|
|
const violations = [];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(fresh, []);
|
|
assert.strictEqual(stale.length, 1);
|
|
assert.strictEqual(stale[0].text, 'X=$(ls *-PLAN.md 2>/dev/null | wc -l)');
|
|
});
|
|
|
|
test('keying is on TEXT not line number: the same trimmed text at a different line is still KNOWN', () => {
|
|
// This is what stops the baseline rotting on an unrelated edit that
|
|
// merely shifts line numbers (a new paragraph, a reworded step).
|
|
const baseline = [{ file: 'a.md', text: 'X=$(ls *-PLAN.md 2>/dev/null | wc -l)' }];
|
|
const violations = [
|
|
{ file: 'a.md', line: 999, found: '*-PLAN.md', text: 'X=$(ls *-PLAN.md 2>/dev/null | wc -l)' },
|
|
];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(fresh, []);
|
|
assert.deepStrictEqual(stale, []);
|
|
});
|
|
|
|
// ─── count-aware ratchet (Finding-3 fix): duplicate (file, text) pairs no
|
|
// longer make a partial migration invisible ───────────────────────────
|
|
|
|
test('a pair with count:2 fully matched by TWO occurrences is KNOWN: neither fresh nor stale', () => {
|
|
const baseline = [{ file: 'a.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)', count: 2 }];
|
|
const violations = [
|
|
{ file: 'a.md', line: 10, found: '*-PLAN.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)' },
|
|
{ file: 'a.md', line: 40, found: '*-PLAN.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)' },
|
|
];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(fresh, []);
|
|
assert.deepStrictEqual(stale, []);
|
|
});
|
|
|
|
test('a pair with count:2 but only ONE occurrence this run is a PARTIAL-migration STALE, naming both numbers', () => {
|
|
// This is the exact defect Finding 3 closes: migrating only ONE of two
|
|
// byte-identical sites must not be invisible to the ratchet just because
|
|
// the OTHER site still matches the (file, text) pair.
|
|
const baseline = [{ file: 'a.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)', count: 2 }];
|
|
const violations = [
|
|
{ file: 'a.md', line: 10, found: '*-PLAN.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)' },
|
|
];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(fresh, []);
|
|
assert.strictEqual(stale.length, 1);
|
|
assert.strictEqual(stale[0].count, 2);
|
|
assert.strictEqual(stale[0].actualCount, 1);
|
|
});
|
|
|
|
test('a pair with count:2 and ZERO occurrences this run is fully STALE (both sites migrated)', () => {
|
|
const baseline = [{ file: 'a.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)', count: 2 }];
|
|
const { fresh, stale } = diffAgainstBaseline([], baseline);
|
|
assert.deepStrictEqual(fresh, []);
|
|
assert.strictEqual(stale.length, 1);
|
|
assert.strictEqual(stale[0].actualCount, 0);
|
|
assert.strictEqual(stale[0].count, 2);
|
|
});
|
|
|
|
test('a pair with count:1 but a THIRD occurrence appears this run: the excess occurrence is FRESH (new copy)', () => {
|
|
const baseline = [{ file: 'a.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)', count: 1 }];
|
|
const violations = [
|
|
{ file: 'a.md', line: 10, found: '*-PLAN.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)' },
|
|
{ file: 'a.md', line: 55, found: '*-PLAN.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)' },
|
|
];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(stale, []);
|
|
assert.strictEqual(fresh.length, 1);
|
|
assert.strictEqual(fresh[0].line, 55);
|
|
});
|
|
|
|
test('an entry with no `count` field defaults to acknowledging exactly ONE occurrence', () => {
|
|
const baseline = [{ file: 'a.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)' }];
|
|
const violations = [
|
|
{ file: 'a.md', line: 10, found: '*-PLAN.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)' },
|
|
{ file: 'a.md', line: 55, found: '*-PLAN.md', text: 'DISK_PLANS=$(ls *-PLAN.md | wc -l)' },
|
|
];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(stale, []);
|
|
assert.strictEqual(fresh.length, 1);
|
|
assert.strictEqual(fresh[0].line, 55);
|
|
});
|
|
});
|
|
|
|
// ─── scanRepo — tree-walk mechanics on a synthetic tree ───────────────────
|
|
|
|
describe('scanRepo — synthetic tree', () => {
|
|
test('a violation in a fresh temp tree is reported with its file, line, and text', (t) => {
|
|
const root = createTempDir('msd-planning-prompt-drift-');
|
|
t.after(() => cleanup(root));
|
|
fs.mkdirSync(path.join(root, 'msd-core', 'workflows'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, 'msd-core', 'workflows', 'fake.md'),
|
|
'X=$(ls dir/*-PLAN.md 2>/dev/null | wc -l)\n',
|
|
);
|
|
|
|
const violations = scanRepo(root);
|
|
assert.strictEqual(violations.length, 1);
|
|
// Always POSIX-separated regardless of the host OS's native separator
|
|
// (`path.join` would build native separators here, which is exactly the
|
|
// Windows-vs-POSIX mismatch this guard's baseline keying must not have —
|
|
// see the Windows-shaped-path coverage below).
|
|
assert.strictEqual(violations[0].file, 'msd-core/workflows/fake.md');
|
|
assert.strictEqual(violations[0].line, 1);
|
|
assert.strictEqual(violations[0].found, '*-PLAN.md');
|
|
});
|
|
|
|
test('a clean temp tree with no re-derivations reports zero violations', (t) => {
|
|
const root = createTempDir('msd-planning-prompt-drift-');
|
|
t.after(() => cleanup(root));
|
|
fs.mkdirSync(path.join(root, 'msd-core', 'workflows'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'msd-core', 'workflows', 'clean.md'), 'no globs or counts here\n');
|
|
|
|
const violations = scanRepo(root);
|
|
assert.deepStrictEqual(violations, []);
|
|
});
|
|
});
|
|
|
|
// ─── WINDOWS PATH-SEPARATOR NORMALIZATION — the #3223 regression ─────────
|
|
//
|
|
// `scanTree` (scripts/lib/drift-scan.cjs) builds its repo-relative path via
|
|
// `path.relative()`, which uses NATIVE separators. On Windows that is
|
|
// `msd-core\workflows\progress.md`, while the committed baseline
|
|
// (`scripts/baselines/planning-prompt-drift-baseline.json`) stores POSIX
|
|
// paths — an un-normalized Windows path silently fails to match ANY
|
|
// baseline entry, so every violation reports FRESH and every baseline entry
|
|
// reports STALE (a 100% guard failure on Windows, caught by GitHub Actions'
|
|
// Windows CI lane on PR #3223; the Linux-only remote runner this repo
|
|
// otherwise gates on cannot see this class at all).
|
|
//
|
|
// This coverage drives the pure functions with a Windows-shaped path
|
|
// directly — no mocking of the filesystem and NOT gated on
|
|
// `process.platform` — so it fails identically on every OS pre-fix and
|
|
// passes identically on every OS post-fix. Skipping it on non-Windows would
|
|
// recreate the exact blind spot that let this ship.
|
|
|
|
describe('Windows-shaped repo-relative paths are normalized to POSIX', () => {
|
|
const WINDOWS_REL = 'msd-core\\workflows\\progress.md';
|
|
const POSIX_REL = 'msd-core/workflows/progress.md';
|
|
const WINDOWS_LINE = 'X=$(ls dir/*-PLAN.md 2>/dev/null | wc -l)';
|
|
|
|
test('toPosixRel converts a Windows-shaped separator run to POSIX, and is a no-op on an already-POSIX path', () => {
|
|
assert.strictEqual(toPosixRel(WINDOWS_REL), POSIX_REL);
|
|
assert.strictEqual(toPosixRel(POSIX_REL), POSIX_REL);
|
|
});
|
|
|
|
test('findPromptDrift on a Windows-shaped relPath reports a POSIX `file`, regardless of input separator', () => {
|
|
const out = findPromptDrift(WINDOWS_LINE, WINDOWS_REL);
|
|
assert.strictEqual(out.length, 1);
|
|
assert.strictEqual(out[0].file, POSIX_REL);
|
|
assert.ok(!out[0].file.includes('\\'), 'reported file must carry no backslashes');
|
|
});
|
|
|
|
test('a violation produced from a Windows-shaped path matches a POSIX baseline entry: classified KNOWN, not fresh and not stale', () => {
|
|
const baseline = [{ file: POSIX_REL, text: WINDOWS_LINE }];
|
|
const violations = findPromptDrift(WINDOWS_LINE, WINDOWS_REL);
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(fresh, []);
|
|
assert.deepStrictEqual(stale, []);
|
|
});
|
|
|
|
test('a violation produced from a Windows-shaped path does NOT match if left un-normalized (sanity check the assertion above is meaningful)', () => {
|
|
// Same inputs as the previous test, but bypassing toPosixRel to prove the
|
|
// KNOWN classification above is actually exercising normalization, not a
|
|
// coincidence of the fixture.
|
|
const baseline = [{ file: POSIX_REL, text: WINDOWS_LINE }];
|
|
const violations = [{ file: WINDOWS_REL, line: 1, found: '*-PLAN.md', text: WINDOWS_LINE }];
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.strictEqual(fresh.length, 1);
|
|
assert.strictEqual(stale.length, 1);
|
|
});
|
|
|
|
test('--update (writeBaseline) serializes a POSIX `file` for a Windows-shaped input', (t) => {
|
|
const root = createTempDir('msd-planning-prompt-drift-update-');
|
|
t.after(() => cleanup(root));
|
|
const violations = findPromptDrift(WINDOWS_LINE, WINDOWS_REL);
|
|
writeBaseline(root, violations);
|
|
const written = JSON.parse(fs.readFileSync(path.join(root, 'scripts', 'baselines', 'planning-prompt-drift-baseline.json'), 'utf8'));
|
|
assert.strictEqual(written.entries.length, 1);
|
|
assert.strictEqual(written.entries[0].file, POSIX_REL);
|
|
assert.ok(!written.entries[0].file.includes('\\'), 'written baseline entry must carry no backslashes');
|
|
});
|
|
});
|
|
|
|
// ─── BASELINE INTEGRITY — both directions, against the real repo ─────────
|
|
|
|
test('loadBaseline on the committed baseline returns ZERO entries — Phase 8 (#3218) burned the ratchet to zero', () => {
|
|
// Pre-#3218 this asserted 6 entries / 7 occurrences (the shell
|
|
// re-derivations). #3218 migrated all 7 sites to `msd_run query find-phase`
|
|
// and, per ADR-3180 Decision 4(e), emptied the baseline rather than
|
|
// acknowledging them going stale — a stale entry left behind after its
|
|
// site migrates ALSO fails (see the "scanRepo matches the baseline
|
|
// exactly" test below), so an empty baseline is the only way this guard
|
|
// can be green on an EARNED zero rather than a baseline still covering
|
|
// sites that no longer fire.
|
|
const { entries, errors } = loadBaseline(REPO_ROOT);
|
|
assert.deepStrictEqual(errors, []);
|
|
assert.deepStrictEqual(entries, []);
|
|
});
|
|
|
|
test('scanRepo(repoRoot) matches the baseline exactly: zero fresh AND zero stale', () => {
|
|
// The guard's actual contract: every re-derivation this run finds is
|
|
// already acknowledged in the baseline, and every baseline entry still
|
|
// fires — no fresh, no stale, in either direction.
|
|
const violations = scanRepo(REPO_ROOT);
|
|
const { entries: baseline, errors } = loadBaseline(REPO_ROOT);
|
|
assert.deepStrictEqual(errors, []);
|
|
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
|
|
assert.deepStrictEqual(fresh, []);
|
|
assert.deepStrictEqual(stale, []);
|
|
});
|
|
|
|
// ─── CLI end-to-end against an ISOLATED --root tree (#3640) ─────────────────
|
|
//
|
|
// This block REPLACES the original E5 shape, which wrote its fixture
|
|
// directly into the real, shared msd-core/workflows/ because main() hardcoded
|
|
// its scan root — and any parallel consumer of the real tree could then
|
|
// observe the fixture mid-lifecycle (the emitted-provenance install baked it
|
|
// into all 19 manifests and later failed its existsSync pass once t.after()
|
|
// removed it; the #3333 TOCTOU ENOENT crash in copyWithPathReplacement was
|
|
// the same writer's first documented symptom). These rows drive the SAME
|
|
// real CLI, through the same exit path, with an explicit --root override
|
|
// pointing at an isolated temp tree — the guard's fail path is proven
|
|
// end-to-end with zero writes into the shared source tree.
|
|
|
|
describe('CLI end-to-end: --root scan-root override (#3640)', () => {
|
|
// The E5 fixture line, shared by every row that needs a violation present.
|
|
const DRIFT_FIXTURE_LINE = 'FIXTURE_COUNT=$(ls .planning/phases/zzz/*-PLAN.md 2>/dev/null | wc -l)\n';
|
|
|
|
// A scan-root skeleton: the SCAN_DIR tree main() walks plus a valid empty
|
|
// baseline, so the run exercises the real loadBaseline -> diff -> report
|
|
// path rather than erroring on a missing baseline.
|
|
function makeScanRoot(prefix) {
|
|
const root = createTempDir(prefix);
|
|
fs.mkdirSync(path.join(root, 'msd-core', 'workflows'), { recursive: true });
|
|
fs.mkdirSync(path.join(root, 'scripts', 'baselines'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, 'scripts', 'baselines', 'planning-prompt-drift-baseline.json'),
|
|
`${JSON.stringify({ entries: [] }, null, 2)}\n`,
|
|
);
|
|
return root;
|
|
}
|
|
|
|
test('a fresh, unacknowledged plan-count re-derivation under --root exits 1 and names itself in stderr', (t) => {
|
|
const root = makeScanRoot('msd-planning-prompt-drift-root-');
|
|
t.after(() => cleanup(root));
|
|
fs.writeFileSync(path.join(root, 'msd-core', 'workflows', 'zzz-e5-drift-fixture.md'), DRIFT_FIXTURE_LINE);
|
|
|
|
const result = runNode([DRIFT_SCRIPT, '--root', root]);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 1);
|
|
// The reported rel path and the echoed violation text are DATA, not
|
|
// formatter prose — the same two anchors the E5 block asserted on. The
|
|
// exit code carries the verdict; these anchors carry the WHICH.
|
|
assert.match(result.stderr, /msd-core\/workflows\/zzz-e5-drift-fixture\.md/);
|
|
assert.match(result.stderr, /FIXTURE_COUNT=/);
|
|
});
|
|
|
|
test('a clean tree under --root exits 0 (clean-fixture control for the row above)', (t) => {
|
|
const root = makeScanRoot('msd-planning-prompt-drift-clean-');
|
|
t.after(() => cleanup(root));
|
|
fs.writeFileSync(path.join(root, 'msd-core', 'workflows', 'clean.md'), 'no globs or counts here\n');
|
|
|
|
const result = runNode([DRIFT_SCRIPT, '--root', root]);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 0);
|
|
});
|
|
|
|
test('an empty tree with a valid empty baseline under --root exits 0', (t) => {
|
|
const root = makeScanRoot('msd-planning-prompt-drift-empty-');
|
|
|
|
t.after(() => cleanup(root));
|
|
const result = runNode([DRIFT_SCRIPT, '--root', root]);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 0);
|
|
});
|
|
|
|
test('the default invocation (no --root) still scans the real repo green', () => {
|
|
// The override must not change the guard lint:ci actually runs: bare
|
|
// invocation scans the real repo against the committed (empty) baseline.
|
|
const result = runNode([DRIFT_SCRIPT]);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 0);
|
|
});
|
|
|
|
test('a --root scan leaves the real msd-core/workflows directory untouched', (t) => {
|
|
const realWorkflows = path.join(REPO_ROOT, 'msd-core', 'workflows');
|
|
const before = fs.readdirSync(realWorkflows).sort();
|
|
const root = makeScanRoot('msd-planning-prompt-drift-untouched-');
|
|
t.after(() => cleanup(root));
|
|
fs.writeFileSync(path.join(root, 'msd-core', 'workflows', 'zzz-e5-drift-fixture.md'), DRIFT_FIXTURE_LINE);
|
|
|
|
const result = runNode([DRIFT_SCRIPT, '--root', root]);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 1);
|
|
// The #3640 acceptance criterion, as behavior: the E5 scenario's fixture
|
|
// never appears in the shared tree the parallel chunks observe.
|
|
assert.deepStrictEqual(fs.readdirSync(realWorkflows).sort(), before);
|
|
});
|
|
|
|
// ─── Usage errors: exit 2, the code distinct from the guard's findings
|
|
// exit (1) — a typed discriminator, so these rows assert the exit code
|
|
// alone and never parse stderr prose. ─────────────────────────────────
|
|
|
|
test('--root without a value is a usage error (exit 2)', () => {
|
|
const result = runNode([DRIFT_SCRIPT, '--root']);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 2);
|
|
});
|
|
|
|
test('--root with an empty-string value is a usage error (exit 2)', () => {
|
|
const result = runNode([DRIFT_SCRIPT, '--root', '']);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 2);
|
|
});
|
|
|
|
test('--root with a flag-shaped value is a usage error, never a scan root (exit 2)', () => {
|
|
// `--root --update` must not resolve the FLAG into a cwd-relative path —
|
|
// composed with --update that would mkdir a stray baseline tree inside
|
|
// the shared source directory (#3640's own write class).
|
|
const result = runNode([DRIFT_SCRIPT, '--root', '--update', '--update']);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 2);
|
|
});
|
|
|
|
test('--root naming an existing FILE is a usage error, not an ENOTDIR crash (exit 2)', () => {
|
|
const result = runNode([DRIFT_SCRIPT, '--root', path.join(REPO_ROOT, 'package.json')]);
|
|
assert.strictEqual(result.outcome, 'exited');
|
|
assert.strictEqual(result.exitCode, 2);
|
|
});
|
|
});
|
|
|