The operator decided on 2026-09-18 that the two checks must stop measuring a
phrase and start measuring behaviour. The reason is the gate's own caveat: a
phrase probe closes on rewording, so "Phase 7 no longer says 'Skip for
trekplans'" and "the rubric no longer says 'exists and passes'" could both have
been satisfied by an edit that changed no behaviour at all. Both entries now
point at code a test exercises:
D-03 lib/**/*.mjs exports runPlanVerification
D-04 lib/**/*.mjs exports formatCriteriaEvidence
Verified open against the pre-fix tree (8d1669e) and closed against this one —
a check that cannot be seen to fire is not evidence.
What the probes can and cannot prove is now stated by the gate itself rather
than left to the reader. `PROBE_NOTES` carries one note per kind, the detail
line prints a note for every kind PRESENT in the row (and none for a kind no
entry declares), and the open/closed tags name the kind instead of labelling
`phrase` alone and leaving every other kind unlabelled. A behaviour probe
proves the capability exists; it does not prove a prose phase calls it — the
wiring is pinned by tests/lib/doc-consistency.test.mjs, and the gate says so.
THE DENOMINATOR MOVED ON PURPOSE. `tests/fixtures/end-state-frozen.json` and
the `FROZEN` literal in `tests/scripts/end-state-gate.test.mjs` carry new
signatures for D-03 and D-04, amended together and dated in both places. That
is the freeze mechanism working, not a bypass: the integrity check went
VIOLATED the moment the checks changed and stayed red until the amendment was
written down. The list may change — only as a decision said out loud. Nothing
else about the denominator moved: seven defect ids, three experiment ids, and
the agents/decisions/freeze configuration are unchanged, and no entry was
removed.
Gate after: defects 0 of 7, registry intact, still RED and exit 1 on the other
four rows — which is correct.
Suite: 1120 (1118/0/2), up 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
761 lines
34 KiB
JavaScript
761 lines
34 KiB
JavaScript
// tests/scripts/end-state-gate.test.mjs
|
|
// The end-state gate counts the distance to "Voyage is finished" as five tallies:
|
|
// open pipeline defects, unresolved experiments/opt-ins, dormant agents, open operator
|
|
// decisions, and feat commits after the freeze tag. "Finished" = all five at 0 AND the
|
|
// registry the gate counts from is intact against its frozen denominator.
|
|
//
|
|
// These tests pin the gate's MECHANICS in both directions — every tally can be 0 and can
|
|
// be > 0 — against throwaway fixture trees. They deliberately do NOT assert that the real
|
|
// repo is red today: the gate is supposed to go green, and a test that pins the current
|
|
// distance would fail on the day the work is done. What they DO pin about the real repo
|
|
// is the denominator itself (see "the frozen denominator" below).
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, dirname, resolve } from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
evaluateCondition,
|
|
evaluateCheck,
|
|
measure,
|
|
loadRegistry,
|
|
loadFrozen,
|
|
spawnedNames,
|
|
checkSignature,
|
|
verifyIntegrity,
|
|
render,
|
|
PROBE_NOTES,
|
|
} from '../../scripts/end-state-gate.mjs';
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const GATE = join(ROOT, 'scripts', 'end-state-gate.mjs');
|
|
|
|
// --- the frozen denominator ------------------------------------------------
|
|
// Denominator frozen 2026-09-17 by the end-state work order, the day the end-state
|
|
// direction was chosen. An entry may CLOSE; it may never be removed or have its check
|
|
// changed, and the gate's agent/decision/freeze configuration may not move. To change
|
|
// anything below you must change this test — say in the commit message who decided it.
|
|
const FROZEN = {
|
|
defects: {
|
|
'D-01': '48f7f5cca070091e8f9b9ca0c7cdb7a1880733691935c25d07ea1d3e8d508fc9',
|
|
'D-02': 'dfc94dba04a9116ae9be2097a0c5a9d8313b02c7de1ca6b49a472c1823a6d1e1',
|
|
// Amended 2026-09-18 (operator decision): D-03/D-04 became behaviour probes.
|
|
'D-03': '20d2837af1a1247220c65e2a04069b41a3e922c2452369294da4d62bb985636b',
|
|
'D-04': '200a142e7792eb00a0f0a58bbfb3e548585d9b45ba36a5c5bf94f28f2c896ffb',
|
|
'D-05': '8fe6df52b43496c3302400507cb98002dcf8d2a12d92085e14bda4c7819bc7ba',
|
|
'D-06': '72a787c8154d1c18849e9856dcf1a67703d6b5541f88db81d6bab13cab14b418',
|
|
'D-07': '1bf3e0cd36c621720697475b0a3fd7db78611d66b58a9ce035e7695e57c69f1b',
|
|
},
|
|
experiments: {
|
|
'E-01': 'f7718897cec8ace6879fe79f5f32760ec957c3642e7186f8aa1688ce77b3f4e8',
|
|
'E-02': 'c8ff12c504ba7a9ac36f11bb8f9b713263e9b4eb28c07d984d9e2115980e740d',
|
|
'E-03': '17153ad8808615a833a49ae0c3523264ce055aa690e5542a75bae1874495e7e8',
|
|
},
|
|
agents: {
|
|
dir: 'agents',
|
|
spawnSites: 'commands/*.md',
|
|
referenceMarker: 'Reference document, not a spawnable capability',
|
|
},
|
|
decisions: {
|
|
file: 'STATE.md',
|
|
section: '## Åpne operatørbeslutninger',
|
|
open: '^- \\[ \\] ',
|
|
closed: '^- \\[x\\] ',
|
|
},
|
|
freeze: { tag: 'end-state-freeze', featPattern: '^feat(\\(|!|:)' },
|
|
};
|
|
|
|
// --- helpers ---------------------------------------------------------------
|
|
|
|
function fixture(files) {
|
|
const dir = mkdtempSync(join(tmpdir(), 'end-state-gate-'));
|
|
for (const [rel, body] of Object.entries(files)) {
|
|
const p = join(dir, rel);
|
|
mkdirSync(dirname(p), { recursive: true });
|
|
writeFileSync(p, body);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
const cleanup = (...dirs) => { for (const d of dirs) rmSync(d, { recursive: true, force: true }); };
|
|
|
|
const DECISIONS = { file: 'STATE.md', section: '## Open decisions', open: '^- \\[ \\] ', closed: '^- \\[x\\] ' };
|
|
const AGENTS = { dir: 'agents', spawnSites: 'commands/*.md', referenceMarker: 'Reference document, not a spawnable capability' };
|
|
const FREEZE = { tag: 'end-state-freeze', featPattern: '^feat(\\(|!|:)' };
|
|
|
|
function registry(overrides = {}) {
|
|
return { defects: [], experiments: [], agents: AGENTS, decisions: DECISIONS, freeze: FREEZE, ...overrides };
|
|
}
|
|
|
|
// A frozen manifest that matches `reg` exactly.
|
|
function frozenFor(reg) {
|
|
const sigs = (list) => Object.fromEntries(list.map((e) => [e.id, checkSignature(e.check)]));
|
|
return { defects: sigs(reg.defects), experiments: sigs(reg.experiments), agents: reg.agents, decisions: reg.decisions, freeze: reg.freeze };
|
|
}
|
|
|
|
function git(dir, ...args) {
|
|
const r = spawnSync('git', [
|
|
'-c', 'user.name=fixture', '-c', 'user.email=fixture@example.invalid',
|
|
'-c', 'core.hooksPath=/dev/null', '-c', 'commit.gpgsign=false', '-c', 'tag.gpgsign=false',
|
|
...args,
|
|
], { cwd: dir, encoding: 'utf8' });
|
|
assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
|
|
return r.stdout;
|
|
}
|
|
|
|
function gitInit(dir) {
|
|
git(dir, 'init', '-q');
|
|
git(dir, 'add', '-A');
|
|
git(dir, 'commit', '-q', '--allow-empty', '-m', 'chore: fixture baseline');
|
|
}
|
|
|
|
function gitCommit(dir, subject) {
|
|
git(dir, 'commit', '-q', '--allow-empty', '-m', subject);
|
|
}
|
|
|
|
const rowById = (result, id) => result.rows.find((r) => r.id === id);
|
|
|
|
// --- conditions -----------------------------------------------------------
|
|
|
|
test('evaluateCondition: match / no-match on a single file, multiline regex', () => {
|
|
const dir = fixture({ 'a.md': 'intro\n### Heading\nbody\n' });
|
|
try {
|
|
assert.equal(evaluateCondition(dir, { path: 'a.md', pattern: '^### Heading', flags: 'm', expect: 'match' }), true);
|
|
assert.equal(evaluateCondition(dir, { path: 'a.md', pattern: '^### Other', flags: 'm', expect: 'match' }), false);
|
|
assert.equal(evaluateCondition(dir, { path: 'a.md', pattern: '^### Other', flags: 'm', expect: 'no-match' }), true);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('evaluateCondition: globs (dir/*.ext, dir/**/*.ext) and path arrays match ANY file', () => {
|
|
const dir = fixture({
|
|
'commands/x.md': 'nothing here\n',
|
|
'lib/deep/y.mjs': 'const FLAG = process.env.SOME_FLAG;\n',
|
|
});
|
|
try {
|
|
assert.equal(evaluateCondition(dir, { path: 'commands/*.md', pattern: 'SOME_FLAG', expect: 'match' }), false);
|
|
assert.equal(evaluateCondition(dir, { path: 'lib/**/*.mjs', pattern: 'SOME_FLAG', expect: 'match' }), true);
|
|
assert.equal(evaluateCondition(dir, { path: ['commands/*.md', 'lib/**/*.mjs'], pattern: 'SOME_FLAG', expect: 'match' }), true);
|
|
assert.equal(evaluateCondition(dir, { path: ['commands/*.md', 'lib/**/*.mjs'], pattern: 'SOME_FLAG', expect: 'no-match' }), false);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('evaluateCondition: section scoping reads only from the heading to the next "## "', () => {
|
|
const dir = fixture({
|
|
'doc.md': '## 5. Design\nno status here\n## 6. PoC\n> **STATUS: RUN AND DECLINED**\n',
|
|
});
|
|
try {
|
|
const inFive = { path: 'doc.md', section: '## 5.', pattern: '^> \\*\\*STATUS:', flags: 'm', expect: 'no-match' };
|
|
const inSix = { ...inFive, section: '## 6.' };
|
|
assert.equal(evaluateCondition(dir, inFive), true, 'section 5 has no status block');
|
|
assert.equal(evaluateCondition(dir, inSix), false, 'section 6 has one');
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('evaluateCondition: a literal NUL byte is detectable', () => {
|
|
const dir = fixture({ 'nul.mjs': "const SEP = '\u0000';\n", 'clean.mjs': "const SEP = '\\x00';\n" });
|
|
try {
|
|
assert.equal(evaluateCondition(dir, { path: 'nul.mjs', pattern: '\\u0000', expect: 'match' }), true);
|
|
assert.equal(evaluateCondition(dir, { path: 'clean.mjs', pattern: '\\u0000', expect: 'match' }), false);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('evaluateCondition: a missing file, a missing glob base or a missing section throws', () => {
|
|
const dir = fixture({ 'a.md': 'x\n' });
|
|
try {
|
|
assert.throws(() => evaluateCondition(dir, { path: 'missing.md', pattern: 'x', expect: 'no-match' }), /missing\.md/);
|
|
assert.throws(() => evaluateCondition(dir, { path: 'nodir/*.md', pattern: 'x', expect: 'no-match' }), /nodir/);
|
|
assert.throws(() => evaluateCondition(dir, { path: 'a.md', section: '## 9.', pattern: 'x', expect: 'no-match' }), /## 9\./);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('evaluateCondition: a glob over an EXISTING dir with no matching files throws (no silent no-match)', () => {
|
|
// In a no-match condition, "silently nothing matched" would read as the condition holding.
|
|
const dir = fixture({ 'docs/readme.txt': 'x\n' });
|
|
try {
|
|
assert.throws(() => evaluateCondition(dir, { path: 'docs/*.md', pattern: 'x', expect: 'no-match' }), /docs\/\*\.md/);
|
|
assert.throws(() => evaluateCondition(dir, { path: 'docs/**/*.md', pattern: 'x', expect: 'no-match' }), /docs\/\*\*\/\*\.md/);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('evaluateCheck: open only when ALL conditions hold; null check is not fellable', () => {
|
|
const dir = fixture({
|
|
'r.md': 'run the `### Bridge agent` block\n',
|
|
'fixed.md': 'run the `### Bridge agent` block\n### Bridge agent\n',
|
|
});
|
|
const dangling = (file) => [
|
|
{ path: file, pattern: '`### Bridge agent`', expect: 'match' },
|
|
{ path: file, pattern: '^### Bridge agent', flags: 'm', expect: 'no-match' },
|
|
];
|
|
try {
|
|
assert.equal(evaluateCheck(dir, dangling('r.md')).status, 'open');
|
|
assert.equal(evaluateCheck(dir, dangling('fixed.md')).status, 'closed');
|
|
assert.equal(evaluateCheck(dir, null).status, 'not-fellable');
|
|
const broken = evaluateCheck(dir, dangling('gone.md'));
|
|
assert.equal(broken.status, 'not-fellable');
|
|
assert.match(broken.detail, /gone\.md/);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
// --- spawn instructions (what "referenced by a command" means) -------------
|
|
|
|
test('spawnedNames: a row of a table headed `Agent` is a spawn instruction; other tables are not', () => {
|
|
const md = [
|
|
'Launch the new agents:', '',
|
|
'| Agent | Purpose |', '|-------|---------|', '| `alpha-agent` | does a |', '| `beta-agent` | does b |', '',
|
|
'| Name | Purpose |', '|------|---------|', '| `gamma-agent` | just a list |', '',
|
|
].join('\n');
|
|
const names = spawnedNames(md);
|
|
assert.ok(names.has('alpha-agent') && names.has('beta-agent'));
|
|
assert.ok(!names.has('gamma-agent'), 'a table not headed "Agent" is not a roster');
|
|
});
|
|
|
|
test('spawnedNames: an imperative Launch/Spawn line names the agents it launches', () => {
|
|
const md = [
|
|
'Launch the **alpha-agent** agent:',
|
|
'- Launch `beta-agent` (Agent tool) with the findings.',
|
|
'3. Spawn `voyage:gamma-agent` in the background.',
|
|
'**Launch** the `delta-agent` agent now.',
|
|
'launch `epsilon-agent` too',
|
|
].join('\n');
|
|
const names = spawnedNames(md);
|
|
for (const n of ['alpha-agent', 'beta-agent', 'gamma-agent', 'delta-agent', 'epsilon-agent']) {
|
|
assert.ok(names.has(n), `${n} should count as spawned`);
|
|
}
|
|
});
|
|
|
|
test('spawnedNames: an agent block with a Prompt: line is a spawn instruction; a bare bold name is not', () => {
|
|
const md = [
|
|
'**alpha-agent** — adversarial review of the plan.',
|
|
'Prompt: "Review this plan."',
|
|
'',
|
|
'**beta-agent** — mentioned for context only.',
|
|
'It is not launched here.',
|
|
].join('\n');
|
|
const names = spawnedNames(md);
|
|
assert.ok(names.has('alpha-agent'));
|
|
assert.ok(!names.has('beta-agent'));
|
|
});
|
|
|
|
test('spawnedNames: prose, negation, comments, fences and quotes are never spawn instructions', () => {
|
|
const md = [
|
|
'The alpha-agent is dormant and is never spawned.',
|
|
'Never launch `beta-agent`.',
|
|
'In quick mode we used to launch `gamma-agent`.',
|
|
'<!-- Launch the `delta-agent` agent -->',
|
|
'<!--',
|
|
'Launch the `epsilon-agent` agent',
|
|
'-->',
|
|
'```',
|
|
'Launch the `zeta-agent` agent',
|
|
'| Agent |',
|
|
'|---|',
|
|
'| `eta-agent` |',
|
|
'```',
|
|
'> Launch the `theta-agent` agent',
|
|
].join('\n');
|
|
const names = spawnedNames(md);
|
|
for (const n of ['alpha-agent', 'beta-agent', 'gamma-agent', 'delta-agent', 'epsilon-agent', 'zeta-agent', 'eta-agent', 'theta-agent']) {
|
|
assert.ok(!names.has(n), `${n} must not count as spawned`);
|
|
}
|
|
});
|
|
|
|
// --- the tallies, both directions ------------------------------------------
|
|
|
|
test('defects tally: > 0 while a check holds, 0 once fixed; not-fellable counts as open', () => {
|
|
const defect = {
|
|
id: 'D-X',
|
|
summary: 'removed tool still referenced',
|
|
probe: 'phrase',
|
|
check: [{ path: 'commands/plan.md', pattern: '\\bTeamCreate\\b', expect: 'match' }],
|
|
};
|
|
const unfellable = { id: 'D-Y', summary: 'no runnable check yet', check: null };
|
|
const base = { 'STATE.md': '## Open decisions\n', 'agents/a.md': 'x\n', 'commands/plan.md': 'Launch `a`\n' };
|
|
|
|
const red = fixture({ ...base, 'commands/plan.md': 'Launch `a` and TeamCreate\n' });
|
|
const green = fixture(base);
|
|
try {
|
|
let row = rowById(measure(red, registry({ defects: [defect] })), 'defects');
|
|
assert.equal(row.open, 1);
|
|
assert.equal(row.total, 1);
|
|
row = rowById(measure(green, registry({ defects: [defect] })), 'defects');
|
|
assert.equal(row.open, 0);
|
|
row = rowById(measure(green, registry({ defects: [defect, unfellable] })), 'defects');
|
|
assert.equal(row.open, 1, 'a defect without a runnable check stays open');
|
|
assert.equal(row.items.find((i) => i.id === 'D-Y').status, 'not-fellable');
|
|
} finally { cleanup(red, green); }
|
|
});
|
|
|
|
test('experiments tally: an env-gated opt-in is open until its gate is gone', () => {
|
|
const exp = {
|
|
id: 'E-X',
|
|
summary: 'default-off loop',
|
|
probe: 'phrase',
|
|
check: [{ path: ['commands/*.md', 'lib/**/*.mjs'], pattern: 'LOOP_ENABLED', expect: 'match' }],
|
|
};
|
|
const base = { 'STATE.md': '## Open decisions\n', 'agents/a.md': 'x\n', 'commands/r.md': 'Launch `a`\n' };
|
|
const red = fixture({ ...base, 'lib/util/cap.mjs': 'if (process.env.LOOP_ENABLED) {}\n' });
|
|
const green = fixture({ ...base, 'lib/util/cap.mjs': 'export const x = 1;\n' });
|
|
try {
|
|
assert.equal(rowById(measure(red, registry({ experiments: [exp] })), 'experiments').open, 1);
|
|
assert.equal(rowById(measure(green, registry({ experiments: [exp] })), 'experiments').open, 0);
|
|
} finally { cleanup(red, green); }
|
|
});
|
|
|
|
test('dormant-agents tally: an agent no command spawns counts; reference docs never do', () => {
|
|
const files = {
|
|
'STATE.md': '## Open decisions\n',
|
|
'agents/used-agent.md': '---\nname: used-agent\ndescription: |\n does work\n---\n',
|
|
'agents/idle-agent.md': '---\nname: idle-agent\ndescription: |\n never wired\n---\n',
|
|
'agents/some-orchestrator.md': '---\nname: some-orchestrator\ndescription: Reference document, not a spawnable capability — docs\n---\n',
|
|
'commands/run.md': 'Launch the **used-agent** agent.\n',
|
|
};
|
|
const red = fixture(files);
|
|
const green = fixture({ ...files, 'commands/other.md': 'Spawn `idle-agent` in parallel.\n' });
|
|
try {
|
|
let row = rowById(measure(red, registry()), 'dormant-agents');
|
|
assert.equal(row.open, 1);
|
|
assert.equal(row.total, 2, 'denominator = spawnable agents (reference docs excluded)');
|
|
assert.deepEqual(row.items.filter((i) => i.status === 'open').map((i) => i.id), ['idle-agent']);
|
|
row = rowById(measure(green, registry()), 'dormant-agents');
|
|
assert.equal(row.open, 0);
|
|
} finally { cleanup(red, green); }
|
|
});
|
|
|
|
test('dormant-agents tally: a prose sentence or an HTML comment does not wake a dormant agent', () => {
|
|
const files = {
|
|
'STATE.md': '## Open decisions\n',
|
|
'agents/idle-agent.md': '---\nname: idle-agent\ndescription: |\n never wired\n---\n',
|
|
'commands/plan.md': 'The idle-agent is dormant and is never spawned.\n',
|
|
'commands/end.md': '<!-- idle-agent -->\nThe research-idle-agents phase.\n',
|
|
};
|
|
const dir = fixture(files);
|
|
try {
|
|
assert.equal(rowById(measure(dir, registry()), 'dormant-agents').open, 1);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('dormant-agents tally: an unmeasurable agents dir is n/a (null), never 0', () => {
|
|
const dir = fixture({ 'STATE.md': '## Open decisions\n', 'commands/r.md': 'x\n' });
|
|
try {
|
|
const result = measure(dir, registry());
|
|
const row = rowById(result, 'dormant-agents');
|
|
assert.equal(row.open, null);
|
|
assert.equal(row.total, null);
|
|
assert.match(row.detail, /not measurable/);
|
|
assert.equal(result.green, false);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('decisions tally: counts open markers inside the section only, and says it counts ticks', () => {
|
|
const state = [
|
|
'# STATE',
|
|
'- [ ] outside the section, ignored',
|
|
'## Open decisions',
|
|
'- [ ] first open',
|
|
'- [x] already decided',
|
|
'- [ ] second open',
|
|
'## Next section',
|
|
'- [ ] also ignored',
|
|
'',
|
|
].join('\n');
|
|
const red = fixture({ 'STATE.md': state, 'agents/a.md': 'x\n', 'commands/r.md': 'Launch `a`\n' });
|
|
const green = fixture({
|
|
'STATE.md': '## Open decisions\n- [x] done\n## Next\n',
|
|
'agents/a.md': 'x\n',
|
|
'commands/r.md': 'Launch `a`\n',
|
|
});
|
|
try {
|
|
let row = rowById(measure(red, registry()), 'decisions');
|
|
assert.equal(row.open, 2);
|
|
assert.equal(row.total, 3);
|
|
assert.match(row.detail, /tick/i, 'the output must say a tick is not verified as a decision');
|
|
row = rowById(measure(green, registry()), 'decisions');
|
|
assert.equal(row.open, 0);
|
|
assert.equal(row.total, 1);
|
|
} finally { cleanup(red, green); }
|
|
});
|
|
|
|
test('decisions tally: a missing STATE.md or section is NOT MEASURABLE (null), never 0', () => {
|
|
const noState = fixture({ 'agents/a.md': 'x\n', 'commands/r.md': 'Launch `a`\n' });
|
|
const noSection = fixture({ 'STATE.md': '# STATE\n- [ ] x\n', 'agents/a.md': 'x\n', 'commands/r.md': 'Launch `a`\n' });
|
|
try {
|
|
for (const dir of [noState, noSection]) {
|
|
const result = measure(dir, registry());
|
|
const row = rowById(result, 'decisions');
|
|
assert.equal(row.open, null);
|
|
assert.ok(row.detail.length > 0, 'the reason is reported');
|
|
assert.equal(result.green, false, 'an unmeasurable tally keeps the gate red');
|
|
}
|
|
} finally { cleanup(noState, noSection); }
|
|
});
|
|
|
|
test('decisions tally: an unmarked list item in the section is format drift → NOT MEASURABLE', () => {
|
|
// A STATE written without the marker convention must not read as "0 open".
|
|
for (const item of ['1. **Some decision?** prose', '- plain bullet decision', '* star bullet']) {
|
|
const dir = fixture({
|
|
'STATE.md': `## Open decisions\n- [ ] marked\n${item}\n## Next\n`,
|
|
'agents/a.md': 'x\n',
|
|
'commands/r.md': 'Launch `a`\n',
|
|
});
|
|
try {
|
|
const row = rowById(measure(dir, registry()), 'decisions');
|
|
assert.equal(row.open, null, `unmarked "${item}" must make the tally unmeasurable`);
|
|
assert.match(row.detail, /unmarked/);
|
|
} finally { cleanup(dir); }
|
|
}
|
|
});
|
|
|
|
test('feat-after-freeze tally: not a git repo → n/a (red), never 0', () => {
|
|
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
|
try {
|
|
const result = measure(dir, registry());
|
|
const row = rowById(result, 'feat-after-freeze');
|
|
assert.equal(row.open, null);
|
|
assert.match(row.detail, /not measured/);
|
|
assert.equal(result.green, false);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('feat-after-freeze tally: no freeze tag yet → n/a (red), and the detail names the tag', () => {
|
|
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
|
try {
|
|
gitInit(dir);
|
|
const row = rowById(measure(dir, registry()), 'feat-after-freeze');
|
|
assert.equal(row.open, null);
|
|
assert.match(row.detail, /end-state-freeze/);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('feat-after-freeze tally: counts feat commits after the tag, 0 when there are none', () => {
|
|
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
|
try {
|
|
gitInit(dir);
|
|
gitCommit(dir, 'feat: before the freeze does not count');
|
|
git(dir, 'tag', 'end-state-freeze');
|
|
let row = rowById(measure(dir, registry()), 'feat-after-freeze');
|
|
assert.equal(row.open, 0);
|
|
assert.equal(row.total, 0);
|
|
gitCommit(dir, 'fix(x): a defect fix is allowed');
|
|
gitCommit(dir, 'feature: not a conventional feat');
|
|
row = rowById(measure(dir, registry()), 'feat-after-freeze');
|
|
assert.equal(row.open, 0);
|
|
assert.equal(row.total, 2);
|
|
gitCommit(dir, 'feat(research): a new capability');
|
|
gitCommit(dir, 'feat!: a breaking capability');
|
|
row = rowById(measure(dir, registry()), 'feat-after-freeze');
|
|
assert.equal(row.open, 2);
|
|
assert.equal(row.total, 4);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
// --- registry integrity (the judged party must not own the ledger) ---------
|
|
|
|
const SAMPLE = registry({
|
|
defects: [
|
|
{ id: 'D-01', summary: 'first defect', probe: 'phrase', check: [{ path: 'a.md', pattern: 'bad', expect: 'match' }] },
|
|
{ id: 'D-02', summary: 'second defect', probe: 'phrase', check: [{ path: 'a.md', pattern: 'worse', expect: 'match' }] },
|
|
],
|
|
experiments: [
|
|
{ id: 'E-01', summary: 'first experiment', probe: 'phrase', check: [{ path: 'a.md', pattern: 'FLAG', expect: 'match' }] },
|
|
{ id: 'E-02', summary: 'second experiment', probe: 'phrase', check: [{ path: 'a.md', pattern: 'OPT', expect: 'match' }] },
|
|
],
|
|
});
|
|
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
|
|
test('verifyIntegrity: the frozen registry itself is intact; a new entry may be added', () => {
|
|
const frozen = frozenFor(SAMPLE);
|
|
assert.deepEqual(verifyIntegrity(SAMPLE, frozen), { ok: true, violations: [] });
|
|
const grown = clone(SAMPLE);
|
|
grown.defects.push({ id: 'D-03', summary: 'found later', probe: 'phrase', check: [{ path: 'a.md', pattern: 'new', expect: 'match' }] });
|
|
assert.equal(verifyIntegrity(grown, frozen).ok, true);
|
|
});
|
|
|
|
test('verifyIntegrity: every registry attack is a violation that names what moved', () => {
|
|
const frozen = frozenFor(SAMPLE);
|
|
const attacks = {
|
|
'delete a defect': (r) => { r.defects = r.defects.filter((d) => d.id !== 'D-02'); },
|
|
'delete an experiment': (r) => { r.experiments = r.experiments.filter((e) => e.id !== 'E-01'); },
|
|
'weaken a defect check': (r) => { r.defects[0].check[0].pattern = '^NEVER MATCHES$'; },
|
|
'weaken an experiment check': (r) => { r.experiments[1].check[0].expect = 'no-match'; },
|
|
'drop a condition': (r) => { r.defects[0].check = []; },
|
|
'point spawnSites at agents': (r) => { r.agents = { ...r.agents, spawnSites: 'agents/*.md' }; },
|
|
'change the reference marker': (r) => { r.agents = { ...r.agents, referenceMarker: 'DORMANT' }; },
|
|
'move the decisions section': (r) => { r.decisions = { ...r.decisions, section: '## Elsewhere' }; },
|
|
'change the open marker': (r) => { r.decisions = { ...r.decisions, open: '^NEVER' }; },
|
|
'rename the freeze tag': (r) => { r.freeze = { ...r.freeze, tag: 'some-future-tag' }; },
|
|
'duplicate an id': (r) => { r.defects.push(clone(r.defects[0])); },
|
|
};
|
|
for (const [name, attack] of Object.entries(attacks)) {
|
|
const reg = clone(SAMPLE);
|
|
attack(reg);
|
|
const v = verifyIntegrity(reg, frozen);
|
|
assert.equal(v.ok, false, `${name} must be caught`);
|
|
assert.ok(v.violations.length > 0 && v.violations.every((s) => typeof s === 'string' && s.length > 0), name);
|
|
}
|
|
});
|
|
|
|
test('verifyIntegrity: spawnSites may never include agents/, even if the manifest says so', () => {
|
|
const reg = clone(SAMPLE);
|
|
reg.agents = { ...reg.agents, spawnSites: 'agents/*.md' };
|
|
const v = verifyIntegrity(reg, frozenFor(reg));
|
|
assert.equal(v.ok, false);
|
|
assert.ok(v.violations.some((s) => /agents\//.test(s)));
|
|
});
|
|
|
|
test('checkSignature: key order does not matter; every field of a condition does', () => {
|
|
const a = [{ path: 'a.md', pattern: 'x', flags: 'm', expect: 'match' }];
|
|
const reordered = [{ expect: 'match', flags: 'm', pattern: 'x', path: 'a.md' }];
|
|
assert.equal(checkSignature(a), checkSignature(reordered));
|
|
for (const change of [{ path: 'b.md' }, { pattern: 'y' }, { flags: '' }, { expect: 'no-match' }, { section: '## 1.' }]) {
|
|
assert.notEqual(checkSignature([{ ...a[0], ...change }]), checkSignature(a), JSON.stringify(change));
|
|
}
|
|
assert.match(checkSignature(a), /^[0-9a-f]{64}$/);
|
|
});
|
|
|
|
test('measure: a missing frozen manifest makes integrity n/a and keeps the gate red', () => {
|
|
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
|
try {
|
|
const result = measure(dir, registry());
|
|
assert.equal(result.integrity.ok, null);
|
|
assert.match(result.integrity.detail, /end-state-frozen\.json/);
|
|
assert.equal(result.green, false);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
// --- render ----------------------------------------------------------------
|
|
|
|
test('render: the header counts only tallies that ARE 0, never n/a ones', () => {
|
|
const result = {
|
|
green: false,
|
|
integrity: { ok: true, violations: [], detail: '' },
|
|
rows: [
|
|
{ id: 'defects', label: 'l', source: 's', open: 0, total: 1, target: 0, items: [], detail: '' },
|
|
{ id: 'experiments', label: 'l', source: 's', open: null, total: null, target: 0, items: [], detail: 'not measurable: x' },
|
|
{ id: 'dormant-agents', label: 'l', source: 's', open: 0, total: 3, target: 0, items: [], detail: '' },
|
|
],
|
|
};
|
|
const text = render(result);
|
|
assert.match(text, /RED \(2 of 3 tallies at 0\)/);
|
|
assert.match(text, /\| experiments \| n\/a \| n\/a \|/);
|
|
});
|
|
|
|
test('render: open phrase probes are labelled, closed ids are listed, integrity is shown', () => {
|
|
const result = {
|
|
green: false,
|
|
integrity: { ok: false, violations: ['D-02 removed from the registry'], detail: '' },
|
|
rows: [{
|
|
id: 'defects', label: 'open pipeline defects', source: 's', open: 1, total: 2, target: 0, detail: '',
|
|
items: [
|
|
{ id: 'D-01', summary: 'still open', probe: 'phrase', status: 'open', detail: '' },
|
|
{ id: 'D-06', summary: 'fixed', probe: 'byte', status: 'closed', detail: '' },
|
|
],
|
|
}],
|
|
};
|
|
const text = render(result);
|
|
assert.match(text, /\[open · phrase probe\] D-01/);
|
|
assert.match(text, /closed: D-06/);
|
|
assert.match(text, /registry integrity: VIOLATED/);
|
|
assert.match(text, /D-02 removed from the registry/);
|
|
});
|
|
|
|
// --- CLI -------------------------------------------------------------------
|
|
|
|
function writeGateFiles(dir, reg, frozen = frozenFor(reg)) {
|
|
mkdirSync(join(dir, 'scripts'), { recursive: true });
|
|
writeFileSync(join(dir, 'scripts', 'end-state-registry.json'), JSON.stringify(reg, null, 2));
|
|
mkdirSync(join(dir, 'tests', 'fixtures'), { recursive: true });
|
|
writeFileSync(join(dir, 'tests', 'fixtures', 'end-state-frozen.json'), JSON.stringify(frozen, null, 2));
|
|
}
|
|
|
|
function runGate(dir, extra = []) {
|
|
return spawnSync(process.execPath, [GATE, '--root', dir, ...extra], { encoding: 'utf8' });
|
|
}
|
|
|
|
const ALL_IDS = ['defects', 'experiments', 'dormant-agents', 'decisions', 'feat-after-freeze'];
|
|
|
|
test('CLI: all five tallies at 0 with an intact registry → green, exit 0; --json carries the rows', () => {
|
|
const dir = fixture({
|
|
'STATE.md': '## Open decisions\n- [x] done\n',
|
|
'agents/a.md': '---\nname: a\n---\n',
|
|
'commands/r.md': 'Spawn `a`\n',
|
|
});
|
|
try {
|
|
writeGateFiles(dir, registry());
|
|
gitInit(dir);
|
|
git(dir, 'tag', 'end-state-freeze');
|
|
const r = runGate(dir, ['--json']);
|
|
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
const out = JSON.parse(r.stdout);
|
|
assert.equal(out.green, true);
|
|
assert.equal(out.integrity.ok, true);
|
|
assert.deepEqual(out.rows.map((x) => x.id), ALL_IDS);
|
|
assert.ok(out.rows.every((x) => x.open === 0 && x.target === 0 && typeof x.source === 'string'));
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('CLI: an intact registry with every measurable tally at 0 but no freeze tag → red, exit 1', () => {
|
|
// n/a must never be read as 0 — also when nothing else is wrong.
|
|
const dir = fixture({
|
|
'STATE.md': '## Open decisions\n- [x] done\n',
|
|
'agents/a.md': '---\nname: a\n---\n',
|
|
'commands/r.md': 'Spawn `a`\n',
|
|
});
|
|
try {
|
|
writeGateFiles(dir, registry());
|
|
gitInit(dir);
|
|
const r = runGate(dir, ['--json']);
|
|
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
const out = JSON.parse(r.stdout);
|
|
assert.equal(out.integrity.ok, true);
|
|
assert.equal(out.green, false);
|
|
assert.equal(out.rows.find((x) => x.id === 'feat-after-freeze').open, null);
|
|
assert.ok(out.rows.filter((x) => x.id !== 'feat-after-freeze').every((x) => x.open === 0));
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('CLI: all tallies at 0 but a tampered registry → red, exit 1 (the combined green attack)', () => {
|
|
const dir = fixture({
|
|
'STATE.md': '## Open decisions\n- [x] ticked\n',
|
|
'agents/a.md': '---\nname: a\n---\n',
|
|
'commands/r.md': 'Spawn `a`\n',
|
|
});
|
|
try {
|
|
const honest = registry({ defects: [{ id: 'D-01', summary: 'a real defect', probe: 'phrase', check: [{ path: 'commands/r.md', pattern: 'Spawn', expect: 'match' }] }] });
|
|
const tampered = registry(); // the open defect simply left the ledger
|
|
writeGateFiles(dir, tampered, frozenFor(honest));
|
|
gitInit(dir);
|
|
git(dir, 'tag', 'end-state-freeze');
|
|
const r = runGate(dir);
|
|
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
assert.match(r.stdout, /RED/);
|
|
assert.match(r.stdout, /VIOLATED/);
|
|
assert.match(r.stdout, /D-01/);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('CLI: any tally > 0 → red, exit 1; the table names every row', () => {
|
|
const dir = fixture({
|
|
'STATE.md': '## Open decisions\n- [ ] undecided\n',
|
|
'agents/a.md': '---\nname: a\n---\n',
|
|
'commands/r.md': 'Spawn `a`\n',
|
|
});
|
|
try {
|
|
writeGateFiles(dir, registry());
|
|
const r = runGate(dir);
|
|
assert.equal(r.status, 1, r.stderr);
|
|
for (const label of ALL_IDS) assert.match(r.stdout, new RegExp(label));
|
|
assert.match(r.stdout, /RED/);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
test('CLI: an unreadable registry is a usage error (exit 2), not a verdict', () => {
|
|
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
|
try {
|
|
const r = runGate(dir);
|
|
assert.equal(r.status, 2);
|
|
assert.match(r.stderr, /end-state-registry\.json/);
|
|
} finally { cleanup(dir); }
|
|
});
|
|
|
|
// --- the real repo: denominator pinned, state never pinned -----------------
|
|
|
|
test('the frozen denominator: tests/fixtures/end-state-frozen.json is exactly the literal above', () => {
|
|
const file = loadFrozen(ROOT);
|
|
const { why, ...rest } = file;
|
|
assert.ok(typeof why === 'string' && why.length > 20, 'the manifest says why it is frozen');
|
|
assert.deepEqual(rest, FROZEN);
|
|
});
|
|
|
|
test('the frozen denominator: the real registry is intact against it', () => {
|
|
const v = verifyIntegrity(loadRegistry(ROOT), FROZEN);
|
|
assert.deepEqual(v, { ok: true, violations: [] }, v.violations.join('\n'));
|
|
});
|
|
|
|
test('real registry: every counted entry has an id, a summary, a probe kind and a check that RUNS here', () => {
|
|
const reg = loadRegistry(ROOT);
|
|
const entries = [...reg.defects, ...reg.experiments];
|
|
const ids = new Set();
|
|
for (const e of entries) {
|
|
assert.match(e.id, /^[DE]-\d{2}$/, `bad id ${e.id}`);
|
|
assert.ok(!ids.has(e.id), `duplicate id ${e.id}`);
|
|
ids.add(e.id);
|
|
assert.ok(typeof e.summary === 'string' && e.summary.length > 10, `${e.id} needs a summary`);
|
|
assert.ok(Object.keys(PROBE_NOTES).includes(e.probe), `${e.id} must declare a probe kind the gate can explain`);
|
|
// A check that cannot evaluate against this very repo is a typo, not a finding.
|
|
assert.notEqual(evaluateCheck(ROOT, e.check).status, 'not-fellable', `${e.id} check does not run here`);
|
|
}
|
|
});
|
|
|
|
test('real repo: five rows, open is a count or null, exit code agrees with rows + integrity', () => {
|
|
const r = spawnSync(process.execPath, [GATE, '--json'], { encoding: 'utf8', cwd: ROOT });
|
|
assert.ok(r.status === 0 || r.status === 1, r.stderr);
|
|
const out = JSON.parse(r.stdout);
|
|
assert.deepEqual(out.rows.map((x) => x.id), ALL_IDS);
|
|
for (const row of out.rows) {
|
|
assert.ok(row.open === null || (Number.isInteger(row.open) && row.open >= 0), `${row.id}: ${row.open}`);
|
|
}
|
|
const green = out.rows.every((row) => row.open === 0) && out.integrity.ok === true;
|
|
assert.equal(out.green, green);
|
|
assert.equal(r.status, green ? 0 : 1);
|
|
assert.ok(existsSync(join(ROOT, 'scripts', 'end-state-registry.json')));
|
|
assert.ok(readFileSync(join(ROOT, 'tests', 'fixtures', 'end-state-frozen.json'), 'utf8').length > 0);
|
|
});
|
|
|
|
// --- probe labelling -------------------------------------------------------
|
|
//
|
|
// The gate labelled only `phrase` probes, so every other probe kind rendered
|
|
// unlabelled — indistinguishable from an entry that declared nothing. Once
|
|
// D-03/D-04 became behaviour probes (operator decision 2026-09-18) the reader
|
|
// needs to see WHICH kind of evidence each closed entry rests on.
|
|
|
|
test('PROBE_NOTES: every probe kind the registry may declare carries a note', () => {
|
|
assert.deepEqual(Object.keys(PROBE_NOTES).sort(), ['behaviour', 'byte', 'phrase']);
|
|
for (const [kind, note] of Object.entries(PROBE_NOTES)) {
|
|
assert.ok(note.length > 20, `${kind} needs a note that says what the probe can and cannot prove`);
|
|
}
|
|
});
|
|
|
|
test('render: every probe kind is labelled, open and closed — not just phrase', () => {
|
|
const text = render({
|
|
green: false,
|
|
integrity: { ok: true, violations: [], detail: '' },
|
|
rows: [{
|
|
id: 'defects', label: 'open pipeline defects', source: 's', open: 2, total: 3, target: 0, detail: '',
|
|
items: [
|
|
{ id: 'D-01', summary: 'still open', probe: 'phrase', status: 'open', detail: '' },
|
|
{ id: 'D-03', summary: 'still open', probe: 'behaviour', status: 'open', detail: '' },
|
|
{ id: 'D-06', summary: 'fixed', probe: 'byte', status: 'closed', detail: '' },
|
|
],
|
|
}],
|
|
});
|
|
assert.match(text, /\[open · phrase probe\] D-01/);
|
|
assert.match(text, /\[open · behaviour probe\] D-03/);
|
|
assert.match(text, /closed: D-06 \(byte probe\)/);
|
|
});
|
|
|
|
test('the detail line carries one note per probe kind PRESENT, and none for kinds absent', () => {
|
|
const dir = fixture({
|
|
'a.md': 'bad\n',
|
|
'STATE.md': '## Open decisions\n- [x] done\n',
|
|
'agents/a.md': '---\nname: a\n---\n',
|
|
'commands/r.md': 'Spawn `a`\n',
|
|
});
|
|
try {
|
|
writeGateFiles(dir, registry({
|
|
defects: [
|
|
{ id: 'D-01', summary: 'a phrase defect', probe: 'phrase', check: [{ path: 'a.md', pattern: 'bad', expect: 'match' }] },
|
|
{ id: 'D-02', summary: 'a behaviour defect', probe: 'behaviour', check: [{ path: 'a.md', pattern: 'nope', expect: 'match' }] },
|
|
],
|
|
}));
|
|
gitInit(dir);
|
|
const out = JSON.parse(runGate(dir, ['--json']).stdout);
|
|
const detail = rowById(out, 'defects').detail;
|
|
assert.match(detail, /phrase probe closes on rewording/);
|
|
assert.match(detail, /behaviour probe/);
|
|
assert.ok(!/byte probe/.test(detail), 'a kind no entry declares must not be explained');
|
|
assert.equal(rowById(out, 'experiments').detail, '', 'an empty row explains nothing');
|
|
} finally { cleanup(dir); }
|
|
});
|