test(coding-agent): cover reasoning-safe helper budgets
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user