Files
oh-my-pi/packages/coding-agent/test/unexpected-stop-classifier.test.ts
T
can1357 e805b34ecf feat(coding-agent): added unexpected-stop detection with automatic retries and a 3-retry cap
- Added `features.unexpectedStopDetection` and `unexpectedStopModel` settings for opt-in behavior.
- Added assistant-stop handling to classify stop reasons and resume generation with retry prompts.
- Added unexpected-stop classifier logic with candidate checks, model fallback, and YES/NO parsing.
- Added retry tracking that caps auto-continues at three attempts and logs a warning when exceeded.
2026-06-15 09:38:25 +02:00

93 lines
3.0 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import {
isUnexpectedStopCandidate,
parseUnexpectedStopClassification,
} from "@oh-my-pi/pi-coding-agent/session/unexpected-stop-classifier";
function makeAssistantMessage(options: {
stopReason: AssistantMessage["stopReason"];
content: AssistantMessage["content"];
}): AssistantMessage {
return {
role: "assistant",
provider: "mock",
model: "mock/mock",
api: "mock" as unknown as AssistantMessage["api"],
content: options.content,
stopReason: options.stopReason,
timestamp: Date.now(),
} as unknown as AssistantMessage;
}
describe("isUnexpectedStopCandidate", () => {
it("returns true for a text-only stop", () => {
const message = makeAssistantMessage({
stopReason: "stop",
content: [{ type: "text", text: "I should do the same for the JS eval worker." }],
});
expect(isUnexpectedStopCandidate(message)).toBe(true);
});
it("returns false when stopReason is not stop", () => {
const length = makeAssistantMessage({
stopReason: "length",
content: [{ type: "text", text: "I should continue." }],
});
expect(isUnexpectedStopCandidate(length)).toBe(false);
const aborted = makeAssistantMessage({
stopReason: "aborted",
content: [{ type: "text", text: "I should continue." }],
});
expect(isUnexpectedStopCandidate(aborted)).toBe(false);
});
it("returns false when the message contains a toolCall", () => {
const message = makeAssistantMessage({
stopReason: "stop",
content: [
{ type: "text", text: "I will run the tests now." },
{ type: "toolCall", id: "call-1", name: "bash", arguments: {} },
],
});
expect(isUnexpectedStopCandidate(message)).toBe(false);
});
it("returns false when the text is only whitespace", () => {
const message = makeAssistantMessage({
stopReason: "stop",
content: [{ type: "text", text: " \n\t " }],
});
expect(isUnexpectedStopCandidate(message)).toBe(false);
});
it("returns false for an empty stop", () => {
const message = makeAssistantMessage({
stopReason: "stop",
content: [],
});
expect(isUnexpectedStopCandidate(message)).toBe(false);
});
});
describe("parseUnexpectedStopClassification", () => {
it("returns true for YES output", () => {
expect(parseUnexpectedStopClassification("YES")).toBe(true);
expect(parseUnexpectedStopClassification("yes")).toBe(true);
expect(parseUnexpectedStopClassification(" Yes, this is unexpected ")).toBe(true);
});
it("returns false for NO output", () => {
expect(parseUnexpectedStopClassification("NO")).toBe(false);
expect(parseUnexpectedStopClassification("no")).toBe(false);
expect(parseUnexpectedStopClassification("No, the task is complete.")).toBe(false);
});
it("returns undefined for unparseable output", () => {
expect(parseUnexpectedStopClassification("maybe")).toBeUndefined();
expect(parseUnexpectedStopClassification("")).toBeUndefined();
expect(parseUnexpectedStopClassification("I don't know")).toBeUndefined();
});
});