181 lines
7.3 KiB
TypeScript
181 lines
7.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { test } from "node:test";
|
|
|
|
import {
|
|
DEFAULT_MAX_NO_PROGRESS_REPORTS,
|
|
initialState,
|
|
recordCheckInjected,
|
|
recordInProgressReport,
|
|
recordWorkEvent,
|
|
setGoal,
|
|
type ProgressDetails,
|
|
} from "../lib/state.ts";
|
|
|
|
const measurement: ProgressDetails = {
|
|
kind: "measurement",
|
|
gate: "PR review",
|
|
owner: "rev-code-01",
|
|
lastMeasurement: "PR head abc123 remains unreviewed at 2026-08-31T15:00:00Z",
|
|
nextAction: "dispatch exact-head review",
|
|
};
|
|
|
|
function reportOnlyCycle(state: ReturnType<typeof setGoal>) {
|
|
const checked = recordCheckInjected(state);
|
|
return recordInProgressReport(checked, { evidence: "", progress: undefined }).state;
|
|
}
|
|
|
|
test("exact report-only loop pauses within the no-progress bound", () => {
|
|
let state = setGoal(initialState(), "ship", 25);
|
|
for (let n = 1; n <= DEFAULT_MAX_NO_PROGRESS_REPORTS; n++) {
|
|
state = reportOnlyCycle(state);
|
|
assert.equal(state.noProgressReports, n);
|
|
}
|
|
assert.equal(state.status, "paused");
|
|
assert.match(state.pausedReason ?? "", /no substantive progress/);
|
|
});
|
|
|
|
test("empty, malformed, and duplicate evidence never reset no-progress", () => {
|
|
let state = setGoal(initialState(), "ship", 25);
|
|
state = recordInProgressReport(recordCheckInjected(state), { evidence: "", progress: undefined }).state;
|
|
assert.equal(state.noProgressReports, 1);
|
|
|
|
state = recordInProgressReport(recordCheckInjected(state), {
|
|
evidence: "measured",
|
|
progress: { ...measurement, owner: "" },
|
|
}).state;
|
|
assert.equal(state.noProgressReports, 2);
|
|
|
|
state = recordWorkEvent(state, "read", "2026-08-31T15:00:00Z");
|
|
const accepted = recordInProgressReport(recordCheckInjected(state), {
|
|
evidence: "measured live PR state",
|
|
progress: measurement,
|
|
});
|
|
assert.equal(accepted.classification, "progress");
|
|
state = accepted.state;
|
|
assert.equal(state.noProgressReports, 0);
|
|
|
|
state = recordWorkEvent(state, "read", "2026-08-31T15:01:00Z");
|
|
const duplicate = recordInProgressReport(recordCheckInjected(state), {
|
|
evidence: "measured live PR state",
|
|
progress: measurement,
|
|
});
|
|
assert.equal(duplicate.classification, "no_progress");
|
|
assert.equal(duplicate.state.noProgressReports, 1);
|
|
});
|
|
|
|
test("measurement, action, and delegation require an observed successful work event", () => {
|
|
for (const kind of ["measurement", "action", "delegation"] as const) {
|
|
const details = { ...measurement, kind };
|
|
let state = setGoal(initialState(), kind, 25);
|
|
const unsupported = recordInProgressReport(recordCheckInjected(state), {
|
|
evidence: `${kind} claim`,
|
|
progress: details,
|
|
});
|
|
assert.equal(unsupported.classification, "no_progress");
|
|
|
|
state = recordWorkEvent(unsupported.state, kind === "action" ? "edit" : "bash", "2026-08-31T15:00:00Z");
|
|
const accepted = recordInProgressReport(recordCheckInjected(state), {
|
|
evidence: `${kind} completed with a new result`,
|
|
progress: { ...details, lastMeasurement: `${details.lastMeasurement} ${kind}` },
|
|
});
|
|
assert.equal(accepted.classification, "progress");
|
|
assert.equal(accepted.state.noProgressReports, 0);
|
|
assert.equal(accepted.state.checks, 0);
|
|
}
|
|
});
|
|
|
|
test("explicit non-tool work requires a concrete artifact", () => {
|
|
let state = setGoal(initialState(), "draft", 25);
|
|
const missing = recordInProgressReport(recordCheckInjected(state), {
|
|
evidence: "drafted reasoning",
|
|
progress: { ...measurement, kind: "non_tool" },
|
|
});
|
|
assert.equal(missing.classification, "no_progress");
|
|
|
|
const accepted = recordInProgressReport(recordCheckInjected(missing.state), {
|
|
evidence: "completed architecture decision",
|
|
progress: { ...measurement, kind: "non_tool", artifact: "work/decision-D12.md" },
|
|
});
|
|
assert.equal(accepted.classification, "progress");
|
|
assert.equal(accepted.state.noProgressReports, 0);
|
|
});
|
|
|
|
test("a legitimate external wait remains active without false-positive pause", () => {
|
|
let state = setGoal(initialState(), "await review", 25);
|
|
state = recordInProgressReport(recordCheckInjected(state), { evidence: "", progress: undefined }).state;
|
|
state = recordInProgressReport(recordCheckInjected(state), { evidence: "", progress: undefined }).state;
|
|
assert.equal(state.noProgressReports, 2);
|
|
|
|
const waiting: ProgressDetails = {
|
|
kind: "wait",
|
|
gate: "independent review",
|
|
owner: "rev-code-01",
|
|
lastMeasurement: "review dispatched at 2026-08-31T15:00:00Z",
|
|
nextAction: "merge only after PASS",
|
|
watchId: "ng8-review",
|
|
nextCheck: "review artifact exists",
|
|
};
|
|
for (let n = 0; n < 10; n++) {
|
|
const outcome = recordInProgressReport(recordCheckInjected(state), {
|
|
evidence: "approved watch remains active",
|
|
progress: waiting,
|
|
});
|
|
assert.equal(outcome.classification, "waiting");
|
|
state = outcome.state;
|
|
assert.equal(state.status, "active");
|
|
assert.equal(state.noProgressReports, 2);
|
|
assert.equal(state.checks, 0);
|
|
}
|
|
});
|
|
|
|
test("RED CONTROL: neutralizing no-progress counting restores the infinite loop", async () => {
|
|
const source = readFileSync(new URL("../lib/state.ts", import.meta.url), "utf8");
|
|
const sabotaged = source.replace(
|
|
"const nextNoProgress = state.noProgressReports + 1; // NG8_COUNT_GATE",
|
|
"const nextNoProgress = state.noProgressReports; // NG8_COUNT_GATE neutralized",
|
|
);
|
|
assert.notEqual(sabotaged, source, "count sabotage must change source");
|
|
const dir = mkdtempSync(join(tmpdir(), "goal-ng8-count-red-"));
|
|
try {
|
|
const path = join(dir, "state.ts");
|
|
writeFileSync(path, sabotaged);
|
|
const red = await import(`${pathToFileURL(path).href}?red=${Date.now()}`);
|
|
let state = red.setGoal(red.initialState(), "ship", 25);
|
|
for (let n = 0; n < DEFAULT_MAX_NO_PROGRESS_REPORTS + 2; n++) {
|
|
state = red.recordCheckInjected(state);
|
|
state = red.recordInProgressReport(state, { evidence: "", progress: undefined }).state;
|
|
}
|
|
assert.equal(state.status, "active", "RED: report-only loop survives when counting is neutralized");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("RED CONTROL: neutralizing pause enforcement leaves the counted loop active", async () => {
|
|
const source = readFileSync(new URL("../lib/state.ts", import.meta.url), "utf8");
|
|
const sabotaged = source.replace(
|
|
"if (nextNoProgress >= state.maxNoProgressReports) { // NG8_PAUSE_GATE",
|
|
"if (false && nextNoProgress >= state.maxNoProgressReports) { // NG8_PAUSE_GATE neutralized",
|
|
);
|
|
assert.notEqual(sabotaged, source, "pause sabotage must change source");
|
|
const dir = mkdtempSync(join(tmpdir(), "goal-ng8-pause-red-"));
|
|
try {
|
|
const path = join(dir, "state.ts");
|
|
writeFileSync(path, sabotaged);
|
|
const red = await import(`${pathToFileURL(path).href}?red=${Date.now()}`);
|
|
let state = red.setGoal(red.initialState(), "ship", 25);
|
|
for (let n = 0; n < DEFAULT_MAX_NO_PROGRESS_REPORTS; n++) {
|
|
state = red.recordCheckInjected(state);
|
|
state = red.recordInProgressReport(state, { evidence: "", progress: undefined }).state;
|
|
}
|
|
assert.equal(state.noProgressReports, DEFAULT_MAX_NO_PROGRESS_REPORTS);
|
|
assert.equal(state.status, "active", "RED: counted loop survives when pause gate is neutralized");
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|