refactor(cleanup): simplify pass on the R-a diff
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>
This commit is contained in:
parent
029e83117c
commit
efe981f7bc
18 changed files with 161 additions and 358 deletions
|
|
@ -5,110 +5,58 @@
|
|||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { findSection } from '../../lib/parsers/plan-schema.mjs';
|
||||
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]]';
|
||||
|
||||
function runShim(args) {
|
||||
try {
|
||||
const out = execFileSync(process.execPath, [SHIM, ...args], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
return { code: 0, out };
|
||||
} catch (e) {
|
||||
return { code: e.status ?? 1, out: e.stdout?.toString() ?? '' };
|
||||
}
|
||||
}
|
||||
|
||||
// The exit code is asserted before any JSON.parse, so a crash or a usage
|
||||
// error never reads as a wrong value.
|
||||
function shimGates(args) {
|
||||
const r = runShim(args);
|
||||
assert.equal(r.code, 0, `expected exit 0 for ${JSON.stringify(args)}, got ${r.code}: ${r.out}`);
|
||||
const parsed = JSON.parse(r.out.trim());
|
||||
assert.deepEqual(parsed, { ok: true, gates: parsed.gates });
|
||||
assert.equal(typeof parsed.gates, 'boolean', `gates must be a boolean: ${r.out}`);
|
||||
return parsed.gates;
|
||||
}
|
||||
|
||||
function shimUsage(args) {
|
||||
const r = runShim(args);
|
||||
assert.equal(r.code, 1, `expected exit 1 for ${JSON.stringify(args)}, got ${r.code}: ${r.out}`);
|
||||
const parsed = JSON.parse(r.out.trim());
|
||||
assert.equal(parsed.ok, false);
|
||||
assert.equal(Object.keys(parsed).sort().join(','), 'error,ok');
|
||||
return parsed.error;
|
||||
}
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||
|
||||
// ---- the exported parser ----------------------------------------------------
|
||||
|
||||
test('gatesValue maps true|closed to on, false|open to off, anything else to undefined', async () => {
|
||||
const mod = await import(pathToFileURL(SHIM).href);
|
||||
assert.equal(typeof mod.gatesValue, 'function', 'autonomy-gate.mjs must export gatesValue');
|
||||
for (const v of ['true', 'closed']) assert.equal(mod.gatesValue(v), true, v);
|
||||
for (const v of ['false', 'open']) assert.equal(mod.gatesValue(v), false, v);
|
||||
for (const v of ['TRUE', 'yes', 'adaptive', '', undefined]) assert.equal(mod.gatesValue(v), undefined, String(v));
|
||||
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('the module is the flag parser only: no state machine is exported', async () => {
|
||||
const mod = await import(pathToFileURL(SHIM).href);
|
||||
for (const gone of ['STATES', 'EVENTS', 'transition', 'isTerminal']) {
|
||||
assert.equal(mod[gone], undefined, `autonomy-gate.mjs still exports ${gone}`);
|
||||
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 ----------------------------------------------------------------
|
||||
|
||||
test('CLI: bare --gates → {"ok":true,"gates":true}, exit 0', () => {
|
||||
assert.equal(shimGates(['--gates']), true);
|
||||
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: no flag → {"ok":true,"gates":false}, exit 0', () => {
|
||||
assert.equal(shimGates([]), false);
|
||||
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`);
|
||||
});
|
||||
|
||||
test('CLI: --gates true and --gates closed → gates true', () => {
|
||||
assert.equal(shimGates(['--gates', 'true']), true);
|
||||
assert.equal(shimGates(['--gates', 'closed']), true);
|
||||
});
|
||||
|
||||
test('CLI: --gates false and --gates open → gates false', () => {
|
||||
assert.equal(shimGates(['--gates', 'false']), false);
|
||||
assert.equal(shimGates(['--gates', 'open']), false);
|
||||
});
|
||||
|
||||
test('CLI: the one-token --gates=<value> form is read as --gates <value>', () => {
|
||||
assert.equal(shimGates(['--gates=true']), true);
|
||||
assert.equal(shimGates(['--gates=closed']), true);
|
||||
assert.equal(shimGates(['--gates=false']), false);
|
||||
assert.equal(shimGates(['--gates=open']), false);
|
||||
});
|
||||
|
||||
for (const bad of [['--gates', 'TRUE'], ['--gates', 'yes'], ['--gates', 'adaptive'], ['--gates=maybe'], ['--gates=']]) {
|
||||
test(`CLI: ${bad.join(' ')} → the exact usage error, exit 1`, () => {
|
||||
assert.equal(shimUsage(bad), GATES_USAGE);
|
||||
});
|
||||
}
|
||||
|
||||
for (const old of [
|
||||
['--state', 'idle', '--event', 'start', '--gates', 'true'],
|
||||
['--state', 'idle', '--event', 'start'],
|
||||
['--gates', '--state', 'idle'],
|
||||
['--event', 'start'],
|
||||
]) {
|
||||
test(`CLI: the old state-machine arguments are a usage error (${old.join(' ')}), exit 1`, () => {
|
||||
assert.match(shimUsage(old), /^usage:/);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
|
|
@ -124,14 +72,12 @@ const GATES_RULE = [
|
|||
'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.',
|
||||
];
|
||||
|
||||
const commandText = (cmd) => readFileSync(join(ROOT, 'commands', `${cmd}.md`), 'utf8');
|
||||
|
||||
/** 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 = commandText(cmd).split('\n')
|
||||
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();
|
||||
|
|
@ -155,7 +101,7 @@ function lineGates(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 = commandText(cmd);
|
||||
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`);
|
||||
|
|
@ -178,27 +124,8 @@ test('every command\'s CLI line maps a bare --gates to true and open to false',
|
|||
// flag and pauses on nothing; the docs give the four values and no strictness
|
||||
// policy (fix round 4-5a, #1).
|
||||
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||
const EXEC = 'commands/trekexecute.md';
|
||||
|
||||
/**
|
||||
* The text of a heading's section, up to the next heading of the same or a
|
||||
* higher level outside a fence. The heading must exist.
|
||||
*/
|
||||
function section(text, heading) {
|
||||
const level = heading.match(/^#+/)[0].length;
|
||||
const s = findSection(text, heading.replace(/^#+ /, ''), level);
|
||||
assert.ok(s, `missing ${heading}`);
|
||||
const lines = s.text.split('\n');
|
||||
let fenced = false;
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
|
||||
const m = !fenced && lines[i].match(/^(#+) /);
|
||||
if (m && m[1].length < level) return lines.slice(0, i).join('\n');
|
||||
}
|
||||
return s.text;
|
||||
}
|
||||
|
||||
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.';
|
||||
|
||||
|
|
@ -210,14 +137,14 @@ test('[#1] sibling: commands/trekbrief.md promises no execute pause and no pause
|
|||
});
|
||||
|
||||
test('[#1] sibling: /trekbrief Phase 1 says --gates adds one pause, before the auto chain', () => {
|
||||
const p1 = flat(section(read('commands/trekbrief.md'), '## Phase 1 — Parse mode and validate input'));
|
||||
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(section(read('commands/trekbrief.md'), '## Phase 5 — Auto-orchestration opt-in (if research_topics > 0)'));
|
||||
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');
|
||||
|
|
@ -263,7 +190,7 @@ test('[#1] sibling: docs/command-modes.md gives every --gates row the four value
|
|||
|
||||
test('[#1] sibling: README.md gives the four values, and no strictness table', () => {
|
||||
const text = read('README.md');
|
||||
const sec = flat(section(text, '### Autonomy mode (`--gates`, v3.4.0)'));
|
||||
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\)/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue