test(coding-agent): cover reasoning-safe helper budgets

This commit is contained in:
can1357
2026-07-05 13:20:48 +02:00
parent 600191b6f5
commit 007a0108b9
4 changed files with 157 additions and 3 deletions
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import * as path from "node:path";
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import * as ai from "@oh-my-pi/pi-ai";
import { Effort, type Model } from "@oh-my-pi/pi-ai";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import {
@@ -134,6 +135,45 @@ describe("auto thinking classifier helpers", () => {
}
});
it("uses a reasoning-safe online classifier budget when the catalog disables reasoning", async () => {
const baseModel = getBundledModel("anthropic", "claude-sonnet-4-6");
if (!baseModel) throw new Error("Expected bundled Claude Sonnet 4.6 model");
const classifierModel = { ...baseModel, reasoning: false };
const settings = {
get(path: string) {
if (path === "providers.autoThinkingModel") return "online";
return undefined;
},
getModelRole(role: string) {
return role === "smol" ? `${classifierModel.provider}/${classifierModel.id}` : undefined;
},
getStorage() {
return undefined;
},
} as never;
const registry = {
getAvailable: () => [classifierModel],
getApiKey: async () => "test-key",
resolver: () => async () => "test-key",
} as never;
const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({
stopReason: "stop",
content: [{ type: "text", text: "high" }],
} as never);
const effort = await classifyDifficulty("add validation around the retry path", {
settings,
registry,
model: baseModel,
});
const options = completeSimpleMock.mock.calls[0]?.[2] as
| { disableReasoning?: boolean; maxTokens?: number }
| undefined;
expect(effort).toBe(Effort.High);
expect(options).toMatchObject({ disableReasoning: true, maxTokens: 1024 });
});
it("clamps auto effort to model support while never resolving below low", () => {
const model = getBundledModel("anthropic", "claude-sonnet-4-6");
if (!model) throw new Error("Expected bundled Claude Sonnet 4.6 model");
@@ -59,6 +59,28 @@ describe("role thinking helper propagation", () => {
});
});
it("keeps the commit budget reasoning-safe when the catalog disables reasoning", async () => {
const model = { ...getModelOrThrow("claude-sonnet-4-5"), reasoning: false };
const settings = createSettings({
smol: `${model.provider}/${model.id}`,
});
const registry = {
getAvailable: () => [model],
getApiKey: async () => "test-key",
resolver: vi.fn(() => async () => "test-key"),
};
const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({
stopReason: "end_turn",
content: [{ type: "text", text: "fix qwen title budget" }],
} as never);
const message = await generateCommitMessage(`diff --git a/x b/x\n+change\n`, registry as never, settings);
expect(message).toBe("fix qwen title budget");
expect(completeSimpleMock.mock.calls[0]?.[2]).toMatchObject({
maxTokens: 1024,
});
});
it("disables reasoning for title generation even when smol role has thinking", async () => {
const model = getModelOrThrow("claude-sonnet-4-5");
const settings = createSettings({
@@ -1,5 +1,49 @@
import { describe, expect, it } from "bun:test";
import { BlockAccumulator } from "@oh-my-pi/pi-coding-agent/tts/speech-enhancer";
import { afterEach, describe, expect, it, vi } from "bun:test";
import * as ai from "@oh-my-pi/pi-ai";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import { BlockAccumulator, SpeechEnhancer } from "@oh-my-pi/pi-coding-agent/tts/speech-enhancer";
afterEach(() => {
vi.restoreAllMocks();
});
describe("SpeechEnhancer rewriting", () => {
it("uses a reasoning-safe rewrite budget when the catalog disables reasoning", async () => {
const baseModel = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!baseModel) throw new Error("Expected bundled Claude Sonnet 4.5 model");
const model = { ...baseModel, reasoning: false };
const settings = {
get() {
return undefined;
},
getModelRole(role: string) {
return role === "tiny" ? `${model.provider}/${model.id}` : undefined;
},
getStorage() {
return undefined;
},
} as never;
const registry = {
getAvailable: () => [model],
getApiKey: async () => "test-key",
resolver: () => async () => "test-key",
} as never;
const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({
stopReason: "stop",
content: [{ type: "text", text: "Spoken text" }],
} as never);
const rewritten = await new SpeechEnhancer({ settings, registry, sessionId: "session-1" }).rewrite(
"**Spoken text**",
);
const options = completeSimpleMock.mock.calls[0]?.[2] as
| { disableReasoning?: boolean; maxTokens?: number }
| undefined;
expect(rewritten).toBe("Spoken text");
expect(options).toMatchObject({ disableReasoning: true, maxTokens: 1536 });
});
});
/** Push each delta in order; returns blocks completed by pushes plus the flush tail. */
function feed(...deltas: string[]): { blocks: string[]; tail: string | null } {
@@ -1,6 +1,9 @@
import { describe, expect, it } from "bun:test";
import { afterEach, describe, expect, it, vi } from "bun:test";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import * as ai from "@oh-my-pi/pi-ai";
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
import {
classifyUnexpectedStop,
isUnexpectedStopCandidate,
parseUnexpectedStopClassification,
} from "@oh-my-pi/pi-coding-agent/session/unexpected-stop-classifier";
@@ -20,6 +23,10 @@ function makeAssistantMessage(options: {
} as unknown as AssistantMessage;
}
afterEach(() => {
vi.restoreAllMocks();
});
describe("isUnexpectedStopCandidate", () => {
it("returns true for a text-only stop", () => {
const message = makeAssistantMessage({
@@ -71,6 +78,47 @@ describe("isUnexpectedStopCandidate", () => {
});
});
describe("classifyUnexpectedStop", () => {
it("uses a reasoning-safe online classifier budget when the catalog disables reasoning", async () => {
const baseModel = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!baseModel) throw new Error("Expected bundled Claude Sonnet 4.5 model");
const model = { ...baseModel, reasoning: false };
const settings = {
get(path: string) {
if (path === "providers.unexpectedStopModel") return "online";
return undefined;
},
getModelRole(role: string) {
return role === "smol" ? `${model.provider}/${model.id}` : undefined;
},
getStorage() {
return undefined;
},
} as never;
const registry = {
getAvailable: () => [model],
getApiKey: async () => "test-key",
resolver: () => async () => "test-key",
} as never;
const completeSimpleMock = vi.spyOn(ai, "completeSimple").mockResolvedValue({
stopReason: "stop",
content: [{ type: "text", text: "YES" }],
} as never);
const result = await classifyUnexpectedStop("I will continue with the next command.", {
settings,
registry,
sessionId: "session-1",
});
const options = completeSimpleMock.mock.calls[0]?.[2] as
| { disableReasoning?: boolean; maxTokens?: number }
| undefined;
expect(result).toBe(true);
expect(options).toMatchObject({ disableReasoning: true, maxTokens: 1024 });
});
});
describe("parseUnexpectedStopClassification", () => {
it("returns true for YES output", () => {
expect(parseUnexpectedStopClassification("YES")).toBe(true);