refactor(review): simplify pass over E (reuse, simplification, efficiency)
Quality only, no behaviour change (four cleanup reviewers, one implementer; npm test 2480/0 on the working tree): - threat-model.mjs splitBacked(): the one known_outside backing check of plan-gate (record and bare gate), verdict-gate and review-finalize, with one why-text; the engines' security keys derived from ENGINES; isSecurity. - coordinator-contract: "not a security engine" read from ENGINES (a string engine only), not the code-review literal. - review-rounds owns trailingBlock, findingsBlockSha, isMajor, isBlocker; review-answers imports them (review-rounds still imports nothing from it). prepare keeps the archive bytes it read instead of reading the previous archive again. - engine-compare exports rowIn and payloadEngine; finalize uses them. - plan-gate: one accepted_open builder; the stop-rule check hoisted; one top-level lookup for --record --brief. - security-engine: notFullShas(), runCli's usage flag, readScopeList shared with engine-findings; findReport and reviewRoundSummary module-private; two dead defaults gone. - tests: shared plan-gate harness and threat-model brief helpers; the [CNV] table cases call convertReport in-process, one CLI case per exit path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b94b060035
commit
f820081bdc
18 changed files with 332 additions and 373 deletions
81
tests/helpers/plan-gate-harness.mjs
Normal file
81
tests/helpers/plan-gate-harness.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// tests/helpers/plan-gate-harness.mjs
|
||||
// The critic-round and CLI harness of the plan-gate tests
|
||||
// (tests/validators/plan-gate-diff-mode, -stop-rule and -threat-model):
|
||||
// rounds through the real recordVerdict and dedup on buildPlan() text, and
|
||||
// plan-gate.mjs run as a child in its own temp `git init` repo.
|
||||
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, realpathSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { recordVerdict, planHash, operativeText } from '../../lib/validators/plan-gate.mjs';
|
||||
import { dedupFindings } from '../../lib/review/plan-review-dedup.mjs';
|
||||
import { childEnv } from './child-env.mjs';
|
||||
|
||||
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
export const PATHS = [resolve('plan.md')];
|
||||
|
||||
/** The 1-based full-file line holding `s` (findIndex over the whole text). */
|
||||
export function at(text, s) {
|
||||
const n = text.split('\n').findIndex(l => l.includes(s)) + 1;
|
||||
assert.ok(n > 0, `the plan holds ${JSON.stringify(s)}`);
|
||||
return n;
|
||||
}
|
||||
|
||||
/** The base a round diffs against: the operative text the previous round reviewed. */
|
||||
export const baseOf = t => ({ sha256: planHash(t), text: operativeText(t) });
|
||||
|
||||
/** `rec(text, d, opts)`: one round as Phase 9 runs it, recorded at `now`, with the plan path known to the gate. */
|
||||
export function recorder(now) {
|
||||
return (text, d, opts = {}) => {
|
||||
const r = recordVerdict(text, d, { reviewed: planHash(text), planPaths: PATHS, now, ...opts });
|
||||
assert.equal(r.code, 0, r.message);
|
||||
return r;
|
||||
};
|
||||
}
|
||||
|
||||
/** Reviewer payloads through the real dedup. */
|
||||
export function dd({ pc = [], sg = [], pcPrev, sgPrev } = {}) {
|
||||
return dedupFindings([
|
||||
{ agent: 'plan-critic', payload: { findings: pc, ...(pcPrev ? { previous: pcPrev } : {}) } },
|
||||
{ agent: 'scope-guardian', payload: { findings: sg, ...(sgPrev ? { previous: sgPrev } : {}) } },
|
||||
]);
|
||||
}
|
||||
|
||||
/** A reviewer finding on plan.md under `rule_key`. */
|
||||
export const keyedFinding = (severity, line, text, rule_key, extra = {}) => ({ file: 'plan.md', line, rule_key, severity, text, ...extra });
|
||||
|
||||
export const R1_CHANGES = 'Add `run1()`';
|
||||
export const R2_CHANGES = '- **Changes:** Add `run2()` to `src/mod2.mjs`. (new file)';
|
||||
export const R2_EDITED = '- **Changes:** Add `run2()` and its error path to `src/mod2.mjs`. (new file)\n - `run2()` throws on empty input.';
|
||||
|
||||
/** Edit A: Step 2's Changes line becomes two lines (a two-line hunk, nothing above it moves). */
|
||||
export function editA(t) {
|
||||
assert.ok(t.includes(R2_CHANGES));
|
||||
return t.replace(R2_CHANGES, R2_EDITED);
|
||||
}
|
||||
|
||||
// ---- CLI ------------------------------------------------------------------------
|
||||
|
||||
export const GATE = join(ROOT, 'lib', 'validators', 'plan-gate.mjs');
|
||||
export const GIT_CLEAN = { GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined };
|
||||
|
||||
/** plan-gate.mjs as a child in `cwd`, with `input` on stdin. */
|
||||
export function runGate(cwd, args, input) {
|
||||
const r = spawnSync(process.execPath, [GATE, ...args], { cwd, input, encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
|
||||
return { code: r.status, out: r.stdout, err: r.stderr };
|
||||
}
|
||||
|
||||
/** The last non-empty output line, parsed. */
|
||||
export const lastJson = out => JSON.parse(out.split('\n').filter(l => l !== '').at(-1));
|
||||
|
||||
/** A fresh temp dir (real path), `git init`-ed; removed again when the init fails. */
|
||||
export function tempRepo(prefix) {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
|
||||
const g = spawnSync('git', ['init', '-q'], { cwd: dir, encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
|
||||
if (g.status !== 0) rmSync(dir, { recursive: true, force: true });
|
||||
assert.equal(g.status, 0, g.stderr);
|
||||
return dir;
|
||||
}
|
||||
41
tests/helpers/threat-model-brief.mjs
Normal file
41
tests/helpers/threat-model-brief.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// tests/helpers/threat-model-brief.mjs
|
||||
// The brief fixture of the threat-model tests (tests/lib/threat-model and
|
||||
// tests/validators/brief-threat-model): a brief_version 2.2 brief with an
|
||||
// optional `## Threat Model` section before its Success Criteria.
|
||||
|
||||
export const OUTSIDE = "a local user who can already edit the repo's hooks";
|
||||
export const INSIDE = 'a crafted file name in a pull request reaches the shell';
|
||||
export const TM = `## Threat Model\n\n- Inside: ${INSIDE}\n- Outside: ${OUTSIDE}\n`;
|
||||
|
||||
/** The brief with `threat` (a `## Threat Model` section, or '') before its Success Criteria, of frontmatter `type`. */
|
||||
export const briefWith = (threat, type = 'trekbrief') => `---
|
||||
type: ${type}
|
||||
brief_version: "2.2"
|
||||
created: 2026-11-08
|
||||
task: "Add an upload hook"
|
||||
slug: upload-hook
|
||||
project_dir: .claude/projects/2026-11-08-upload-hook/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
framing: new-direction
|
||||
---
|
||||
|
||||
# Task
|
||||
|
||||
## TL;DR
|
||||
|
||||
An upload hook.
|
||||
|
||||
## Intent
|
||||
|
||||
Uploads arrive from strangers.
|
||||
|
||||
## Goal
|
||||
|
||||
A hook that checks each upload.
|
||||
|
||||
${threat}
|
||||
## Success Criteria
|
||||
|
||||
- It checks.
|
||||
`;
|
||||
|
|
@ -276,23 +276,30 @@ test('[CNV] not measured: exit 2, the reason on stderr, nothing on stdout', () =
|
|||
['a record with an unknown confidence', { records: [record({ confidence: 'certain' })] }],
|
||||
['a record with no title', { records: [record({ title: '' })] }],
|
||||
];
|
||||
// The CLI's channels, once: exit 2, nothing on stdout, the reason on stderr.
|
||||
const cli = run(['convert', '--report-dir', reportDir(), '--base', BASE, '--head', HEAD]);
|
||||
assert.equal(cli.status, 2, `CLI: exit 2, got ${cli.status}: ${cli.stdout}${cli.stderr}`);
|
||||
assert.equal(cli.stdout, '', 'CLI: nothing on stdout');
|
||||
assert.match(cli.stderr, /not measured/, `CLI: the reason on stderr says not measured: ${cli.stderr}`);
|
||||
for (const [name, opts] of cases) {
|
||||
const dir = reportDir();
|
||||
if (opts !== null) writeReport(dir, { head: HEAD, base: BASE, ...opts });
|
||||
const r = run(['convert', '--report-dir', dir, '--base', BASE, '--head', HEAD]);
|
||||
assert.equal(r.status, 2, `${name}: exit 2, got ${r.status}: ${r.stdout}${r.stderr}`);
|
||||
assert.equal(r.stdout, '', `${name}: nothing on stdout`);
|
||||
assert.match(r.stderr, /not measured/, `${name}: the reason says not measured: ${r.stderr}`);
|
||||
const r = convertReport({ reportDir: dir, base: BASE, head: HEAD });
|
||||
assert.equal(r.exit, 2, `${name}: exit 2, got ${r.exit}: ${JSON.stringify(r)}`);
|
||||
assert.equal(r.payload, undefined, `${name}: no payload`);
|
||||
assert.match(r.reason, /not measured/, `${name}: the reason says not measured: ${r.reason}`);
|
||||
}
|
||||
});
|
||||
|
||||
/** convert's CLI on one fresh report folder per case: [name, exit, nothing on stdout, the reason says not measured]. */
|
||||
/** The convert result of one report folder: [name, exit, no payload, the reason says not measured]. */
|
||||
const convertRow = (name, r) => [name, r.exit, r.payload === undefined, /not measured/.test(r.reason ?? '')];
|
||||
|
||||
/** convertReport on one fresh report folder per case, as convertRow gives it. */
|
||||
function convertEach(cases) {
|
||||
return cases.map(([name, opts]) => {
|
||||
const dir = reportDir();
|
||||
writeReport(dir, { head: HEAD, base: BASE, ...opts });
|
||||
const r = run(['convert', '--report-dir', dir, '--base', BASE, '--head', HEAD]);
|
||||
return [name, r.status, r.stdout === '', /not measured/.test(r.stderr)];
|
||||
return convertRow(name, convertReport({ reportDir: dir, base: BASE, head: HEAD }));
|
||||
});
|
||||
}
|
||||
const notMeasuredEach = (cases) => cases.map(([name]) => [name, 2, true, true]);
|
||||
|
|
@ -381,10 +388,7 @@ test('[CNV] a symlinked stamp, JSONL or report folder is refused: not measured',
|
|||
symlinkSync(`${what}.real`, join(folder, name));
|
||||
return dir;
|
||||
};
|
||||
const got = kinds.map((what) => {
|
||||
const r = run(['convert', '--report-dir', linked(what), '--base', BASE, '--head', HEAD]);
|
||||
return [what, r.status, r.stdout === '', /not measured/.test(r.stderr)];
|
||||
});
|
||||
const got = kinds.map((what) => convertRow(what, convertReport({ reportDir: linked(what), base: BASE, head: HEAD })));
|
||||
assert.deepEqual(got, kinds.map((what) => [what, 2, true, true]));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { OUTSIDE, INSIDE, TM, briefWith } from '../helpers/threat-model-brief.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
|
@ -19,41 +20,7 @@ const load = () => import(join(ROOT, 'lib', 'review', 'threat-model.mjs'));
|
|||
const approval = () => import(join(ROOT, 'lib', 'validators', 'intent-approval.mjs'));
|
||||
const log = () => import(join(ROOT, 'lib', 'util', 'approval-log.mjs'));
|
||||
|
||||
const OUTSIDE = "a local user who can already edit the repo's hooks";
|
||||
const INSIDE = 'a crafted file name in a pull request reaches the shell';
|
||||
const TM = `## Threat Model\n\n- Inside: ${INSIDE}\n- Outside: ${OUTSIDE}\n`;
|
||||
|
||||
const brief = (threat = TM, type = 'trekbrief') => `---
|
||||
type: ${type}
|
||||
brief_version: "2.2"
|
||||
created: 2026-11-08
|
||||
task: "Add an upload hook"
|
||||
slug: upload-hook
|
||||
project_dir: .claude/projects/2026-11-08-upload-hook/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
framing: new-direction
|
||||
---
|
||||
|
||||
# Task
|
||||
|
||||
## TL;DR
|
||||
|
||||
An upload hook.
|
||||
|
||||
## Intent
|
||||
|
||||
Uploads arrive from strangers.
|
||||
|
||||
## Goal
|
||||
|
||||
A hook that checks each upload.
|
||||
|
||||
${threat}
|
||||
## Success Criteria
|
||||
|
||||
- It checks.
|
||||
`;
|
||||
const brief = (threat = TM, type = 'trekbrief') => briefWith(threat, type);
|
||||
|
||||
const own = (extra = {}) => ({ id: 'f1', rule_key: 'SECURITY_INJECTION', severity: 'blocker', text: 'shell built from a name', threat_model_ref: OUTSIDE, ...extra });
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { strict as assert } from 'node:assert';
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { TM, briefWith } from '../helpers/threat-model-brief.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
|
|
@ -19,43 +20,7 @@ const validator = () => import(join(ROOT, 'lib', 'validators', 'brief-validator.
|
|||
const approval = () => import(join(ROOT, 'lib', 'validators', 'intent-approval.mjs'));
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||
|
||||
const TM = `## Threat Model
|
||||
|
||||
- Inside: a crafted file name in a pull request reaches the shell
|
||||
- Outside: a local user who can already edit the repo's hooks
|
||||
`;
|
||||
|
||||
const brief = (threat = '') => `---
|
||||
type: trekbrief
|
||||
brief_version: "2.2"
|
||||
created: 2026-11-08
|
||||
task: "Add an upload hook"
|
||||
slug: upload-hook
|
||||
project_dir: .claude/projects/2026-11-08-upload-hook/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
framing: new-direction
|
||||
---
|
||||
|
||||
# Task
|
||||
|
||||
## TL;DR
|
||||
|
||||
An upload hook.
|
||||
|
||||
## Intent
|
||||
|
||||
Uploads arrive from strangers.
|
||||
|
||||
## Goal
|
||||
|
||||
A hook that checks each upload.
|
||||
|
||||
${threat}
|
||||
## Success Criteria
|
||||
|
||||
- It checks.
|
||||
`;
|
||||
const brief = (threat = '') => briefWith(threat);
|
||||
|
||||
const codes = (r) => r.errors.map((e) => e.code);
|
||||
const STAMP_AT = new Date('2026-11-08T08:00:00Z');
|
||||
|
|
|
|||
|
|
@ -13,51 +13,28 @@
|
|||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { resolve } from 'node:path';
|
||||
import { gatePlan, recordVerdict, planHash, operativeText, acceptOpen } from '../../lib/validators/plan-gate.mjs';
|
||||
import { gatePlan, planHash, operativeText, acceptOpen } from '../../lib/validators/plan-gate.mjs';
|
||||
import * as diffMod from '../../lib/review/critic-diff.mjs';
|
||||
import { dedupFindings, contentOverlap } from '../../lib/review/plan-review-dedup.mjs';
|
||||
import { contentOverlap } from '../../lib/review/plan-review-dedup.mjs';
|
||||
import { computeFindingId } from '../../lib/parsers/finding-id.mjs';
|
||||
import { buildPlan } from '../helpers/plan-v18-builder.mjs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {
|
||||
mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync, readdirSync, appendFileSync, realpathSync,
|
||||
} from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { childEnv } from '../helpers/child-env.mjs';
|
||||
import {
|
||||
ROOT, at, baseOf, recorder, dd, R2_CHANGES, R2_EDITED, editA, runGate as gate, GIT_CLEAN, tempRepo,
|
||||
} from '../helpers/plan-gate-harness.mjs';
|
||||
|
||||
const NOW = '2026-10-16T00:00:00.000Z';
|
||||
const PLAN = resolve('plan.md');
|
||||
const PATHS = [PLAN];
|
||||
const BOTH = [{ agent: 'plan-critic', present: true }, { agent: 'scope-guardian', present: true }];
|
||||
|
||||
/** The 1-based full-file line holding `s` (findIndex over the whole text). */
|
||||
function at(text, s) {
|
||||
const n = text.split('\n').findIndex(l => l.includes(s)) + 1;
|
||||
assert.ok(n > 0, `the plan holds ${JSON.stringify(s)}`);
|
||||
return n;
|
||||
}
|
||||
|
||||
/** The base a round diffs against: the operative text the previous round reviewed. */
|
||||
const baseOf = t => ({ sha256: planHash(t), text: operativeText(t) });
|
||||
|
||||
/** One round as Phase 9 runs it, with the plan path known to the gate. */
|
||||
function rec(text, d, opts = {}) {
|
||||
const r = recordVerdict(text, d, { reviewed: planHash(text), planPaths: PATHS, now: NOW, ...opts });
|
||||
assert.equal(r.code, 0, r.message);
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Reviewer payloads through the real dedup. */
|
||||
function dd({ pc = [], sg = [], pcPrev, sgPrev } = {}) {
|
||||
return dedupFindings([
|
||||
{ agent: 'plan-critic', payload: { findings: pc, ...(pcPrev ? { previous: pcPrev } : {}) } },
|
||||
{ agent: 'scope-guardian', payload: { findings: sg, ...(sgPrev ? { previous: sgPrev } : {}) } },
|
||||
]);
|
||||
}
|
||||
const rec = recorder(NOW);
|
||||
|
||||
const f = (severity, line, text, extra = {}) => ({ file: 'plan.md', line, rule_key: 'step-quality', severity, text, ...extra });
|
||||
const byText = (v, s) => {
|
||||
|
|
@ -69,15 +46,8 @@ const byId = (v, id) => v.findings.filter(x => x.id === id);
|
|||
|
||||
// ---- the edits ---------------------------------------------------------------
|
||||
|
||||
const R2_CHANGES = '- **Changes:** Add `run2()` to `src/mod2.mjs`. (new file)';
|
||||
const R2_EDITED = '- **Changes:** Add `run2()` and its error path to `src/mod2.mjs`. (new file)\n - `run2()` throws on empty input.';
|
||||
const WHY = 'Why: the brief asks for a parser CLI.';
|
||||
|
||||
/** Edit A: Step 2's Changes line becomes two lines (a two-line hunk, nothing above it moves). */
|
||||
function editA(t) {
|
||||
assert.ok(t.includes(R2_CHANGES));
|
||||
return t.replace(R2_CHANGES, R2_EDITED);
|
||||
}
|
||||
/** Edit B: the Context line is reworded (a one-line hunk high in the plan, nothing moves). */
|
||||
function editB(t) {
|
||||
assert.ok(t.includes(WHY));
|
||||
|
|
@ -702,26 +672,10 @@ test('acceptOpen after round 3 lists only the real majors, never a lowered one',
|
|||
// .git. Expected hashes come from the CLI's own --hash (taken before the
|
||||
// round), expected line numbers from findIndex over the plan text.
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const GATE = join(ROOT, 'lib', 'validators', 'plan-gate.mjs');
|
||||
const GIT_CLEAN = { GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined };
|
||||
|
||||
function gate(cwd, args, input) {
|
||||
const r = spawnSync(process.execPath, [GATE, ...args], { cwd, input, encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
|
||||
return { code: r.status, out: r.stdout, err: r.stderr };
|
||||
}
|
||||
const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
|
||||
|
||||
/** A fresh temp dir (real path), `git init`-ed when `repo` is true. */
|
||||
function tempDir(prefix, repo) {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
|
||||
if (repo) {
|
||||
const g = git(dir, ['init', '-q']);
|
||||
assert.equal(g.status, 0, g.stderr);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
const tempDir = (prefix, repo) => (repo ? tempRepo(prefix) : realpathSync(mkdtempSync(join(tmpdir(), prefix))));
|
||||
|
||||
/** The plan in its own temp git repo; the gate runs with that repo as cwd. */
|
||||
function withRepo(text, fn) {
|
||||
|
|
|
|||
|
|
@ -23,45 +23,18 @@
|
|||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, realpathSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { join } from 'node:path';
|
||||
import { writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||
import {
|
||||
gatePlan, recordVerdict, planHash, operativeText, acceptOpen, extractVerdict, REVIEW_LOG_SECTIONS,
|
||||
gatePlan, recordVerdict, planHash, acceptOpen, extractVerdict, REVIEW_LOG_SECTIONS,
|
||||
} from '../../lib/validators/plan-gate.mjs';
|
||||
import { dedupFindings } from '../../lib/review/plan-review-dedup.mjs';
|
||||
import { buildPlan } from '../helpers/plan-v18-builder.mjs';
|
||||
import { childEnv } from '../helpers/child-env.mjs';
|
||||
import {
|
||||
PATHS, at, baseOf, recorder, dd, keyedFinding as f, R1_CHANGES, editA, runGate as run, lastJson, tempRepo,
|
||||
} from '../helpers/plan-gate-harness.mjs';
|
||||
|
||||
const NOW = '2026-11-08T00:00:00.000Z';
|
||||
const PLAN = resolve('plan.md');
|
||||
const PATHS = [PLAN];
|
||||
|
||||
function at(text, s) {
|
||||
const n = text.split('\n').findIndex(l => l.includes(s)) + 1;
|
||||
assert.ok(n > 0, `the plan holds ${JSON.stringify(s)}`);
|
||||
return n;
|
||||
}
|
||||
const baseOf = t => ({ sha256: planHash(t), text: operativeText(t) });
|
||||
function rec(text, d, opts = {}) {
|
||||
const r = recordVerdict(text, d, { reviewed: planHash(text), planPaths: PATHS, now: NOW, ...opts });
|
||||
assert.equal(r.code, 0, r.message);
|
||||
return r;
|
||||
}
|
||||
function dd({ pc = [], sg = [], pcPrev, sgPrev } = {}) {
|
||||
return dedupFindings([
|
||||
{ agent: 'plan-critic', payload: { findings: pc, ...(pcPrev ? { previous: pcPrev } : {}) } },
|
||||
{ agent: 'scope-guardian', payload: { findings: sg, ...(sgPrev ? { previous: sgPrev } : {}) } },
|
||||
]);
|
||||
}
|
||||
const f = (severity, line, text, rule_key, extra = {}) => ({ file: 'plan.md', line, rule_key, severity, text, ...extra });
|
||||
|
||||
const R2_CHANGES = '- **Changes:** Add `run2()` to `src/mod2.mjs`. (new file)';
|
||||
const R2_EDITED = '- **Changes:** Add `run2()` and its error path to `src/mod2.mjs`. (new file)\n - `run2()` throws on empty input.';
|
||||
const R1_CHANGES = 'Add `run1()`';
|
||||
const editA = t => { assert.ok(t.includes(R2_CHANGES)); return t.replace(R2_CHANGES, R2_EDITED); };
|
||||
const rec = recorder(NOW);
|
||||
|
||||
const B1 = 'The dependency on the parser module is never declared in the manifest list';
|
||||
const B2 = 'A second import chain reaches the CLI without passing through its argument checker';
|
||||
|
|
@ -327,19 +300,9 @@ test('the "Adversarial Pass 2 (v5.1.1 high-effort)" heading is no longer review
|
|||
|
||||
// ---- CLI ------------------------------------------------------------------------------
|
||||
|
||||
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..');
|
||||
const GATE = join(ROOT, 'lib', 'validators', 'plan-gate.mjs');
|
||||
const GIT_CLEAN = { GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined };
|
||||
const run = (cwd, args, input) => {
|
||||
const r = spawnSync(process.execPath, [GATE, ...args], { cwd, input, encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
|
||||
return { code: r.status, out: r.stdout, err: r.stderr };
|
||||
};
|
||||
const lastJson = out => JSON.parse(out.split('\n').filter(l => l !== '').at(-1));
|
||||
|
||||
test('CLI: two rounds with a new dep blocker each -> the gate stops with PLAN_GATE_STOP_RULE and a third --record is refused', () => {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'plan-gate-stop-')));
|
||||
const dir = tempRepo('plan-gate-stop-');
|
||||
try {
|
||||
assert.equal(spawnSync('git', ['init', '-q'], { cwd: dir, env: childEnv(GIT_CLEAN) }).status, 0);
|
||||
const p = join(dir, 'plan.md');
|
||||
writeFileSync(p, buildPlan());
|
||||
const round = (payload, extra = []) => run(dir, ['--record', p, '--reviewed', planHash(readFileSync(p, 'utf-8')), ...extra], JSON.stringify(dd(payload)));
|
||||
|
|
|
|||
|
|
@ -21,22 +21,20 @@
|
|||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, realpathSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gatePlan, recordVerdict, planHash, operativeText, extractVerdict } from '../../lib/validators/plan-gate.mjs';
|
||||
import { join } from 'node:path';
|
||||
import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { gatePlan, recordVerdict, planHash, extractVerdict } from '../../lib/validators/plan-gate.mjs';
|
||||
import { dedupFindings } from '../../lib/review/plan-review-dedup.mjs';
|
||||
import { readThreatModel } from '../../lib/review/threat-model.mjs';
|
||||
import { stampIntentApproval } from '../../lib/validators/intent-approval.mjs';
|
||||
import { appendApproval, normalizeHex, normalizeSubject, resolveRepo } from '../../lib/util/approval-log.mjs';
|
||||
import { buildPlan } from '../helpers/plan-v18-builder.mjs';
|
||||
import { childEnv } from '../helpers/child-env.mjs';
|
||||
import {
|
||||
ROOT, PATHS, at, baseOf, recorder, dd, keyedFinding as f, R1_CHANGES, editA, runGate as run, lastJson, tempRepo,
|
||||
} from '../helpers/plan-gate-harness.mjs';
|
||||
|
||||
const NOW = '2026-11-08T00:00:00.000Z';
|
||||
const PLAN = resolve('plan.md');
|
||||
const PATHS = [PLAN];
|
||||
const rec = recorder(NOW);
|
||||
|
||||
const OUTSIDE = "a local user who can already edit the repo's hooks";
|
||||
const INSIDE = 'a crafted file name in a pull request reaches the shell';
|
||||
|
|
@ -78,30 +76,6 @@ A parser CLI that checks its input.
|
|||
const TM = { usable: true, why: null, threatModel: readThreatModel(BRIEF) };
|
||||
const NOT_USABLE = { usable: false, why: 'the intent approval is not current (BRIEF_INTENT_APPROVAL_STALE)', threatModel: readThreatModel(BRIEF) };
|
||||
|
||||
function at(text, s) {
|
||||
const n = text.split('\n').findIndex(l => l.includes(s)) + 1;
|
||||
assert.ok(n > 0, `the plan holds ${JSON.stringify(s)}`);
|
||||
return n;
|
||||
}
|
||||
const baseOf = t => ({ sha256: planHash(t), text: operativeText(t) });
|
||||
function rec(text, d, opts = {}) {
|
||||
const r = recordVerdict(text, d, { reviewed: planHash(text), planPaths: PATHS, now: NOW, ...opts });
|
||||
assert.equal(r.code, 0, r.message);
|
||||
return r;
|
||||
}
|
||||
function dd({ pc = [], sg = [], pcPrev, sgPrev } = {}) {
|
||||
return dedupFindings([
|
||||
{ agent: 'plan-critic', payload: { findings: pc, ...(pcPrev ? { previous: pcPrev } : {}) } },
|
||||
{ agent: 'scope-guardian', payload: { findings: sg, ...(sgPrev ? { previous: sgPrev } : {}) } },
|
||||
]);
|
||||
}
|
||||
const f = (severity, line, text, rule_key, extra = {}) => ({ file: 'plan.md', line, rule_key, severity, text, ...extra });
|
||||
|
||||
const R1_CHANGES = 'Add `run1()`';
|
||||
const R2_CHANGES = '- **Changes:** Add `run2()` to `src/mod2.mjs`. (new file)';
|
||||
const R2_EDITED = '- **Changes:** Add `run2()` and its error path to `src/mod2.mjs`. (new file)\n - `run2()` throws on empty input.';
|
||||
const editA = t => { assert.ok(t.includes(R2_CHANGES)); return t.replace(R2_CHANGES, R2_EDITED); };
|
||||
|
||||
const S1 = 'A hook file edited by a local user runs before the parser checks its input';
|
||||
const B1 = 'The dependency on the parser module is never declared in the manifest list';
|
||||
|
||||
|
|
@ -267,19 +241,9 @@ test('gate: without --brief the brief is read from the verdict\'s brief_path; no
|
|||
|
||||
// ---- CLI ------------------------------------------------------------------------------
|
||||
|
||||
const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..');
|
||||
const GATE = join(ROOT, 'lib', 'validators', 'plan-gate.mjs');
|
||||
const GIT_CLEAN = { GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_INDEX_FILE: undefined };
|
||||
const run = (cwd, args, input) => {
|
||||
const r = spawnSync(process.execPath, [GATE, ...args], { cwd, input, encoding: 'utf-8', env: childEnv(GIT_CLEAN) });
|
||||
return { code: r.status, out: r.stdout, err: r.stderr };
|
||||
};
|
||||
const lastJson = out => JSON.parse(out.split('\n').filter(l => l !== '').at(-1));
|
||||
|
||||
test('CLI: --record --brief lists a backed finding as a known issue; the gate warns, and STOPs once the Outside line changes', () => {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'plan-gate-tm-')));
|
||||
const dir = tempRepo('plan-gate-tm-');
|
||||
try {
|
||||
assert.equal(spawnSync('git', ['init', '-q'], { cwd: dir, env: childEnv(GIT_CLEAN) }).status, 0);
|
||||
const b = join(dir, 'brief.md');
|
||||
const s = stampIntentApproval(BRIEF, new Date(NOW));
|
||||
assert.equal(s.stamped, true);
|
||||
|
|
@ -315,9 +279,8 @@ test('CLI: --record --brief lists a backed finding as a known issue; the gate wa
|
|||
});
|
||||
|
||||
test('fix 6: brief_path is stored relative to the repo top level, and the gate run from a subdirectory reads the same brief', () => {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'plan-gate-tm-')));
|
||||
const dir = tempRepo('plan-gate-tm-');
|
||||
try {
|
||||
assert.equal(spawnSync('git', ['init', '-q'], { cwd: dir, env: childEnv(GIT_CLEAN) }).status, 0);
|
||||
const b = join(dir, 'brief.md');
|
||||
const s = stampIntentApproval(BRIEF, new Date(NOW));
|
||||
writeFileSync(b, s.text);
|
||||
|
|
@ -344,9 +307,8 @@ test('fix 6: brief_path is stored relative to the repo top level, and the gate r
|
|||
});
|
||||
|
||||
test('CLI: --brief without a value is a usage error; a threat model that is not usable is said in the output', () => {
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'plan-gate-tm-')));
|
||||
const dir = tempRepo('plan-gate-tm-');
|
||||
try {
|
||||
assert.equal(spawnSync('git', ['init', '-q'], { cwd: dir, env: childEnv(GIT_CLEAN) }).status, 0);
|
||||
const p = join(dir, 'plan.md');
|
||||
writeFileSync(p, buildPlan());
|
||||
const t1 = readFileSync(p, 'utf-8');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue