191 lines
5.6 KiB
TypeScript
191 lines
5.6 KiB
TypeScript
/**
|
|
* Tests for headless completion detection.
|
|
*
|
|
* Verifies that isTerminalNotification only matches actual autonomous mode stop
|
|
* signals and does not false-positive on progress notifications that
|
|
* happen to contain words like "complete" or "stopped".
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import { test } from "vitest";
|
|
|
|
// Import the module to get access to the functions via a dynamic import
|
|
// since headless.ts has side-effect-free detection functions but no exports.
|
|
// We'll test by extracting the logic inline.
|
|
|
|
// ─── Extracted detection logic (mirrors headless.ts) ────────────────────────
|
|
|
|
const TERMINAL_PREFIXES = [
|
|
"autonomous mode stopped",
|
|
"assisted mode stopped",
|
|
"autonomous mode paused",
|
|
"assisted mode paused",
|
|
];
|
|
|
|
function isTerminalNotification(event: Record<string, unknown>): boolean {
|
|
if (event.type !== "extension_ui_request" || event.method !== "notify")
|
|
return false;
|
|
const message = String(event.message ?? "").toLowerCase();
|
|
return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
}
|
|
|
|
function isBlockedNotification(event: Record<string, unknown>): boolean {
|
|
if (event.type !== "extension_ui_request" || event.method !== "notify")
|
|
return false;
|
|
const message = String(event.message ?? "").toLowerCase();
|
|
return (
|
|
message.includes("blocked:") ||
|
|
message.startsWith("autonomous mode paused") ||
|
|
message.startsWith("assisted mode paused")
|
|
);
|
|
}
|
|
|
|
function makeNotify(message: string): Record<string, unknown> {
|
|
return { type: "extension_ui_request", method: "notify", message };
|
|
}
|
|
|
|
// ─── isTerminalNotification ─────────────────────────────────────────────────
|
|
|
|
test("detects 'Autonomous mode stopped.' as terminal", () => {
|
|
assert.ok(isTerminalNotification(makeNotify("Autonomous mode stopped.")));
|
|
});
|
|
|
|
test("detects 'Autonomous mode stopped (All milestones complete).' as terminal", () => {
|
|
assert.ok(
|
|
isTerminalNotification(
|
|
makeNotify(
|
|
"Autonomous mode stopped (All milestones complete). Session: $0.42 · 15K tokens · 8 units",
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("detects 'Autonomous mode stopped (Blocked: missing API key).' as terminal", () => {
|
|
assert.ok(
|
|
isTerminalNotification(
|
|
makeNotify("Autonomous mode stopped (Blocked: missing API key)."),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("detects 'Autonomous mode stopped (Milestone M001 complete).' as terminal", () => {
|
|
assert.ok(
|
|
isTerminalNotification(
|
|
makeNotify("Autonomous mode stopped (Milestone M001 complete)."),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("detects 'Assisted mode stopped.' as terminal", () => {
|
|
assert.ok(isTerminalNotification(makeNotify("Assisted mode stopped.")));
|
|
});
|
|
|
|
test("detects provider-error auto pause as terminal", () => {
|
|
assert.ok(
|
|
isTerminalNotification(
|
|
makeNotify(
|
|
"Autonomous mode paused due to provider error: Schema overload: consecutive tool validation failures exceeded cap",
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
// ─── False positives that previously triggered early exit (#879) ────────────
|
|
|
|
test("does NOT match 'All slices are complete — nothing to discuss.'", () => {
|
|
assert.ok(
|
|
!isTerminalNotification(
|
|
makeNotify("All slices are complete — nothing to discuss."),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("does NOT match 'Override(s) resolved — rewrite-docs completed.'", () => {
|
|
assert.ok(
|
|
!isTerminalNotification(
|
|
makeNotify("Override(s) resolved — rewrite-docs completed."),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("does NOT match 'Skipped 5+ completed units. Yielding to UI before continuing.'", () => {
|
|
assert.ok(
|
|
!isTerminalNotification(
|
|
makeNotify(
|
|
"Skipped 5+ completed units. Yielding to UI before continuing.",
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("does NOT match 'Cannot dispatch reassess-roadmap: no completed slices.'", () => {
|
|
assert.ok(
|
|
!isTerminalNotification(
|
|
makeNotify("Cannot dispatch reassess-roadmap: no completed slices."),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("does NOT match 'Committed: feat(S03): complete task implementation'", () => {
|
|
assert.ok(
|
|
!isTerminalNotification(
|
|
makeNotify("Committed: feat(S03): complete task implementation"),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("does NOT match 'Post-hook: applied 3 fix(es).'", () => {
|
|
assert.ok(
|
|
!isTerminalNotification(makeNotify("Post-hook: applied 3 fix(es).")),
|
|
);
|
|
});
|
|
|
|
test("does NOT match non-notify events", () => {
|
|
assert.ok(!isTerminalNotification({ type: "agent_end" }));
|
|
assert.ok(
|
|
!isTerminalNotification({
|
|
type: "extension_ui_request",
|
|
method: "select",
|
|
message: "Autonomous mode stopped.",
|
|
}),
|
|
);
|
|
});
|
|
|
|
// ─── isBlockedNotification ──────────────────────────────────────────────────
|
|
|
|
test("detects blocked notification with 'Blocked:' prefix", () => {
|
|
assert.ok(
|
|
isBlockedNotification(
|
|
makeNotify("Autonomous mode stopped (Blocked: missing API key)."),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("detects inline 'Blocked:' message", () => {
|
|
assert.ok(
|
|
isBlockedNotification(
|
|
makeNotify("Blocked: no active milestone. Fix and run /sf autonomous."),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("detects auto pause as blocked for headless callers", () => {
|
|
assert.ok(
|
|
isBlockedNotification(
|
|
makeNotify(
|
|
"Autonomous mode paused due to provider error: Schema overload: consecutive tool validation failures exceeded cap",
|
|
),
|
|
),
|
|
);
|
|
assert.ok(
|
|
isBlockedNotification(makeNotify("Autonomous mode paused (Escape).")),
|
|
);
|
|
});
|
|
|
|
test("does NOT match 'blocked' without colon (avoids false positives)", () => {
|
|
assert.ok(
|
|
!isBlockedNotification(
|
|
makeNotify("The request was blocked by the firewall"),
|
|
),
|
|
);
|
|
});
|