test(idle-triage): lock in surfaceSelfFeedbackQueueOnIdle contract
Five unit tests covering the bail-time queue notifier landed in
001740680: notify-with-pointer when candidates exist, plural/singular
noun agreement, silent on empty queue, silent on non-forge basePath,
no-throw when downstream notify itself crashes (bail-path safety).
Locks in the contract for the partial-AC1 slice of sf-mp4rxkwb-l4baga
(autonomous loop surfaces the queue at idle) without yet touching the
larger remaining work (real self-feedback-triage unit type with
begin/dispatch/checkpoint/complete).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fa9baf71d5
commit
65c1914b1f
2 changed files with 153 additions and 1 deletions
|
|
@ -138,7 +138,7 @@ import { closeoutAndStop, generateMilestoneReport, maybeFireProductAudit, should
|
|||
* Best-effort: never throws, never blocks. If candidate selection fails or
|
||||
* the basePath isn't a forge repo, this is a no-op.
|
||||
*/
|
||||
function surfaceSelfFeedbackQueueOnIdle(ctx, basePath, exitReason) {
|
||||
export function surfaceSelfFeedbackQueueOnIdle(ctx, basePath, exitReason) {
|
||||
try {
|
||||
const candidates = selectInlineFixCandidates(basePath);
|
||||
if (!Array.isArray(candidates) || candidates.length === 0) return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* phases-pre-dispatch-idle-triage.test.mjs — verify the bail-time
|
||||
* self-feedback queue notifier (sf-mp4rxkwb-l4baga partial).
|
||||
*
|
||||
* Scope: unit-test the surfaceSelfFeedbackQueueOnIdle helper in isolation.
|
||||
* The wider runPreDispatch integration (which calls this helper from the
|
||||
* "no-active-milestone" / "milestone-complete" bail paths) is not unit-
|
||||
* tested here — the helper is the contract and the call sites are
|
||||
* straightforward two-line additions verified by code review + the full
|
||||
* SF suite passing.
|
||||
*/
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { surfaceSelfFeedbackQueueOnIdle } from "../auto/phases-pre-dispatch.js";
|
||||
|
||||
let tempDirs = [];
|
||||
|
||||
function makeForgeProject() {
|
||||
const dir = mkdtempSync(join(tmpdir(), "sf-idle-triage-test-"));
|
||||
tempDirs.push(dir);
|
||||
writeFileSync(
|
||||
join(dir, "package.json"),
|
||||
JSON.stringify({ name: "singularity-forge" }),
|
||||
"utf-8",
|
||||
);
|
||||
mkdirSync(join(dir, ".sf"), { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writeEntries(basePath, entries) {
|
||||
const path = join(basePath, ".sf", "self-feedback.jsonl");
|
||||
writeFileSync(
|
||||
path,
|
||||
`${entries.map((e) => JSON.stringify(e)).join("\n")}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
function entry(overrides = {}) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
id: `sf-test-${Math.random().toString(36).slice(2, 8)}`,
|
||||
ts: new Date().toISOString(),
|
||||
kind: "gap:test",
|
||||
severity: "high",
|
||||
blocking: false,
|
||||
summary: "test entry",
|
||||
source: "runtime",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeFakeCtx() {
|
||||
const calls = [];
|
||||
return {
|
||||
calls,
|
||||
ctx: {
|
||||
ui: {
|
||||
notify: (msg, level) => {
|
||||
calls.push({ msg, level });
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
tempDirs = [];
|
||||
});
|
||||
|
||||
describe("surfaceSelfFeedbackQueueOnIdle", () => {
|
||||
test("notifies when open candidates exist with the canonical pointer to triage", () => {
|
||||
const dir = makeForgeProject();
|
||||
writeEntries(dir, [
|
||||
entry({ id: "sf-a", severity: "high", summary: "first" }),
|
||||
entry({ id: "sf-b", severity: "high", summary: "second" }),
|
||||
]);
|
||||
const { ctx, calls } = makeFakeCtx();
|
||||
surfaceSelfFeedbackQueueOnIdle(ctx, dir, "no-active-milestone");
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].level).toBe("warning");
|
||||
expect(calls[0].msg).toContain("Idle (no-active-milestone)");
|
||||
expect(calls[0].msg).toContain("2 self-feedback entries");
|
||||
expect(calls[0].msg).toContain("sf headless triage --list");
|
||||
expect(calls[0].msg).toContain("sf headless triage --run");
|
||||
});
|
||||
|
||||
test("uses singular 'entry' for n=1", () => {
|
||||
const dir = makeForgeProject();
|
||||
writeEntries(dir, [entry({ id: "sf-only" })]);
|
||||
const { ctx, calls } = makeFakeCtx();
|
||||
surfaceSelfFeedbackQueueOnIdle(ctx, dir, "milestone-complete");
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].msg).toContain("1 self-feedback entry");
|
||||
expect(calls[0].msg).toContain("milestone-complete");
|
||||
});
|
||||
|
||||
test("is silent when no open candidates remain", () => {
|
||||
const dir = makeForgeProject();
|
||||
writeEntries(dir, [
|
||||
entry({
|
||||
id: "sf-resolved",
|
||||
resolvedAt: "2026-05-14T00:00:00Z",
|
||||
resolvedReason: "fixed",
|
||||
resolvedEvidence: { kind: "agent-fix", commitSha: "abc123" },
|
||||
}),
|
||||
]);
|
||||
const { ctx, calls } = makeFakeCtx();
|
||||
surfaceSelfFeedbackQueueOnIdle(ctx, dir, "no-active-milestone");
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("is silent on a non-forge basePath (best-effort no-op)", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "sf-idle-triage-nonforge-"));
|
||||
tempDirs.push(dir);
|
||||
writeFileSync(
|
||||
join(dir, "package.json"),
|
||||
JSON.stringify({ name: "some-other-app" }),
|
||||
"utf-8",
|
||||
);
|
||||
const { ctx, calls } = makeFakeCtx();
|
||||
surfaceSelfFeedbackQueueOnIdle(ctx, dir, "no-active-milestone");
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("does not throw when ctx.ui.notify itself throws", () => {
|
||||
// Bail-path safety: a downstream notification failure must never
|
||||
// take down the loop's clean exit.
|
||||
const dir = makeForgeProject();
|
||||
writeEntries(dir, [entry({ id: "sf-x" })]);
|
||||
const ctx = {
|
||||
ui: {
|
||||
notify: () => {
|
||||
throw new Error("ui crash");
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(() =>
|
||||
surfaceSelfFeedbackQueueOnIdle(ctx, dir, "no-active-milestone"),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue