Files
stack/extensions/goal/test/progress.test.ts
T

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 });
}
});