Row 139 round 2, Dewey's candidate a4c5657f (14 paths) on5661bd233. Filbert approved round 2 (comment 28786) after round 1 changes. Dewey's gate: 14 runs rc 0. Mutants m1-m9 killed. Filbert ran 7 files singly: 0 fail, 0 skipped. The broker's GATED and the business's GATED_ONLY are now the hard waits. A cited gated decision goes to the domain arbiter unless it is a hard wait, which always goes to the human. Under Jason's ruling Q169, a role.revoke of the assistant is a hard wait. Its end gets past END_PROTECTED only for the chief of staff, or when the cited decision was resolved by the human through the CLI. PRD version 1.6 records Q169. Mos approved the 1.6 text, including the REQ-ROLE-2 sentence on the chief's end and revoke. scripts/test-land-work.sh adds the not-human cases for arbiter-resolved merges. land-work's read.mjs is unchanged. Row 137 lands second, so it carries the gated-merge refusal and the host land op's arbiter acceptance. Making an end or revoke stop an assistant run, and configuring the assistant, are row 155 (#1649). Sage's landing gate ran on26b78d483plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 19:23:57Z to 19:28:28Z, network sage-r139_default removed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
82 lines
4.4 KiB
JavaScript
82 lines
4.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { loadRequirements, checkRequirements, RequirementsError, REQUIREMENTS_FILE } from '../src/requirements.mjs';
|
|
// Row 90 (#1584): requirements.json against the approved PRD it names. The PRD's **REQ-...**
|
|
// bullets are the source; the list must hold the same ids in the same order, and the PRD's
|
|
// status line must give the list's version.
|
|
const list = loadRequirements();
|
|
const raw = () => JSON.parse(readFileSync(REQUIREMENTS_FILE, 'utf8'));
|
|
const REPO = fileURLToPath(new URL('../../../', import.meta.url));
|
|
const listError = (message) => (e) => e instanceof RequirementsError && (message === undefined || e.message === message);
|
|
|
|
test("the list holds the PRD's requirement ids, in its order, at its version", () => {
|
|
const prd = readFileSync(join(REPO, list.prd), 'utf8');
|
|
const ids = [...prd.matchAll(/^- \*\*(REQ-[A-Z]+-[0-9]+)\.\*\*/gm)].map((m) => m[1]);
|
|
// Any other mention of a bold id would be a bullet this pattern missed.
|
|
assert.equal(ids.length, [...prd.matchAll(/\*\*REQ-/g)].length);
|
|
assert.equal(ids.length, 58);
|
|
assert.deepEqual(list.requirements.map((r) => r.id), ids);
|
|
assert.match(prd, new RegExp(`^- Status: approved, version ${list.version.replace('.', '\\.')}\\.`, 'm'));
|
|
assert.deepEqual([list.prd, list.version], ['docs/prd/mosaic-stack.md', '1.6']);
|
|
});
|
|
|
|
test('the committed list loads frozen', () => {
|
|
assert.ok(Object.isFrozen(list) && Object.isFrozen(list.requirements) && Object.isFrozen(list.requirements[0]));
|
|
assert.deepEqual(checkRequirements(raw()), raw());
|
|
assert.deepEqual(loadRequirements(), list);
|
|
});
|
|
|
|
test('the schema refuses any other shape', () => {
|
|
const bad = {
|
|
'an unknown top key': (l) => (l.source = 'x'),
|
|
'no prd': (l) => delete l.prd,
|
|
'a prd outside docs/prd': (l) => (l.prd = 'docs/plans/x.md'),
|
|
'a prd that is not markdown': (l) => (l.prd = 'docs/prd/mosaic-stack.txt'),
|
|
'a prd with a parent step': (l) => (l.prd = 'docs/prd/../x.md'),
|
|
'no version': (l) => delete l.version,
|
|
'a numeric version': (l) => (l.version = 1.5),
|
|
'a version with no minor': (l) => (l.version = '1'),
|
|
'a version 0': (l) => (l.version = '0.4'),
|
|
'no requirements': (l) => delete l.requirements,
|
|
'an empty list': (l) => (l.requirements = []),
|
|
'requirements not a list': (l) => (l.requirements = {}),
|
|
'an entry not an object': (l) => (l.requirements[0] = 'REQ-ROLE-1'),
|
|
'an unknown entry key': (l) => (l.requirements[0].note = 'x'),
|
|
'no id': (l) => delete l.requirements[0].id,
|
|
'no title': (l) => delete l.requirements[0].title,
|
|
'an id in lower case': (l) => (l.requirements[0].id = 'REQ-role-1'),
|
|
'an id numbered 0': (l) => (l.requirements[0].id = 'REQ-ROLE-0'),
|
|
'an id with a suffix': (l) => (l.requirements[0].id = 'REQ-ROLE-1a'),
|
|
'an id twice': (l) => (l.requirements[1].id = l.requirements[0].id),
|
|
'a blank title': (l) => (l.requirements[0].title = ' '),
|
|
'a title over 120 characters': (l) => (l.requirements[0].title = 'x'.repeat(121)),
|
|
'a title of two lines': (l) => (l.requirements[0].title = 'a\nb'),
|
|
'a title that is not a string': (l) => (l.requirements[0].title = 1),
|
|
};
|
|
for (const x of [[], null, 'list']) assert.throws(() => checkRequirements(x), listError('list: not an object'));
|
|
for (const [what, edit] of Object.entries(bad)) {
|
|
const l = raw();
|
|
edit(l);
|
|
assert.throws(() => checkRequirements(l), listError(), what);
|
|
}
|
|
const ok = raw();
|
|
ok.requirements[0].title = 'x'.repeat(120);
|
|
assert.equal(checkRequirements(ok), ok);
|
|
const dir = mkdtempSync(join(tmpdir(), 'requirements-'));
|
|
try {
|
|
writeFileSync(join(dir, 'cut.json'), '{"prd":');
|
|
assert.throws(() => loadRequirements(join(dir, 'cut.json')), listError('requirements.json: not JSON'));
|
|
assert.throws(() => loadRequirements(join(dir, 'absent.json')), listError('requirements.json: ENOENT'));
|
|
const twice = raw();
|
|
twice.requirements.push(structuredClone(twice.requirements[0]));
|
|
writeFileSync(join(dir, 'twice.json'), JSON.stringify(twice));
|
|
assert.throws(() => loadRequirements(join(dir, 'twice.json')), listError('requirements[58].id: REQ-ROLE-1 twice'));
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|