engine-compare imports the coordinator's title limit and trim instead of a copy whose reason went with Arm B; the autonomy gate exports one parser and its test runs in-process with two CLI runs; one import scanner in lib-imports; the fenced-section walker shared from tests/helpers/md-section.mjs instead of six copies; plan-gate hash in-process; living-paths scans once; routing drops the orchestrator filter whose files are gone; small leftovers removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
11 KiB
JavaScript
198 lines
11 KiB
JavaScript
// tests/lib/autonomy-gate.test.mjs
|
|
// Cover the --gates flag parser (lib/util/autonomy-gate.mjs): the exported
|
|
// parser function, the CLI's JSON-on-stdout contract, and the one CLI line
|
|
// each of the four commands runs.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { parseGatesArgs } from '../../lib/util/autonomy-gate.mjs';
|
|
import { sectionText } from '../helpers/md-section.mjs';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(HERE, '..', '..');
|
|
const SHIM = join(ROOT, 'lib', 'util', 'autonomy-gate.mjs');
|
|
const GATES_USAGE = 'usage: --gates takes true, false, closed or open';
|
|
const USAGE = 'usage: autonomy-gate.mjs [--gates [true|false|closed|open]]';
|
|
|
|
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
|
|
|
// ---- the exported parser ----------------------------------------------------
|
|
|
|
test('parseGatesArgs: bare --gates and true|closed are on, false|open and no flag are off, in both forms', () => {
|
|
const on = (argv) => assert.deepEqual(parseGatesArgs(argv), { ok: true, gates: true }, JSON.stringify(argv));
|
|
const off = (argv) => assert.deepEqual(parseGatesArgs(argv), { ok: true, gates: false }, JSON.stringify(argv));
|
|
on(['--gates']);
|
|
off([]);
|
|
for (const v of ['true', 'closed']) { on(['--gates', v]); on([`--gates=${v}`]); }
|
|
for (const v of ['false', 'open']) { off(['--gates', v]); off([`--gates=${v}`]); }
|
|
});
|
|
|
|
test('parseGatesArgs: any other value is the exact --gates usage error', () => {
|
|
for (const bad of [['--gates', 'TRUE'], ['--gates', 'yes'], ['--gates', 'adaptive'], ['--gates=maybe'], ['--gates=']]) {
|
|
assert.deepEqual(parseGatesArgs(bad), { ok: false, error: GATES_USAGE }, bad.join(' '));
|
|
}
|
|
});
|
|
|
|
test('parseGatesArgs: any other argument (the old --state/--event too) is the exact usage error', () => {
|
|
assert.deepEqual(parseGatesArgs(['--gates', '--state', 'idle']), { ok: false, error: USAGE });
|
|
});
|
|
|
|
// ---- the CLI ----------------------------------------------------------------
|
|
|
|
const runShim = (args) => spawnSync(process.execPath, [SHIM, ...args], { encoding: 'utf8' });
|
|
|
|
test('CLI: --gates closed prints {"ok":true,"gates":true}, exit 0', () => {
|
|
const r = runShim(['--gates', 'closed']);
|
|
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
assert.equal(r.stdout, '{"ok":true,"gates":true}\n');
|
|
});
|
|
|
|
test('CLI: a bad value prints the usage error as JSON, exit 1', () => {
|
|
const r = runShim(['--gates', 'maybe']);
|
|
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
assert.equal(r.stdout, `${JSON.stringify({ ok: false, error: GATES_USAGE })}\n`);
|
|
});
|
|
|
|
// ---- the four commands' --gates rule and CLI line ----------------------------
|
|
// Each of the four commands holds one rule: the next token is the flag's value
|
|
// only when it is true, false, closed or open; the CLI maps it, run once; a
|
|
// broken CLI falls back to the same mapping in prose and the run goes on.
|
|
|
|
const GATES_COMMANDS = ['trekbrief', 'trekresearch', 'trekplan', 'trekexecute'];
|
|
const GATES_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/util/autonomy-gate.mjs --gates [value]';
|
|
|
|
const GATES_RULE = [
|
|
'`--gates` takes the next token as its value only when it is `true`, `false`, `closed` or `open` (strip both); any other next token is not its value (strip only the flag).',
|
|
'Run the shim once to map the value, bare when there is no value:',
|
|
'`gates: true` sets `gates_mode = true`, `false` sets it false.',
|
|
'On exit 1 or no JSON (a broken shim; the four values cannot cause it), print the error in one line and map in prose instead: bare, `true`, `closed` → true; `false`, `open` → false. The run goes on either way.',
|
|
];
|
|
|
|
/** Whitespace (line breaks too) collapsed to one space. */
|
|
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
|
|
|
/** The command's one CLI line, `[value]` filled in with `value` (left out when ''). */
|
|
function gatesLine(cmd, value) {
|
|
const hits = read(`commands/${cmd}.md`).split('\n')
|
|
.filter((l) => /^\s*node \$\{CLAUDE_PLUGIN_ROOT\}\/lib\/util\/autonomy-gate\.mjs\b/.test(l));
|
|
assert.equal(hits.length, 1, `${cmd}: expected exactly one autonomy-gate.mjs line; found ${hits.length}`);
|
|
const l = hits[0].trim();
|
|
assert.equal(l, GATES_LINE, `${cmd}: the line must read "${GATES_LINE}"`);
|
|
return l.replace(' [value]', value ? ` ${value}` : '');
|
|
}
|
|
|
|
const lineCache = new Map();
|
|
|
|
/** Run a CLI line from the prose with the plugin root filled in; its `gates`. Each distinct line runs once. */
|
|
function lineGates(cmdLine) {
|
|
if (!lineCache.has(cmdLine)) {
|
|
const r = spawnSync('bash', ['-c', cmdLine.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT)], { cwd: ROOT, encoding: 'utf8' });
|
|
assert.equal(r.status, 0, `CLI exited ${r.status}: ${r.stdout}${r.stderr}`);
|
|
const parsed = JSON.parse(r.stdout);
|
|
assert.equal(parsed.ok, true);
|
|
lineCache.set(cmdLine, parsed.gates);
|
|
}
|
|
return lineCache.get(cmdLine);
|
|
}
|
|
|
|
for (const cmd of GATES_COMMANDS) {
|
|
test(`[#1] commands/${cmd}.md holds the shared --gates rule and a CLI line that maps closed to gates true`, () => {
|
|
const text = read(`commands/${cmd}.md`);
|
|
const f = flat(text);
|
|
for (const s of GATES_RULE) assert.ok(f.includes(s), `${cmd}: missing the shared --gates sentence: ${s}`);
|
|
assert.doesNotMatch(text, /--state \w+ --event \w+/, `${cmd}: the state-machine line is still there`);
|
|
assert.doesNotMatch(f, /next_state|auto_running|gates_on/, `${cmd}: still reads a state of the old state machine`);
|
|
assert.equal(lineGates(gatesLine(cmd, 'closed')), true);
|
|
});
|
|
}
|
|
|
|
// `/trekplan --gates --project d` and `/trekexecute --gates plan.md` are a bare
|
|
// --gates (the next token is not a value), so the line runs bare.
|
|
test('every command\'s CLI line maps a bare --gates to true and open to false', () => {
|
|
for (const cmd of GATES_COMMANDS) {
|
|
assert.equal(lineGates(gatesLine(cmd, '')), true, `${cmd}: bare --gates`);
|
|
assert.equal(lineGates(gatesLine(cmd, 'open')), false, `${cmd}: --gates open`);
|
|
}
|
|
});
|
|
|
|
// ---- [#1] siblings: what --gates does, said the same everywhere --------------
|
|
// /trekbrief adds one pause, before its auto chain; /trekexecute accepts the
|
|
// flag and pauses on nothing; the docs give the four values and no strictness
|
|
// policy (fix round 4-5a, #1).
|
|
|
|
const EXEC = 'commands/trekexecute.md';
|
|
|
|
const GATES_ONE_PAUSE = '`gates_mode = true` adds one pause, before the auto chain starts (Phase 5); the chain does not pass '
|
|
+ '`--gates` on to `/trekresearch` or `/trekplan`. Default `gates_mode = false` skips that pause.';
|
|
|
|
test('[#1] sibling: commands/trekbrief.md promises no execute pause and no pause at every wave in the executor', () => {
|
|
const f = flat(read('commands/trekbrief.md'));
|
|
assert.doesNotMatch(f, /every wave in the executor/, 'trekbrief still promises a pause at every wave in the executor');
|
|
assert.doesNotMatch(f, /\(research, plan, execute\)/, 'trekbrief still promises an execute pause');
|
|
assert.ok(f.includes(GATES_ONE_PAUSE), 'trekbrief does not say --gates adds one pause before its auto chain');
|
|
});
|
|
|
|
test('[#1] sibling: /trekbrief Phase 1 says --gates adds one pause, before the auto chain', () => {
|
|
const p1 = flat(sectionText(read('commands/trekbrief.md'), '## Phase 1 — Parse mode and validate input'));
|
|
assert.ok(p1.includes(GATES_ONE_PAUSE), 'Phase 1 does not say --gates adds one pause before the auto chain');
|
|
assert.ok(!p1.includes('every phase boundary in the downstream pipeline'), 'Phase 1 still promises a pause at every downstream phase boundary');
|
|
assert.ok(!p1.includes('runs continuously until the main-merge gate'), 'Phase 1 still says the auto chain runs until the main-merge gate');
|
|
});
|
|
|
|
test('[#1] sibling: /trekbrief Phase 5 says the auto chain ends at the plan, never at the main-merge gate', () => {
|
|
const p5 = flat(sectionText(read('commands/trekbrief.md'), '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)'));
|
|
assert.ok(p5.includes('The chain ends when `/trekplan` has written the plan (Step 6e); it never starts `/trekexecute`.'),
|
|
'Phase 5 does not say the chain ends when /trekplan has written the plan');
|
|
assert.ok(!p5.includes('The chain stops only at the main-merge gate'), 'Phase 5 still says the chain stops only at the main-merge gate');
|
|
});
|
|
|
|
test('[#1] sibling: docs/operations.md, docs/command-modes.md and README.md say /trekexecute accepts the flag and pauses on nothing', () => {
|
|
for (const rel of ['docs/operations.md', 'docs/command-modes.md', 'README.md']) {
|
|
assert.ok(flat(read(rel)).includes('`/trekexecute` accepts the flag and pauses on nothing'), `${rel}: missing "accepts the flag and pauses on nothing"`);
|
|
}
|
|
for (const rel of ['docs/operations.md', 'docs/command-modes.md', 'README.md', EXEC]) {
|
|
const f = flat(read(rel));
|
|
for (const old of ['records `gates_mode`', 'Record gates_mode', 'records the setting']) {
|
|
assert.ok(!f.includes(old), `${rel}: still says "${old}"`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('[#1] sibling: commands/trekexecute.md: `gates_mode` changes nothing in `/trekexecute`, and its one stop is the merge into main', () => {
|
|
const f = flat(read('commands/trekexecute.md'));
|
|
assert.doesNotMatch(f, /consumed by the autonomy-gate state machine/);
|
|
assert.ok(f.includes('`gates_mode` changes nothing in `/trekexecute`: no phase of it pauses on it. The one stop is the merge into main, which always asks (Phase 8).'),
|
|
'trekexecute does not say gates_mode changes nothing there and the one stop is the merge into main');
|
|
});
|
|
|
|
test('[#1] sibling: docs/architecture.md does not call --gates open the executor\'s low effort', () => {
|
|
const text = read('docs/architecture.md');
|
|
assert.doesNotMatch(text, /--gates open/);
|
|
assert.match(text, /\(`\/trekexecute` low effort = one session at a time\)/);
|
|
});
|
|
|
|
test('[#1] sibling: docs/command-modes.md gives every --gates row the four values and no strictness policy', () => {
|
|
const text = read('docs/command-modes.md');
|
|
assert.doesNotMatch(text, /--gates \{true\\\|false\}/);
|
|
assert.doesNotMatch(text, /\{open\\?\|closed\\?\|adaptive\}/);
|
|
assert.doesNotMatch(text, /Policy \(`gates_mode`\)/, 'a row still points at a gates_mode strictness policy');
|
|
const rows = text.split('\n').filter((l) => l.startsWith('| `--gates'));
|
|
assert.equal(rows.length, 4, 'one --gates row per command');
|
|
for (const r of rows) {
|
|
assert.ok(r.startsWith('| `--gates [true\\|false\\|closed\\|open]` |'), `row does not give the four values: ${r}`);
|
|
assert.match(r, /`## Autonomy mode` in `docs\/operations\.md`/);
|
|
}
|
|
});
|
|
|
|
test('[#1] sibling: README.md gives the four values, and no strictness table', () => {
|
|
const text = read('README.md');
|
|
const sec = flat(sectionText(text, '### Autonomy mode (`--gates`, v3.4.0)'));
|
|
assert.doesNotMatch(text, /\{open\|closed\|adaptive\}/);
|
|
assert.doesNotMatch(sec, /\| Value \| Behavior \|/, 'the strictness table is still there');
|
|
assert.doesNotMatch(sec, /`adaptive` \(default\)/);
|
|
assert.match(sec, /accept `--gates \[true\|false\|closed\|open\]`/);
|
|
});
|