chore: remove garbage tests

This commit is contained in:
can1357
2026-05-12 03:47:43 +02:00
parent 9b536bd6e9
commit 975941aba4
87 changed files with 33 additions and 4300 deletions
+1 -155
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "bun:test";
import { Agent, type AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import { getBundledModel, type SimpleStreamOptions, type ThinkingBudgets } from "@oh-my-pi/pi-ai";
import { getBundledModel, type SimpleStreamOptions } from "@oh-my-pi/pi-ai";
import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream";
import { Type } from "@sinclair/typebox";
import { createAssistantMessage, pushAlphaThenDoneEvent } from "./helpers";
@@ -8,96 +8,6 @@ import { createAssistantMessage, pushAlphaThenDoneEvent } from "./helpers";
class MockAssistantStream extends AssistantMessageEventStream {}
describe("Agent", () => {
it("should create an agent instance with default state", () => {
const agent = new Agent();
expect(agent.state).toBeDefined();
expect(agent.state.systemPrompt).toEqual([]);
expect(agent.state.model).toBeDefined();
expect(agent.state.thinkingLevel).toBeUndefined();
expect(agent.state.tools).toEqual([]);
expect(agent.state.messages).toEqual([]);
expect(agent.state.isStreaming).toBe(false);
expect(agent.state.streamMessage).toBe(null);
expect(agent.state.pendingToolCalls).toEqual(new Set());
expect(agent.state.error).toBeUndefined();
});
it("should create an agent instance with custom initial state", () => {
const customModel = getBundledModel("openai", "gpt-4o-mini");
const agent = new Agent({
initialState: {
systemPrompt: ["You are a helpful assistant."],
model: customModel,
thinkingLevel: ThinkingLevel.Low,
},
});
expect(agent.state.systemPrompt).toEqual(["You are a helpful assistant."]);
expect(agent.state.model).toBe(customModel);
expect(agent.state.thinkingLevel).toBe(ThinkingLevel.Low);
});
it("should subscribe to events", () => {
const agent = new Agent();
let eventCount = 0;
const unsubscribe = agent.subscribe(_event => {
eventCount++;
});
// No initial event on subscribe
expect(eventCount).toBe(0);
// State mutators don't emit events
agent.setSystemPrompt(["Test prompt"]);
expect(eventCount).toBe(0);
expect(agent.state.systemPrompt).toEqual(["Test prompt"]);
// Unsubscribe should work
unsubscribe();
agent.setSystemPrompt(["Another prompt"]);
expect(eventCount).toBe(0); // Should not increase
});
it("should update state with mutators", () => {
const agent = new Agent();
// Test setSystemPrompt
agent.setSystemPrompt(["Custom prompt"]);
expect(agent.state.systemPrompt).toEqual(["Custom prompt"]);
// Test setModel
const newModel = getBundledModel("google", "gemini-2.5-flash");
agent.setModel(newModel);
expect(agent.state.model).toBe(newModel);
// Test setThinkingLevel
agent.setThinkingLevel(ThinkingLevel.High);
expect(agent.state.thinkingLevel).toBe(ThinkingLevel.High);
// Test setTools
const tools = [{ name: "test", description: "test tool" } as any];
agent.setTools(tools);
expect(agent.state.tools).toBe(tools);
// Test replaceMessages
const messages = [{ role: "user" as const, content: "Hello", timestamp: Date.now() }];
agent.replaceMessages(messages);
expect(agent.state.messages).toEqual(messages);
expect(agent.state.messages).not.toBe(messages); // Should be a copy
// Test appendMessage
const newMessage = createAssistantMessage([{ type: "text", text: "Hi" }]);
agent.appendMessage(newMessage);
expect(agent.state.messages).toHaveLength(2);
expect(agent.state.messages[1]).toBe(newMessage);
// Test clearMessages
agent.clearMessages();
expect(agent.state.messages).toEqual([]);
});
it("should support steering message queueing", async () => {
const agent = new Agent();
@@ -108,13 +18,6 @@ describe("Agent", () => {
expect(agent.state.messages).not.toContainEqual(message);
});
it("should handle abort controller", () => {
const agent = new Agent();
// Should not throw even if nothing is running
expect(() => agent.abort()).not.toThrow();
});
it("continue() should process queued follow-up messages after an assistant turn", async () => {
const agent = new Agent({
streamFn: () => {
@@ -323,63 +226,6 @@ describe("Agent", () => {
]);
});
it("forwards sessionId and thinkingBudgets to streamFn options", async () => {
let receivedSessionId: string | undefined;
let receivedBudgets: ThinkingBudgets | undefined;
const agent = new Agent({
sessionId: "session-abc",
thinkingBudgets: { minimal: 64, low: 256 },
streamFn: (_model, _context, options) => {
receivedSessionId = options?.sessionId;
receivedBudgets = options?.thinkingBudgets;
const stream = new MockAssistantStream();
queueMicrotask(() => {
const message = createAssistantMessage([{ type: "text", text: "ok" }]);
stream.push({ type: "done", reason: "stop", message });
});
return stream;
},
});
await agent.prompt("hello");
expect(receivedSessionId).toBe("session-abc");
expect(receivedBudgets).toEqual({ minimal: 64, low: 256 });
agent.sessionId = "session-def";
agent.thinkingBudgets = { medium: 512 };
await agent.prompt("hello again");
expect(receivedSessionId).toBe("session-def");
expect(receivedBudgets).toEqual({ medium: 512 });
});
it("forwards onPayload to streamFn options", async () => {
let receivedOnPayload: SimpleStreamOptions["onPayload"] | undefined;
const agent = new Agent({
onPayload: async (payload, model) => ({ payload, provider: model?.provider }),
streamFn: (_model, _context, options) => {
receivedOnPayload = options?.onPayload;
const stream = new MockAssistantStream();
queueMicrotask(() => {
const message = createAssistantMessage([{ type: "text", text: "ok" }]);
stream.push({ type: "done", reason: "stop", message });
});
return stream;
},
});
await agent.prompt("hello");
expect(receivedOnPayload).toBeDefined();
const replacementPayload = await receivedOnPayload?.({ request: true }, getBundledModel("openai", "gpt-4o-mini"));
expect(replacementPayload).toEqual({
payload: { request: true },
provider: "openai",
});
});
it("re-reads thinking level for each model call within a run", async () => {
const toolSchema = Type.Object({ value: Type.String() });
type Details = { value: string };
@@ -1,34 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test";
import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "../src/provider-models/descriptors";
import { alibabaCodingPlanModelManagerOptions } from "../src/provider-models/openai-compat";
import { getEnvApiKey } from "../src/stream";
const originalAlibabaApiKey = Bun.env.ALIBABA_CODING_PLAN_API_KEY;
afterEach(() => {
if (originalAlibabaApiKey === undefined) {
delete Bun.env.ALIBABA_CODING_PLAN_API_KEY;
return;
}
Bun.env.ALIBABA_CODING_PLAN_API_KEY = originalAlibabaApiKey;
});
describe("alibaba-coding-plan provider support", () => {
test("resolves ALIBABA_CODING_PLAN_API_KEY from environment", () => {
Bun.env.ALIBABA_CODING_PLAN_API_KEY = "alibaba-test-key";
expect(getEnvApiKey("alibaba-coding-plan")).toBe("alibaba-test-key");
});
test("registers built-in descriptor and default model", () => {
const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === "alibaba-coding-plan");
expect(descriptor).toBeDefined();
expect(descriptor?.defaultModel).toBe("qwen3.5-plus");
expect(DEFAULT_MODEL_PER_PROVIDER["alibaba-coding-plan"]).toBe("qwen3.5-plus");
});
test("builds model manager options with alibaba-coding-plan defaults", () => {
const options = alibabaCodingPlanModelManagerOptions();
expect(options.providerId).toBe("alibaba-coding-plan");
expect(options.fetchDynamicModels).toBeDefined();
});
});
@@ -9,8 +9,6 @@ import {
buildAnthropicClientOptions,
buildAnthropicHeaders,
buildAnthropicSystemBlocks,
claudeCodeHeaders,
claudeCodeSystemInstruction,
claudeCodeVersion,
generateClaudeCloakingUserId,
isClaudeCloakingUserId,
@@ -83,21 +81,6 @@ function captureAnthropicPayload(
}
describe("Anthropic request fingerprint alignment", () => {
it("uses updated Claude Code header defaults", () => {
const headers = buildAnthropicHeaders({
apiKey: "sk-ant-oat-test",
isOAuth: true,
stream: true,
});
expect(headers["Anthropic-Beta"]).toContain("context-management-2025-06-27");
expect(headers["Anthropic-Beta"]).toContain("prompt-caching-scope-2026-01-05");
expect(headers["Anthropic-Beta"]).not.toContain("fine-grained-tool-streaming-2025-05-14");
expect(headers["User-Agent"]).toBe(`claude-cli/${claudeCodeVersion} (external, cli)`);
expect(claudeCodeHeaders["X-Stainless-Package-Version"]).toBe("0.74.0");
expect("X-Stainless-Helper-Method" in claudeCodeHeaders).toBe(false);
});
it("maps Stainless OS and arch values from explicit inputs", () => {
expect(mapStainlessOs("darwin")).toBe("MacOS");
expect(mapStainlessOs("windows")).toBe("Windows");
@@ -124,29 +107,6 @@ describe("Anthropic request fingerprint alignment", () => {
expect(headers["X-Stainless-Arch"]).toBe(mapStainlessArch(process.arch));
});
it("injects billing header and Claude Agent SDK identity block", () => {
const blocks = buildAnthropicSystemBlocks(["Stay concise."], {
includeClaudeCodeInstruction: true,
extraInstructions: ["Use citations when possible"],
});
expect(blocks).toBeDefined();
expect(blocks?.[0]?.text.startsWith(`x-anthropic-billing-header: cc_version=${claudeCodeVersion}.`)).toBe(true);
expect(blocks?.[0]?.text).toMatch(/cc_entrypoint=cli; cch=[0-9a-f]{5};$/);
expect(blocks?.[1]).toEqual({
type: "text",
text: claudeCodeSystemInstruction,
});
expect(blocks?.[2]).toEqual({
type: "text",
text: "Use citations when possible",
});
expect(blocks?.[3]).toEqual({
type: "text",
text: "Stay concise.",
});
});
it("attaches cache_control only to the last emitted system block when cacheControl is set", () => {
const blocks = buildAnthropicSystemBlocks(["Stay concise."], {
includeClaudeCodeInstruction: true,
@@ -607,21 +567,6 @@ describe("Anthropic request fingerprint alignment", () => {
expect(strictNames).toEqual(["python"]);
});
it("drops fine-grained tool-streaming beta from default Anthropic client options", () => {
const options = buildAnthropicClientOptions({
model: ANTHROPIC_MODEL,
apiKey: "sk-ant-oat-test",
extraBetas: [],
stream: true,
interleavedThinking: false,
dynamicHeaders: {},
});
const beta = options.defaultHeaders["Anthropic-Beta"];
expect(beta).toContain("context-management-2025-06-27");
expect(beta).not.toContain("fine-grained-tool-streaming-2025-05-14");
});
it("adds legacy fine-grained tool-streaming beta only for tool requests on incompatible models", () => {
const incompatibleModel: Model<"anthropic-messages"> = {
...ANTHROPIC_MODEL,
-8
View File
@@ -14,14 +14,6 @@ afterEach(() => {
describe("custom API registry", () => {
const streamSimple: CustomStreamSimpleFn = () => ({}) as unknown as AssistantMessageEventStream;
test("registers and resolves a custom API provider", () => {
registerCustomApi("custom-provider", streamSimple, "ext-a");
const provider = getCustomApi("custom-provider");
expect(provider).toBeDefined();
expect(provider?.streamSimple).toBe(streamSimple);
expect(provider?.sourceId).toBe("ext-a");
});
test("rejects registrations that collide with built-in API names", () => {
expect(() => registerCustomApi("openai-responses", streamSimple)).toThrow(
@@ -107,11 +107,6 @@ function makeUnionTool(strict: boolean): Tool {
}
describe("supportsFreeformApplyPatch", () => {
test("absent flag returns false", () => {
// No runtime auto-detection — requires generated model metadata.
expect(supportsFreeformApplyPatch(makeModel())).toBe(false);
});
test("applyPatchToolType: freeform enables", () => {
expect(supportsFreeformApplyPatch(makeModel({ applyPatchToolType: "freeform" }))).toBe(true);
});
-763
View File
@@ -1,763 +0,0 @@
import { describe, expect, it } from "bun:test";
import { getBundledModel } from "@oh-my-pi/pi-ai/models";
import { complete } from "@oh-my-pi/pi-ai/stream";
import type { Api, AssistantMessage, Context, Model, OptionsForApi, UserMessage } from "@oh-my-pi/pi-ai/types";
import { e2eApiKey, resolveApiKey } from "./oauth";
// Resolve OAuth tokens at module level (async, runs before tests)
const oauthTokens = await Promise.all([
resolveApiKey("anthropic"),
resolveApiKey("github-copilot"),
resolveApiKey("google-gemini-cli"),
resolveApiKey("google-antigravity"),
resolveApiKey("openai-codex"),
]);
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
async function testEmptyMessage<TApi extends Api>(llm: Model<TApi>, options: OptionsForApi<TApi> = {}) {
// Test with completely empty content array
const emptyMessage: UserMessage = {
role: "user",
content: [],
timestamp: Date.now(),
};
const context: Context = {
messages: [emptyMessage],
};
const response = await complete(llm, context, options);
// Should either handle gracefully or return an error
expect(response).toBeDefined();
expect(response.role).toBe("assistant");
// Should handle empty string gracefully
if (response.stopReason === "error") {
expect(response.errorMessage).toBeDefined();
} else {
expect(response.content).toBeDefined();
}
}
async function testEmptyStringMessage<TApi extends Api>(llm: Model<TApi>, options: OptionsForApi<TApi> = {}) {
// Test with empty string content
const context: Context = {
messages: [
{
role: "user",
content: "",
timestamp: Date.now(),
},
],
};
const response = await complete(llm, context, options);
expect(response).toBeDefined();
expect(response.role).toBe("assistant");
// Should handle empty string gracefully
if (response.stopReason === "error") {
expect(response.errorMessage).toBeDefined();
} else {
expect(response.content).toBeDefined();
}
}
async function testWhitespaceOnlyMessage<TApi extends Api>(llm: Model<TApi>, options: OptionsForApi<TApi> = {}) {
// Test with whitespace-only content
const context: Context = {
messages: [
{
role: "user",
content: " \n\t ",
timestamp: Date.now(),
},
],
};
const response = await complete(llm, context, options);
expect(response).toBeDefined();
expect(response.role).toBe("assistant");
// Should handle whitespace-only gracefully
if (response.stopReason === "error") {
expect(response.errorMessage).toBeDefined();
} else {
expect(response.content).toBeDefined();
}
}
async function testEmptyAssistantMessage<TApi extends Api>(llm: Model<TApi>, options: OptionsForApi<TApi> = {}) {
// Test with empty assistant message in conversation flow
// User -> Empty Assistant -> User
const emptyAssistant: AssistantMessage = {
role: "assistant",
content: [],
api: llm.api,
provider: llm.provider,
model: llm.id,
usage: {
input: 10,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 10,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: Date.now(),
};
const context: Context = {
messages: [
{
role: "user",
content: "Hello, how are you?",
timestamp: Date.now(),
},
emptyAssistant,
{
role: "user",
content: "Please respond this time.",
timestamp: Date.now(),
},
],
};
const response = await complete(llm, context, options);
expect(response).toBeDefined();
expect(response.role).toBe("assistant");
// Should handle empty assistant message in context gracefully
if (response.stopReason === "error") {
expect(response.errorMessage).toBeDefined();
} else {
expect(response.content).toBeDefined();
expect(response.content.length).toBeGreaterThan(0);
}
}
describe("AI Providers Empty Message Tests", () => {
describe.skipIf(!e2eApiKey("GEMINI_API_KEY"))("Google Provider Empty Messages", () => {
const llm = getBundledModel("google", "gemini-2.5-flash");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Completions Provider Empty Messages", () => {
const llm = getBundledModel("openai", "gpt-4o-mini");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe.skipIf(!e2eApiKey("OPENAI_API_KEY"))("OpenAI Responses Provider Empty Messages", () => {
const llm = getBundledModel("openai", "gpt-5-mini");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe.skipIf(!e2eApiKey("ANTHROPIC_API_KEY"))("Anthropic Provider Empty Messages", () => {
const llm = getBundledModel("anthropic", "claude-haiku-4-5-20251001");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe.skipIf(!e2eApiKey("XAI_API_KEY"))("xAI Provider Empty Messages", () => {
const llm = getBundledModel("xai", "grok-3");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe.skipIf(!e2eApiKey("GROQ_API_KEY"))("Groq Provider Empty Messages", () => {
const llm = getBundledModel("groq", "openai/gpt-oss-20b");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe.skipIf(!e2eApiKey("CEREBRAS_API_KEY"))("Cerebras Provider Empty Messages", () => {
const llm = getBundledModel("cerebras", "gpt-oss-120b");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe.skipIf(!e2eApiKey("ZAI_API_KEY"))("zAI Provider Empty Messages", () => {
const llm = getBundledModel("zai", "glm-4.5-air");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe.skipIf(!e2eApiKey("MISTRAL_API_KEY"))("Mistral Provider Empty Messages", () => {
const llm = getBundledModel("mistral", "devstral-medium-latest");
it(
"should handle empty content array",
async () => {
await testEmptyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
it(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm);
},
{ retry: 3, timeout: 30000 },
);
});
describe("Anthropic OAuth Provider Empty Messages", () => {
const llm = getBundledModel("anthropic", "claude-haiku-4-5-20251001");
it.skipIf(!anthropicOAuthToken)(
"should handle empty content array",
async () => {
await testEmptyMessage(llm, { apiKey: anthropicOAuthToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!anthropicOAuthToken)(
"should handle empty string content",
async () => {
await testEmptyStringMessage(llm, { apiKey: anthropicOAuthToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!anthropicOAuthToken)(
"should handle whitespace-only content",
async () => {
await testWhitespaceOnlyMessage(llm, { apiKey: anthropicOAuthToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!anthropicOAuthToken)(
"should handle empty assistant message in conversation",
async () => {
await testEmptyAssistantMessage(llm, { apiKey: anthropicOAuthToken });
},
{ retry: 3, timeout: 30000 },
);
});
describe("GitHub Copilot Provider Empty Messages", () => {
it.skipIf(!githubCopilotToken)(
"gpt-4o - should handle empty content array",
async () => {
const llm = getBundledModel("github-copilot", "gpt-4o");
await testEmptyMessage(llm, { apiKey: githubCopilotToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!githubCopilotToken)(
"gpt-4o - should handle empty string content",
async () => {
const llm = getBundledModel("github-copilot", "gpt-4o");
await testEmptyStringMessage(llm, { apiKey: githubCopilotToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!githubCopilotToken)(
"gpt-4o - should handle whitespace-only content",
async () => {
const llm = getBundledModel("github-copilot", "gpt-4o");
await testWhitespaceOnlyMessage(llm, { apiKey: githubCopilotToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!githubCopilotToken)(
"gpt-4o - should handle empty assistant message in conversation",
async () => {
const llm = getBundledModel("github-copilot", "gpt-4o");
await testEmptyAssistantMessage(llm, { apiKey: githubCopilotToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!githubCopilotToken)(
"claude-sonnet-4 - should handle empty content array",
async () => {
const llm = getBundledModel("github-copilot", "claude-sonnet-4");
await testEmptyMessage(llm, { apiKey: githubCopilotToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!githubCopilotToken)(
"claude-sonnet-4 - should handle empty string content",
async () => {
const llm = getBundledModel("github-copilot", "claude-sonnet-4");
await testEmptyStringMessage(llm, { apiKey: githubCopilotToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!githubCopilotToken)(
"claude-sonnet-4 - should handle whitespace-only content",
async () => {
const llm = getBundledModel("github-copilot", "claude-sonnet-4");
await testWhitespaceOnlyMessage(llm, { apiKey: githubCopilotToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!githubCopilotToken)(
"claude-sonnet-4 - should handle empty assistant message in conversation",
async () => {
const llm = getBundledModel("github-copilot", "claude-sonnet-4");
await testEmptyAssistantMessage(llm, { apiKey: githubCopilotToken });
},
{ retry: 3, timeout: 30000 },
);
});
describe("Google Gemini CLI Provider Empty Messages", () => {
it.skipIf(!geminiCliToken)(
"gemini-2.5-flash - should handle empty content array",
async () => {
const llm = getBundledModel("google-gemini-cli", "gemini-2.5-flash");
await testEmptyMessage(llm, { apiKey: geminiCliToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!geminiCliToken)(
"gemini-2.5-flash - should handle empty string content",
async () => {
const llm = getBundledModel("google-gemini-cli", "gemini-2.5-flash");
await testEmptyStringMessage(llm, { apiKey: geminiCliToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!geminiCliToken)(
"gemini-2.5-flash - should handle whitespace-only content",
async () => {
const llm = getBundledModel("google-gemini-cli", "gemini-2.5-flash");
await testWhitespaceOnlyMessage(llm, { apiKey: geminiCliToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!geminiCliToken)(
"gemini-2.5-flash - should handle empty assistant message in conversation",
async () => {
const llm = getBundledModel("google-gemini-cli", "gemini-2.5-flash");
await testEmptyAssistantMessage(llm, { apiKey: geminiCliToken });
},
{ retry: 3, timeout: 30000 },
);
});
describe("Google Antigravity Provider Empty Messages", () => {
it.skipIf(!antigravityToken)(
"gemini-3-flash - should handle empty content array",
async () => {
const llm = getBundledModel("google-antigravity", "gemini-3-flash");
await testEmptyMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"gemini-3-flash - should handle empty string content",
async () => {
const llm = getBundledModel("google-antigravity", "gemini-3-flash");
await testEmptyStringMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"gemini-3-flash - should handle whitespace-only content",
async () => {
const llm = getBundledModel("google-antigravity", "gemini-3-flash");
await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"gemini-3-flash - should handle empty assistant message in conversation",
async () => {
const llm = getBundledModel("google-antigravity", "gemini-3-flash");
await testEmptyAssistantMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"claude-sonnet-4-5 - should handle empty content array",
async () => {
const llm = getBundledModel("google-antigravity", "claude-sonnet-4-5");
await testEmptyMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"claude-sonnet-4-5 - should handle empty string content",
async () => {
const llm = getBundledModel("google-antigravity", "claude-sonnet-4-5");
await testEmptyStringMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"claude-sonnet-4-5 - should handle whitespace-only content",
async () => {
const llm = getBundledModel("google-antigravity", "claude-sonnet-4-5");
await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"claude-sonnet-4-5 - should handle empty assistant message in conversation",
async () => {
const llm = getBundledModel("google-antigravity", "claude-sonnet-4-5");
await testEmptyAssistantMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"gpt-oss-120b-medium - should handle empty content array",
async () => {
const llm = getBundledModel("google-antigravity", "gpt-oss-120b-medium");
await testEmptyMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"gpt-oss-120b-medium - should handle empty string content",
async () => {
const llm = getBundledModel("google-antigravity", "gpt-oss-120b-medium");
await testEmptyStringMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"gpt-oss-120b-medium - should handle whitespace-only content",
async () => {
const llm = getBundledModel("google-antigravity", "gpt-oss-120b-medium");
await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!antigravityToken)(
"gpt-oss-120b-medium - should handle empty assistant message in conversation",
async () => {
const llm = getBundledModel("google-antigravity", "gpt-oss-120b-medium");
await testEmptyAssistantMessage(llm, { apiKey: antigravityToken });
},
{ retry: 3, timeout: 30000 },
);
});
describe("OpenAI Codex Provider Empty Messages", () => {
it.skipIf(!openaiCodexToken)(
"gpt-5.2-codex - should handle empty content array",
async () => {
const llm = getBundledModel("openai-codex", "gpt-5.2-codex");
await testEmptyMessage(llm, { apiKey: openaiCodexToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!openaiCodexToken)(
"gpt-5.2-codex - should handle empty string content",
async () => {
const llm = getBundledModel("openai-codex", "gpt-5.2-codex");
await testEmptyStringMessage(llm, { apiKey: openaiCodexToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!openaiCodexToken)(
"gpt-5.2-codex - should handle whitespace-only content",
async () => {
const llm = getBundledModel("openai-codex", "gpt-5.2-codex");
await testWhitespaceOnlyMessage(llm, { apiKey: openaiCodexToken });
},
{ retry: 3, timeout: 30000 },
);
it.skipIf(!openaiCodexToken)(
"gpt-5.2-codex - should handle empty assistant message in conversation",
async () => {
const llm = getBundledModel("openai-codex", "gpt-5.2-codex");
await testEmptyAssistantMessage(llm, { apiKey: openaiCodexToken });
},
{ retry: 3, timeout: 30000 },
);
});
});
@@ -1,49 +0,0 @@
import { describe, expect, it } from "bun:test";
import { getBundledModel } from "../src/models";
describe("Copilot Claude model routing", () => {
it("routes claude-sonnet-4 via anthropic-messages API", () => {
const model = getBundledModel("github-copilot", "claude-sonnet-4");
expect(model).toBeDefined();
expect(model.api).toBe("anthropic-messages");
});
it("routes claude-sonnet-4.5 via anthropic-messages API", () => {
const model = getBundledModel("github-copilot", "claude-sonnet-4.5");
expect(model).toBeDefined();
expect(model.api).toBe("anthropic-messages");
});
it("routes claude-haiku-4.5 via anthropic-messages API", () => {
const model = getBundledModel("github-copilot", "claude-haiku-4.5");
expect(model).toBeDefined();
expect(model.api).toBe("anthropic-messages");
});
it("routes claude-opus-4.5 via anthropic-messages API", () => {
const model = getBundledModel("github-copilot", "claude-opus-4.5");
expect(model).toBeDefined();
expect(model.api).toBe("anthropic-messages");
});
it("does not have compat block on Claude models (completions-API-specific)", () => {
const sonnet = getBundledModel("github-copilot", "claude-sonnet-4");
expect("compat" in sonnet).toBe(false);
});
it("preserves static Copilot headers on Claude models", () => {
const model = getBundledModel("github-copilot", "claude-sonnet-4");
expect(model.headers).toBeDefined();
expect(model.headers?.["User-Agent"]).toContain("opencode");
});
it("keeps non-Claude Copilot models on their existing APIs", () => {
const gpt4o = getBundledModel("github-copilot", "gpt-4o");
expect(gpt4o).toBeDefined();
expect(gpt4o.api).toBe("openai-completions");
const gpt5 = getBundledModel("github-copilot", "gpt-5");
expect(gpt5).toBeDefined();
expect(gpt5.api).toBe("openai-responses");
});
});
@@ -1,20 +0,0 @@
import { describe, expect, test } from "bun:test";
import { getModelMapping } from "../src/providers/gitlab-duo";
describe("gitlab duo model mapping", () => {
test("resolves Duo alias IDs", () => {
const mapping = getModelMapping("duo-chat-gpt-5-codex");
expect(mapping).toBeDefined();
expect(mapping?.model).toBe("gpt-5-codex");
});
test("resolves canonical model IDs", () => {
const mapping = getModelMapping("gpt-5-codex");
expect(mapping).toBeDefined();
expect(mapping?.model).toBe("gpt-5-codex");
});
test("returns undefined for unknown IDs", () => {
expect(getModelMapping("totally-unknown-model")).toBeUndefined();
});
});
@@ -2,7 +2,6 @@ import { afterEach, describe, expect, it, vi } from "bun:test";
import { Effort } from "@oh-my-pi/pi-ai";
import { enrichModelThinking } from "@oh-my-pi/pi-ai/model-thinking";
import { hookFetch } from "@oh-my-pi/pi-utils";
import { getBundledModel } from "../src/models";
import { streamSimple } from "../src/stream";
import type { Context, Model } from "../src/types";
@@ -48,10 +47,6 @@ describe("google-gemini-cli Gemini 3.x thinking mapping", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("includes gemini-3.1-pro-preview in bundled google-gemini-cli models", () => {
expect(getBundledModel("google-gemini-cli", "gemini-3.1-pro-preview")?.id).toBe("gemini-3.1-pro-preview");
});
it("uses thinkingLevel for gemini-3.1-pro-preview when the effort is supported", async () => {
let requestBody: string | undefined;
using _hook = hookFetch((_input, init) => {
-6
View File
@@ -16,12 +16,6 @@ const OPENCODE_GO_BASE = "https://opencode.ai/zen/go/v1";
describe("opencode-go resolver routes 404-ing ids to openai-completions (issue #887)", () => {
const descriptor = MODELS_DEV_PROVIDER_DESCRIPTORS.find(d => d.providerId === "opencode-go");
test("descriptor exists and exposes resolveApi", () => {
expect(descriptor).toBeDefined();
expect(descriptor?.modelsDevKey).toBe("opencode-go");
expect(descriptor?.resolveApi).toBeTypeOf("function");
});
// Per upstream models.dev (verified 2026-05-02 against
// https://models.dev/api.json["opencode-go"].models), these three ids carry
// `provider.npm = "@ai-sdk/anthropic"`. The naive @ai-sdk/anthropic rule
-41
View File
@@ -1,41 +0,0 @@
import { afterEach, describe, expect, test } from "bun:test";
import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "../src/provider-models/descriptors";
import { kiloModelManagerOptions } from "../src/provider-models/openai-compat";
import { getEnvApiKey } from "../src/stream";
import { getOAuthProviders } from "../src/utils/oauth";
const originalKiloApiKey = Bun.env.KILO_API_KEY;
afterEach(() => {
if (originalKiloApiKey === undefined) {
delete Bun.env.KILO_API_KEY;
return;
}
Bun.env.KILO_API_KEY = originalKiloApiKey;
});
describe("kilo provider support", () => {
test("resolves KILO_API_KEY from environment", () => {
Bun.env.KILO_API_KEY = "kilo-test-key";
expect(getEnvApiKey("kilo")).toBe("kilo-test-key");
});
test("registers built-in descriptor and default model", () => {
const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === "kilo");
expect(descriptor).toBeDefined();
expect(descriptor?.defaultModel).toBe("anthropic/claude-sonnet-4.5");
expect(descriptor?.catalogDiscovery?.envVars).toContain("KILO_API_KEY");
expect(descriptor?.catalogDiscovery?.allowUnauthenticated).toBe(true);
expect(DEFAULT_MODEL_PER_PROVIDER.kilo).toBe("anthropic/claude-sonnet-4.5");
});
test("registers Kilo in OAuth provider selector", () => {
const provider = getOAuthProviders().find(item => item.id === "kilo");
expect(provider?.name).toBe("Kilo Gateway");
});
test("builds model manager options with kilo defaults", () => {
const options = kiloModelManagerOptions();
expect(options.providerId).toBe("kilo");
expect(options.fetchDynamicModels).toBeDefined();
});
});
-48
View File
@@ -10,8 +10,6 @@ import {
requireSupportedEffort,
} from "@oh-my-pi/pi-ai/model-thinking";
import type { Api, Model, Provider } from "@oh-my-pi/pi-ai/types";
import { getBundledModel } from "../src/models";
import MODELS from "../src/models.json" with { type: "json" };
function createModel<TApi extends Api>(overrides: {
id: string;
@@ -133,52 +131,6 @@ describe("model thinking metadata", () => {
});
});
describe("bundled GPT-5.4 model metadata", () => {
it("stores raw GPT-5.4 mini/nano catalog metadata for OpenAI, OpenAI Codex, and Copilot", () => {
const openAiMini = MODELS.openai["gpt-5.4-mini"];
const openAiNano = MODELS.openai["gpt-5.4-nano"];
const openAiCodexMini = MODELS["openai-codex"]["gpt-5.4-mini"];
const openAiCodexNano = MODELS["openai-codex"]["gpt-5.4-nano"];
const copilotMini = MODELS["github-copilot"]["gpt-5.4-mini"];
expect(openAiMini?.thinking).toEqual({ mode: "effort", minLevel: "low", maxLevel: "xhigh" });
expect(openAiNano?.thinking).toEqual({ mode: "effort", minLevel: "low", maxLevel: "xhigh" });
expect(openAiCodexMini?.thinking).toEqual({ mode: "effort", minLevel: "low", maxLevel: "xhigh" });
expect(openAiCodexNano?.thinking).toEqual({ mode: "effort", minLevel: "low", maxLevel: "xhigh" });
expect(copilotMini?.thinking).toEqual({ mode: "effort", minLevel: "low", maxLevel: "xhigh" });
expect(openAiCodexMini?.api).toBe("openai-codex-responses");
expect(openAiCodexNano?.api).toBe("openai-codex-responses");
expect(openAiCodexMini?.contextWindow).toBe(272000);
expect(openAiCodexNano?.contextWindow).toBe(272000);
expect(openAiCodexMini?.preferWebsockets).toBe(true);
expect(openAiCodexNano?.preferWebsockets).toBe(true);
expect(openAiCodexMini?.priority).toBe(1);
expect(openAiCodexNano?.priority).toBe(2);
});
it("exposes xhigh support for bundled GPT-5.4 mini/nano runtime models across supported providers", () => {
const openAiMini = getBundledModel("openai", "gpt-5.4-mini");
const openAiNano = getBundledModel("openai", "gpt-5.4-nano");
const openAiCodexMini = getBundledModel("openai-codex", "gpt-5.4-mini");
const openAiCodexNano = getBundledModel("openai-codex", "gpt-5.4-nano");
const copilotMini = getBundledModel("github-copilot", "gpt-5.4-mini");
expect(openAiCodexMini.contextWindow).toBe(272000);
expect(openAiCodexNano.contextWindow).toBe(272000);
expect(requireSupportedEffort(openAiMini, Effort.XHigh)).toBe(Effort.XHigh);
expect(requireSupportedEffort(openAiNano, Effort.XHigh)).toBe(Effort.XHigh);
expect(requireSupportedEffort(openAiCodexMini, Effort.XHigh)).toBe(Effort.XHigh);
expect(requireSupportedEffort(openAiCodexNano, Effort.XHigh)).toBe(Effort.XHigh);
expect(requireSupportedEffort(copilotMini, Effort.XHigh)).toBe(Effort.XHigh);
});
it("does not bundle GitHub Copilot GPT-5.4 nano", () => {
const copilotModels = MODELS["github-copilot"] as Record<string, unknown>;
expect(copilotModels["gpt-5.4-nano"]).toBeUndefined();
expect(getBundledModel("github-copilot", "gpt-5.4-nano")).toBeUndefined();
});
});
describe("generated model policies", () => {
it("refreshes thinking metadata and applies parsed catalog corrections", () => {
const models: Model<Api>[] = [
@@ -1,9 +1,7 @@
import { afterEach, describe, expect, test, vi } from "bun:test";
import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "../src/provider-models/descriptors";
import { ollamaCloudModelManagerOptions } from "../src/provider-models/ollama";
import { completeSimple, getEnvApiKey, stream, streamSimple } from "../src/stream";
import type { Context, Model, Tool } from "../src/types";
import { getOAuthProviders } from "../src/utils/oauth";
const originalApiKey = Bun.env.OLLAMA_CLOUD_API_KEY;
const originalFetch = global.fetch;
@@ -64,18 +62,6 @@ describe("ollama-cloud provider support", () => {
expect(getEnvApiKey("ollama-cloud")).toBe("ollama-cloud-test-key");
});
test("registers built-in descriptor, default model, and oauth selector entry", () => {
const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === "ollama-cloud");
expect(descriptor).toBeDefined();
expect(descriptor?.defaultModel).toBe("gpt-oss:120b");
expect(descriptor?.catalogDiscovery?.envVars).toContain("OLLAMA_CLOUD_API_KEY");
expect(descriptor?.catalogDiscovery?.allowUnauthenticated).toBeUndefined();
expect(DEFAULT_MODEL_PER_PROVIDER["ollama-cloud"]).toBe("gpt-oss:120b");
const provider = getOAuthProviders().find(item => item.id === "ollama-cloud");
expect(provider?.name).toBe("Ollama Cloud");
});
test("discovers ollama-cloud models from native cloud endpoints", async () => {
global.fetch = vi.fn(async (input, init) => {
const url = String(input);
@@ -1,10 +1,6 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import { getBundledModel } from "../src/models";
import {
normalizeOpenAIResponsesPromptCacheKey,
type OpenAIResponsesOptions,
streamOpenAIResponses,
} from "../src/providers/openai-responses";
import { type OpenAIResponsesOptions, streamOpenAIResponses } from "../src/providers/openai-responses";
import type { Context, Model } from "../src/types";
const originalFetch = global.fetch;
@@ -115,23 +111,4 @@ describe("openai-responses cache affinity", () => {
expect(captured.clientRequestId).toBeNull();
expect(captured.body?.prompt_cache_key).toBeUndefined();
});
it("normalizes long prompt cache keys while preserving ordered system prompts", async () => {
const longSessionId = "session-".repeat(20);
const expectedCacheKey = normalizeOpenAIResponsesPromptCacheKey(longSessionId);
if (!expectedCacheKey) throw new Error("Expected normalized prompt cache key");
const captured = await captureOpenAIResponseHeaders({ sessionId: longSessionId });
expect(captured.sessionId).toBe(expectedCacheKey);
expect(captured.clientRequestId).toBe(expectedCacheKey);
expect(captured.body?.prompt_cache_key).toBe(expectedCacheKey);
expect(expectedCacheKey?.length).toBeLessThanOrEqual(64);
const input = captured.body?.input;
expect(Array.isArray(input)).toBe(true);
expect((input as Array<{ role?: string; content?: string }>).slice(0, 2)).toEqual([
{ role: "developer", content: "stable system" },
{ role: "developer", content: "stable durable context" },
]);
});
});
-16
View File
@@ -48,14 +48,6 @@ describe("parseRateLimitReason", () => {
});
describe("calculateRateLimitBackoffMs", () => {
it("returns 30 minutes for QUOTA_EXHAUSTED", () => {
expect(calculateRateLimitBackoffMs("QUOTA_EXHAUSTED")).toBe(30 * 60 * 1000);
});
it("returns 30s for RATE_LIMIT_EXCEEDED", () => {
expect(calculateRateLimitBackoffMs("RATE_LIMIT_EXCEEDED")).toBe(30_000);
});
it("returns 45–75s range for MODEL_CAPACITY_EXHAUSTED (jitter)", () => {
for (let i = 0; i < 20; i++) {
const ms = calculateRateLimitBackoffMs("MODEL_CAPACITY_EXHAUSTED");
@@ -63,12 +55,4 @@ describe("calculateRateLimitBackoffMs", () => {
expect(ms).toBeLessThanOrEqual(75_000);
}
});
it("returns 20s for SERVER_ERROR", () => {
expect(calculateRateLimitBackoffMs("SERVER_ERROR")).toBe(20_000);
});
it("returns conservative fallback for UNKNOWN", () => {
expect(calculateRateLimitBackoffMs("UNKNOWN")).toBe(30 * 60 * 1000);
});
});
-26
View File
@@ -1,26 +0,0 @@
import { describe, expect, it } from "bun:test";
import MODELS from "@oh-my-pi/pi-ai/models.json" with { type: "json" };
import { complete } from "@oh-my-pi/pi-ai/stream";
import type { Model } from "@oh-my-pi/pi-ai/types";
import { e2eApiKey } from "./oauth";
describe.skipIf(!e2eApiKey("OPENCODE_API_KEY"))("OpenCode Models Smoke Test", () => {
const providers = [
{ key: "opencode-zen", label: "OpenCode Zen" },
{ key: "opencode-go", label: "OpenCode Go" },
] as const;
providers.forEach(({ key, label }) => {
const providerModels = Object.values(MODELS[key]);
providerModels.forEach(model => {
it(`${label}: ${model.id}`, async () => {
const response = await complete(model as unknown as Model, {
messages: [{ role: "user", content: "Say hello.", timestamp: Date.now() }],
});
expect(response.content).toBeTruthy();
expect(response.stopReason).toBe("stop");
}, 60000);
});
});
});
@@ -346,89 +346,6 @@ describe("AgentSession retry fallback", () => {
expect(lastAssistant.content).toContainEqual({ type: "text", text: "Recovered after OpenAI timeout" });
});
it("auto-retries Bun socket closure errors", async () => {
const model = getBundledModel("openai", "gpt-4o-mini");
if (!model) {
throw new Error("Expected bundled OpenAI test model to exist");
}
const socketError =
"The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()";
const requestedModels: string[] = [];
let attemptCount = 0;
const agent = new Agent({
getApiKey: provider => `${provider}-test-key`,
initialState: {
model,
systemPrompt: ["Test"],
tools: [],
messages: [],
},
streamFn: requestedModel => {
requestedModels.push(`${requestedModel.provider}/${requestedModel.id}`);
const stream = new MockAssistantStream();
queueMicrotask(() => {
attemptCount += 1;
if (attemptCount === 1) {
const message = createAssistantMessage(requestedModel, {
stopReason: "error",
errorMessage: socketError,
});
stream.push({ type: "start", partial: message });
stream.push({ type: "error", reason: "error", error: message });
return;
}
if (attemptCount === 2) {
const message = createAssistantMessage(requestedModel, {
text: "Recovered after socket closure",
stopReason: "stop",
});
stream.push({
type: "start",
partial: createAssistantMessage(requestedModel, { text: "", stopReason: "stop" }),
});
stream.push({ type: "done", reason: "stop", message });
return;
}
throw new Error(`Unexpected retry attempt in socket closure test: ${attemptCount}`);
});
return stream;
},
});
const settings = Settings.isolated({
"compaction.enabled": false,
"retry.baseDelayMs": 5,
"retry.maxRetries": 1,
});
settings.setModelRole("default", `${model.provider}/${model.id}`);
session = new AgentSession({
agent,
sessionManager: SessionManager.inMemory(),
settings,
modelRegistry,
});
const { retryStartEvents, retryEndEvents } = trackRetryEvents(session);
await session.prompt("Retry Bun socket closure");
await session.waitForIdle();
expect(requestedModels).toEqual([`${model.provider}/${model.id}`, `${model.provider}/${model.id}`]);
expect(retryStartEvents).toHaveLength(1);
expect(retryStartEvents[0]).toMatchObject({
attempt: 1,
maxAttempts: 1,
errorMessage: socketError,
});
expect(retryEndEvents).toHaveLength(1);
expect(retryEndEvents[0]).toMatchObject({ success: true, attempt: 1 });
const lastAssistant = getLastAssistantMessage(session);
expect(lastAssistant.stopReason).toBe("stop");
expect(lastAssistant.content).toContainEqual({ type: "text", text: "Recovered after socket closure" });
});
it("auto-retries Anthropic stream-envelope failures before message_start", async () => {
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) {
@@ -10,7 +10,6 @@ import type { ExtensionRunner } from "@oh-my-pi/pi-coding-agent/extensibility/ex
import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { TOOL_TIMEOUTS } from "@oh-my-pi/pi-coding-agent/tools/tool-timeouts";
import { TempDir } from "@oh-my-pi/pi-utils";
describe("AgentSession user shortcut hooks", () => {
@@ -178,29 +177,4 @@ describe("AgentSession user shortcut hooks", () => {
session.messages.some(message => message.role === "pythonExecution" && message.excludeFromContext === false),
).toBe(true);
});
it("passes the default native timeout to fallback bash execution", async () => {
vi.spyOn(bashExecutor, "executeBash").mockResolvedValue({
output: "bash fallback",
exitCode: 0,
cancelled: false,
truncated: false,
totalLines: 1,
totalBytes: 13,
outputLines: 1,
outputBytes: 13,
});
createSession();
await session.executeBash("pwd", undefined, { excludeFromContext: false });
expect(bashExecutor.executeBash).toHaveBeenCalledWith(
"pwd",
expect.objectContaining({
timeout: TOOL_TIMEOUTS.bash.default * 1000,
sessionKey: expect.any(String),
signal: expect.any(AbortSignal),
}),
);
});
});
-288
View File
@@ -1,288 +0,0 @@
import { describe, expect, test } from "bun:test";
import { Effort } from "@oh-my-pi/pi-ai";
import { parseArgs } from "@oh-my-pi/pi-coding-agent/cli/args";
describe("parseArgs", () => {
describe("--version flag", () => {
test("parses --version flag", () => {
const result = parseArgs(["--version"]);
expect(result.version).toBe(true);
});
test("parses -v shorthand", () => {
const result = parseArgs(["-v"]);
expect(result.version).toBe(true);
});
test("--version takes precedence over other args", () => {
const result = parseArgs(["--version", "--help", "some message"]);
expect(result.version).toBe(true);
expect(result.help).toBe(true);
expect(result.messages).toContain("some message");
});
});
describe("--help flag", () => {
test("parses --help flag", () => {
const result = parseArgs(["--help"]);
expect(result.help).toBe(true);
});
test("parses -h shorthand", () => {
const result = parseArgs(["-h"]);
expect(result.help).toBe(true);
});
});
describe("--print flag", () => {
test("parses --print flag", () => {
const result = parseArgs(["--print"]);
expect(result.print).toBe(true);
});
test("parses -p shorthand", () => {
const result = parseArgs(["-p"]);
expect(result.print).toBe(true);
});
});
describe("--continue flag", () => {
test("parses --continue flag", () => {
const result = parseArgs(["--continue"]);
expect(result.continue).toBe(true);
});
test("parses -c shorthand", () => {
const result = parseArgs(["-c"]);
expect(result.continue).toBe(true);
});
});
describe("--resume flag", () => {
test("parses --resume flag", () => {
const result = parseArgs(["--resume"]);
expect(result.resume).toBe(true);
});
test("parses -r shorthand", () => {
const result = parseArgs(["-r"]);
expect(result.resume).toBe(true);
});
test("parses --resume with session ID", () => {
const result = parseArgs(["--resume", "abc123"]);
expect(result.resume).toBe("abc123");
});
test("parses -r with session path", () => {
const result = parseArgs(["-r", "/path/to/session.jsonl"]);
expect(result.resume).toBe("/path/to/session.jsonl");
});
test("--resume without value before another flag stays boolean", () => {
const result = parseArgs(["--resume", "--model", "opus"]);
expect(result.resume).toBe(true);
expect(result.model).toBe("opus");
});
});
describe("--fork flag", () => {
test("parses --fork with session ID", () => {
const result = parseArgs(["--fork", "abc123"]);
expect(result.fork).toBe("abc123");
});
});
describe("flags with values", () => {
test("parses --provider", () => {
const result = parseArgs(["--provider", "openai"]);
expect(result.provider).toBe("openai");
});
test("parses --model", () => {
const result = parseArgs(["--model", "gpt-4o"]);
expect(result.model).toBe("gpt-4o");
});
test("parses --model=value with equals syntax", () => {
const result = parseArgs(["--model=gpt-4o"]);
expect(result.model).toBe("gpt-4o");
});
test("parses --api-key", () => {
const result = parseArgs(["--api-key", "sk-test-key"]);
expect(result.apiKey).toBe("sk-test-key");
});
test("parses --system-prompt", () => {
const result = parseArgs(["--system-prompt", "You are a helpful assistant"]);
expect(result.systemPrompt).toBe("You are a helpful assistant");
});
test("parses --append-system-prompt", () => {
const result = parseArgs(["--append-system-prompt", "Additional context"]);
expect(result.appendSystemPrompt).toBe("Additional context");
});
test("parses --provider-session-id", () => {
const result = parseArgs(["--provider-session-id", "reb_cache_key"]);
expect(result.providerSessionId).toBe("reb_cache_key");
});
test("parses --mode", () => {
const result = parseArgs(["--mode", "json"]);
expect(result.mode).toBe("json");
});
test("parses --mode rpc", () => {
const result = parseArgs(["--mode", "rpc"]);
expect(result.mode).toBe("rpc");
});
test("parses --session as alias for --resume", () => {
const result = parseArgs(["--session", "/path/to/session.jsonl"]);
expect(result.resume).toBe("/path/to/session.jsonl");
});
test("parses --export", () => {
const result = parseArgs(["--export", "session.jsonl"]);
expect(result.export).toBe("session.jsonl");
});
test("parses --thinking", () => {
const result = parseArgs(["--thinking", "high"]);
expect(result.thinking).toBe(Effort.High);
});
test("parses --models as comma-separated list", () => {
const result = parseArgs(["--models", "gpt-4o,claude-sonnet,gemini-pro"]);
expect(result.models).toEqual(["gpt-4o", "claude-sonnet", "gemini-pro"]);
});
});
describe("--no-session flag", () => {
test("parses --no-session flag", () => {
const result = parseArgs(["--no-session"]);
expect(result.noSession).toBe(true);
});
});
describe("--hook flag", () => {
test("parses single --hook", () => {
const result = parseArgs(["--hook", "./my-hook.ts"]);
expect(result.hooks).toEqual(["./my-hook.ts"]);
});
test("parses multiple --hook flags", () => {
const result = parseArgs(["--hook", "./hook1.ts", "--hook", "./hook2.ts"]);
expect(result.hooks).toEqual(["./hook1.ts", "./hook2.ts"]);
});
});
describe("--no-extensions flag", () => {
test("parses --no-extensions flag", () => {
const result = parseArgs(["--no-extensions"]);
expect(result.noExtensions).toBe(true);
});
test("parses --no-extensions with explicit -e flags", () => {
const result = parseArgs(["--no-extensions", "-e", "foo.ts", "-e", "bar.ts"]);
expect(result.noExtensions).toBe(true);
expect(result.extensions).toEqual(["foo.ts", "bar.ts"]);
});
});
describe("--no-skills flag", () => {
test("parses --no-skills flag", () => {
const result = parseArgs(["--no-skills"]);
expect(result.noSkills).toBe(true);
});
});
describe("--no-rules flag", () => {
test("parses --no-rules flag", () => {
const result = parseArgs(["--no-rules"]);
expect(result.noRules).toBe(true);
});
});
describe("--no-tools flag", () => {
test("parses --no-tools flag", () => {
const result = parseArgs(["--no-tools"]);
expect(result.noTools).toBe(true);
});
test("parses --no-tools with explicit --tools flags", () => {
const result = parseArgs(["--no-tools", "--tools", "read,bash"]);
expect(result.noTools).toBe(true);
expect(result.tools).toEqual(["read", "bash"]);
});
test("lowercases tool names passed to --tools", () => {
const result = parseArgs(["--tools", "Read,Search"]);
expect(result.tools).toEqual(["read", "search"]);
});
test("parses --tools=value with equals syntax", () => {
const result = parseArgs(["--tools=read,bash"]);
expect(result.tools).toEqual(["read", "bash"]);
});
test("parses --tools=value with single tool", () => {
const result = parseArgs(["--tools=ask"]);
expect(result.tools).toEqual(["ask"]);
});
});
describe("--no-lsp flag", () => {
test("parses --no-lsp flag", () => {
const result = parseArgs(["--no-lsp"]);
expect(result.noLsp).toBe(true);
});
});
describe("messages and file args", () => {
test("parses plain text messages", () => {
const result = parseArgs(["hello", "world"]);
expect(result.messages).toEqual(["hello", "world"]);
});
test("parses @file arguments", () => {
const result = parseArgs(["@README.md", "@src/main.ts"]);
expect(result.fileArgs).toEqual(["README.md", "src/main.ts"]);
});
test("parses mixed messages and file args", () => {
const result = parseArgs(["@file.txt", "explain this", "@image.png"]);
expect(result.fileArgs).toEqual(["file.txt", "image.png"]);
expect(result.messages).toEqual(["explain this"]);
});
test("ignores unknown flags starting with -", () => {
const result = parseArgs(["--unknown-flag", "message"]);
expect(result.messages).toEqual(["message"]);
});
});
describe("complex combinations", () => {
test("parses multiple flags together", () => {
const result = parseArgs([
"--provider",
"anthropic",
"--model",
"claude-sonnet",
"--print",
"--thinking",
"high",
"@prompt.md",
"Do the task",
]);
expect(result.provider).toBe("anthropic");
expect(result.model).toBe("claude-sonnet");
expect(result.print).toBe(true);
expect(result.thinking).toBe(Effort.High);
expect(result.fileArgs).toEqual(["prompt.md"]);
expect(result.messages).toEqual(["Do the task"]);
});
});
});
@@ -228,18 +228,4 @@ describe("AsyncJobManager", () => {
expect(manager.getAllJobs()).toHaveLength(0);
expect(manager.hasPendingDeliveries()).toBe(false);
});
test("uses custom id when provided and deduplicates collisions", async () => {
const manager = new AsyncJobManager({
onJobComplete: async () => {},
});
const first = manager.register("task", "alpha", async () => "ok", { id: "2-CheckRustCrate" });
const second = manager.register("task", "beta", async () => "ok", { id: "2-CheckRustCrate" });
expect(first).toBe("2-CheckRustCrate");
expect(second).toBe("2-CheckRustCrate-2");
await manager.waitForAll();
});
});
@@ -3,7 +3,6 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { getDefault } from "@oh-my-pi/pi-coding-agent/config/settings-schema";
import { SelectorController } from "@oh-my-pi/pi-coding-agent/modes/controllers/selector-controller";
import { getProjectAgentDir, Snowflake } from "@oh-my-pi/pi-utils";
import { YAML } from "bun";
@@ -29,15 +28,6 @@ describe("autocompleteMaxVisible setting", () => {
}
});
it("should have default value of 5", () => {
expect(getDefault("autocompleteMaxVisible")).toBe(5);
});
it("should return default when not configured", async () => {
const settings = await Settings.init({ cwd: projectDir, agentDir });
expect(settings.get("autocompleteMaxVisible")).toBe(5);
});
it("should persist and read back a configured value", async () => {
const settings = await Settings.init({ cwd: projectDir, agentDir });
settings.set("autocompleteMaxVisible", 10);
@@ -22,38 +22,6 @@ function createTestToolSession(cwd: string, settings: Settings = Settings.isolat
}
describe("blockImages setting", () => {
describe("Settings", () => {
it("should default blockImages to false", () => {
const settings = Settings.isolated({});
expect(settings.get("images.blockImages")).toBe(false);
});
it("should return true when blockImages is set to true", () => {
const settings = Settings.isolated({ "images.blockImages": true });
expect(settings.get("images.blockImages")).toBe(true);
});
it("should persist blockImages setting via set", () => {
const settings = Settings.isolated({});
expect(settings.get("images.blockImages")).toBe(false);
settings.set("images.blockImages", true);
expect(settings.get("images.blockImages")).toBe(true);
settings.set("images.blockImages", false);
expect(settings.get("images.blockImages")).toBe(false);
});
it("should handle blockImages alongside autoResize", () => {
const settings = Settings.isolated({
"images.autoResize": true,
"images.blockImages": true,
});
expect(settings.get("images.autoResize")).toBe(true);
expect(settings.get("images.blockImages")).toBe(true);
});
});
describe("Read tool", () => {
let testDir: string;
@@ -1,70 +0,0 @@
/**
* Verify the documentation example from hooks.md compiles and works.
*/
import { describe, expect, it } from "bun:test";
import type {
HookAPI,
SessionBeforeCompactEvent,
SessionCompactEvent,
} from "@oh-my-pi/pi-coding-agent/extensibility/hooks";
describe("Documentation example", () => {
it("custom compaction example should type-check correctly", () => {
// This is the example from hooks.md - verify it compiles
const exampleHook = (pi: HookAPI) => {
pi.on("session_before_compact", async (event: SessionBeforeCompactEvent, ctx) => {
// All these should be accessible on the event
const { preparation, branchEntries } = event;
// sessionManager, modelRegistry, and model come from ctx
const { sessionManager, modelRegistry } = ctx;
const { messagesToSummarize, turnPrefixMessages, tokensBefore, firstKeptEntryId, isSplitTurn } =
preparation;
// Verify types
expect(Array.isArray(messagesToSummarize)).toBe(true);
expect(Array.isArray(turnPrefixMessages)).toBe(true);
expect(typeof isSplitTurn).toBe("boolean");
expect(typeof tokensBefore).toBe("number");
expect(typeof sessionManager.getEntries).toBe("function");
expect(typeof modelRegistry.getApiKey).toBe("function");
expect(typeof firstKeptEntryId).toBe("string");
expect(Array.isArray(branchEntries)).toBe(true);
const summary = messagesToSummarize
.filter(m => m.role === "user")
.map(m => `- ${typeof m.content === "string" ? m.content.slice(0, 100) : "[complex]"}`)
.join("\n");
// Hooks return compaction content - SessionManager adds id/parentId
return {
compaction: {
summary: `User requests:\n${summary}`,
firstKeptEntryId,
tokensBefore,
},
};
});
};
// Just verify the function exists and is callable
expect(typeof exampleHook).toBe("function");
});
it("compact event should have correct fields", () => {
const checkCompactEvent = (pi: HookAPI) => {
pi.on("session_compact", async (event: SessionCompactEvent) => {
// These should all be accessible
const entry = event.compactionEntry;
const fromExtension = event.fromExtension;
expect(entry.type).toBe("compaction");
expect(typeof entry.summary).toBe("string");
expect(typeof entry.tokensBefore).toBe("number");
expect(typeof fromExtension).toBe("boolean");
});
};
expect(typeof checkCompactEvent).toBe("function");
});
});
@@ -325,40 +325,6 @@ describe("remote compaction setting", () => {
}
});
it("leaves local summarization requests unattributed when no override is provided", async () => {
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Expected anthropic/claude-sonnet-4-5 model to exist");
const entries: SessionEntry[] = [
createMessageEntry(createUserMessage("Turn 1")),
createMessageEntry(createAssistantMessage("Answer 1", createMockUsage(0, 100, 2000, 0))),
createMessageEntry(createUserMessage("Turn 2")),
createMessageEntry(createAssistantMessage("Answer 2", createMockUsage(0, 100, 5000, 0))),
createMessageEntry(createUserMessage("Turn 3")),
createMessageEntry(createAssistantMessage("Answer 3", createMockUsage(0, 100, 9000, 0))),
];
const preparation = prepareCompaction(entries, {
...DEFAULT_COMPACTION_SETTINGS,
keepRecentTokens: 1000,
remoteEnabled: false,
});
if (!preparation) throw new Error("Expected compaction preparation");
const completeSimpleSpy = vi.spyOn(ai, "completeSimple");
completeSimpleSpy
.mockResolvedValueOnce(createAssistantMessage("History summary"))
.mockResolvedValueOnce(createAssistantMessage("Turn prefix summary"))
.mockResolvedValueOnce(createAssistantMessage("Short summary"));
await compact(preparation, model, "test-api-key");
expect(completeSimpleSpy).toHaveBeenCalledTimes(3);
for (const call of completeSimpleSpy.mock.calls) {
const options = call[2] as { initiatorOverride?: string } | undefined;
expect(options?.initiatorOverride).toBeUndefined();
}
});
it("uses local summarization when remote compaction is disabled", async () => {
const model = getBundledModel("openai", "gpt-4o");
if (!model) {
@@ -906,14 +872,6 @@ describe("buildSessionContext", () => {
// ============================================================================
describe("Large session fixture", () => {
it("should parse the large session", async () => {
const entries = await loadLargeSessionEntries();
expect(entries.length).toBeGreaterThan(100);
const messageCount = entries.filter(e => e.type === "message").length;
expect(messageCount).toBeGreaterThan(100);
});
it("should find cut point in large session", async () => {
const entries = await loadLargeSessionEntries();
const result = findCutPoint(entries, 0, entries.length, DEFAULT_COMPACTION_SETTINGS.keepRecentTokens);
@@ -923,14 +881,6 @@ describe("Large session fixture", () => {
const role = (entries[result.firstKeptEntryIndex] as SessionMessageEntry).message.role;
expect(role === "user" || role === "assistant").toBe(true);
});
it("should load session correctly", async () => {
const entries = await loadLargeSessionEntries();
const loaded = buildSessionContext(entries);
expect(loaded.messages.length).toBeGreaterThan(100);
expect(Object.keys(loaded.models).length).toBeGreaterThan(0);
});
});
// ============================================================================
@@ -29,40 +29,6 @@ afterEach(async () => {
});
describe("config CLI schema coverage", () => {
it("lists non-UI schema settings in JSON output", async () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runConfigCommand({ action: "list", flags: { json: true } });
expect(logSpy).toHaveBeenCalledTimes(1);
const payload = logSpy.mock.calls[0]?.[0];
expect(typeof payload).toBe("string");
const parsed = JSON.parse(String(payload)) as Record<string, { type: string; description: string }>;
expect(parsed.enabledModels).toBeDefined();
expect(parsed.enabledModels.type).toBe("array");
expect(parsed.enabledModels.description).toBe("");
});
it("gets non-UI schema settings by key", async () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runConfigCommand({ action: "get", key: "enabledModels", flags: { json: true } });
expect(logSpy).toHaveBeenCalledTimes(1);
const payload = logSpy.mock.calls[0]?.[0];
expect(typeof payload).toBe("string");
const parsed = JSON.parse(String(payload)) as {
key: string;
type: string;
description: string;
};
expect(parsed.key).toBe("enabledModels");
expect(parsed.type).toBe("array");
expect(parsed.description).toBe("");
});
it("renders record settings as JSON and with record type in text output", async () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
@@ -21,18 +21,6 @@ describe("indentation resolver", () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it("falls back to hard default when settings are not initialized", () => {
expect(getDefaultTabWidth()).toBe(3);
expect(getIndentation()).toBe(3);
});
it("uses configured default tab width from settings", async () => {
const runtimeSettings = await Settings.init({ inMemory: true, cwd: tempDir });
runtimeSettings.set("display.tabWidth", 5);
expect(getDefaultTabWidth()).toBe(5);
expect(getIndentation()).toBe(5);
});
it("applies current display tab width during initial settings load", async () => {
await Settings.init({ inMemory: true, cwd: tempDir, overrides: { "display.tabWidth": 7 } });
expect(getDefaultTabWidth()).toBe(7);
@@ -272,15 +272,4 @@ describe("executeJs", () => {
expect(result.exitCode).toBe(0);
expect(result.output.trim()).toBe(path.join("a", "b"));
});
it("exposes a cwd-bound `require` and `createRequire`", async () => {
const result = await executeJs(
'return { hasRequire: typeof require === "function", hasCreate: typeof createRequire === "function", path: require("node:path").sep };',
{ sessionId, session, sessionFile },
);
expect(result.exitCode).toBe(0);
expect(result.displayOutputs).toEqual([
{ type: "json", data: { hasRequire: true, hasCreate: true, path: path.sep } },
]);
});
});
@@ -41,18 +41,4 @@ describe("Python gateway environment filtering", () => {
expect(filtered.LC_CTYPE).toBe("UTF-8");
expect(filtered.LC_MESSAGES).toBe("en_US.UTF-8");
});
it("passes filtered env through to resolved runtime", () => {
const env: Record<string, string | undefined> = {
PATH: "/usr/bin",
HOME: "/home/test",
OPENAI_API_KEY: "secret",
PI_DEBUG: "1",
};
const filtered = filterEnv(env);
expect(filtered.OPENAI_API_KEY).toBeUndefined();
expect(filtered.PATH).toBe("/usr/bin");
expect(filtered.PI_DEBUG).toBe("1");
});
});
@@ -1,73 +0,0 @@
import { describe, expect, it } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { EvalTool } from "@oh-my-pi/pi-coding-agent/tools/eval";
import { $which, getProjectDir } from "@oh-my-pi/pi-utils";
const resolvePythonPath = (): string | null => {
const venvPath = Bun.env.VIRTUAL_ENV;
const candidates = [venvPath, path.join(getProjectDir(), ".venv"), path.join(getProjectDir(), "venv")].filter(
Boolean,
) as string[];
for (const candidate of candidates) {
const binDir = process.platform === "win32" ? "Scripts" : "bin";
const exeName = process.platform === "win32" ? "python.exe" : "python";
const pythonCandidate = path.join(candidate, binDir, exeName);
if (fs.existsSync(pythonCandidate)) {
return pythonCandidate;
}
}
return $which("python") ?? $which("python3");
};
const pythonPath = resolvePythonPath();
const hasKernelDeps = (() => {
if (!pythonPath) return false;
const result = Bun.spawnSync(
[
pythonPath,
"-c",
"import importlib.util,sys;sys.exit(0 if importlib.util.find_spec('kernel_gateway') and importlib.util.find_spec('ipykernel') else 1)",
],
{ stdin: "ignore", stdout: "pipe", stderr: "pipe" },
);
return result.exitCode === 0;
})();
const shouldRun = Boolean(pythonPath) && hasKernelDeps;
describe.skipIf(!shouldRun)("PYTHON_PRELUDE integration", () => {
it("exposes prelude helpers via eval python backend", async () => {
const helpers = ["env", "read", "write", "append", "tree", "diff", "run", "output"];
const session = {
cwd: getProjectDir(),
hasUI: false,
getSessionFile: () => null,
getSessionSpawns: () => null,
settings: Settings.isolated({
"lsp.diagnosticsOnWrite": false,
"eval.py": true,
"python.kernelMode": "per-call",
"python.sharedGateway": true,
}),
};
const tool = new EvalTool(session);
const code = [
`helpers = ${JSON.stringify(helpers)}`,
"missing = [name for name in helpers if name not in globals() or not callable(globals()[name])]",
'print("HELPERS_OK=" + ("1" if not missing else "0"))',
"if missing:",
' print("MISSING=" + ",".join(missing))',
].join("\n");
const result = await tool.execute("tool-call-1", {
input: `*** Begin PY\n*** Title: prelude helpers\n${code}\n*** End PY\n`,
});
const output = result.content.find(item => item.type === "text")?.text ?? "";
expect(output).toContain("HELPERS_OK=1");
expect(tool.description).toContain("read");
expect(tool.description).not.toContain("Documentation unavailable");
});
});
@@ -1,32 +0,0 @@
import { describe, expect, test } from "bun:test";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
describe("cycleOrder with custom roles", () => {
test("cycleOrder setting accepts custom role names", () => {
const settings = Settings.isolated({
cycleOrder: ["smol", "custom-fast", "default"],
});
expect(settings.get("cycleOrder")).toEqual(["smol", "custom-fast", "default"]);
});
test("cycleOrder falls back to default when not set", () => {
const settings = Settings.isolated({});
expect(settings.get("cycleOrder")).toEqual(["smol", "default", "slow"]);
});
test("modelTags can define custom role display info", () => {
const settings = Settings.isolated({
modelTags: {
"custom-fast": {
name: "Fast Custom",
color: "warning",
},
},
});
const modelTags = settings.get("modelTags") as Record<string, any>;
expect(modelTags["custom-fast"]).toEqual({
name: "Fast Custom",
color: "warning",
});
});
});
@@ -19,12 +19,6 @@ describe("DebugLogViewerModel", () => {
return row.kind;
}
};
it("defaults cursor to the newest log entry", () => {
const logs = ["alpha", "beta", "gamma"].join("\n");
const model = new DebugLogViewerModel(logs, { processStartMs: Date.now() });
expect(model.cursorLogIndex).toBe(2);
});
it("inserts session boundary warning between older and current-session logs", () => {
const processStartMs = Date.parse("2026-02-14T12:00:00.000Z");
const logs = [
@@ -33,24 +33,6 @@ describe("dropIncompleteLastEdit", () => {
});
});
describe("apply_patch extractCompleteEdits", () => {
const strategy = EDIT_MODE_STRATEGIES.apply_patch;
test("returns args unchanged (payload is plain text)", () => {
const args = { input: "*** Begin Patch\n*** Update File: a.ts\n@@\n-x\n+y\n*** End Patch\n" };
expect(strategy.extractCompleteEdits(args, undefined)).toEqual(args);
});
});
describe("vim extractCompleteEdits", () => {
const strategy = EDIT_MODE_STRATEGIES.vim;
test("returns args unchanged (vim stream handled elsewhere)", () => {
const args = { file: "a.ts", steps: [] };
expect(strategy.extractCompleteEdits(args, undefined)).toEqual(args);
});
});
describe("hashline streaming preview (multi-section)", () => {
const strategy = EDIT_MODE_STRATEGIES.hashline;
let tmpDir: string;
@@ -25,12 +25,6 @@ function createApi(): CustomCommandAPI {
}
describe("GreenCommand", () => {
it("exposes the /green command name", () => {
const command = new GreenCommand(createApi());
expect(command.name).toBe("green");
});
it("includes tag instructions when HEAD has a tag", async () => {
vi.spyOn(git.ref, "tags").mockResolvedValue(["v0.1.0-alpha2"]);
const command = new GreenCommand(createApi());
@@ -1,89 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { isHindsightConfigured, loadHindsightConfig } from "@oh-my-pi/pi-coding-agent/hindsight/config";
describe("loadHindsightConfig", () => {
beforeEach(() => {
_resetSettingsForTest();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("returns sane defaults from an empty Settings", () => {
const settings = Settings.isolated();
const cfg = loadHindsightConfig(settings, {});
expect(cfg.hindsightApiUrl).toBe("http://localhost:8888"); // schema default
expect(cfg.recallBudget).toBe("mid");
expect(cfg.retainMode).toBe("full-session");
expect(cfg.recallTypes).toEqual(["world", "experience"]);
expect(cfg.autoRecall).toBe(true);
expect(cfg.autoRetain).toBe(true);
expect(cfg.scoping).toBe("per-project-tagged");
});
it("env overrides win over settings", () => {
const settings = Settings.isolated({
"hindsight.apiUrl": "http://settings.example",
"hindsight.autoRecall": true,
"hindsight.recallMaxTokens": 256,
"hindsight.scoping": "global",
"hindsight.retainMode": "full-session",
});
const cfg = loadHindsightConfig(settings, {
HINDSIGHT_API_URL: "http://env.example",
HINDSIGHT_AUTO_RECALL: "false",
HINDSIGHT_RECALL_MAX_TOKENS: "9999",
HINDSIGHT_SCOPING: "per-project",
HINDSIGHT_RETAIN_MODE: "last-turn",
});
expect(cfg.hindsightApiUrl).toBe("http://env.example");
expect(cfg.autoRecall).toBe(false);
expect(cfg.recallMaxTokens).toBe(9999);
expect(cfg.scoping).toBe("per-project");
expect(cfg.retainMode).toBe("last-turn");
});
it("ignores invalid scoping values and falls back to the schema default", () => {
const settings = Settings.isolated();
const cfg = loadHindsightConfig(settings, { HINDSIGHT_SCOPING: "garbage" });
expect(cfg.scoping).toBe("per-project-tagged");
});
it("ignores invalid retainMode env values and falls back to schema default", () => {
const settings = Settings.isolated();
const cfg = loadHindsightConfig(settings, { HINDSIGHT_RETAIN_MODE: "garbage" });
expect(cfg.retainMode).toBe("full-session");
});
it("coerces non-numeric ints back to undefined so settings/default takes over", () => {
const settings = Settings.isolated({ "hindsight.recallMaxTokens": 512 });
const cfg = loadHindsightConfig(settings, { HINDSIGHT_RECALL_MAX_TOKENS: "not-a-number" });
expect(cfg.recallMaxTokens).toBe(512);
});
it("respects falsy boolean env strings", () => {
const settings = Settings.isolated();
const cfg = loadHindsightConfig(settings, {
HINDSIGHT_AUTO_RECALL: "no",
HINDSIGHT_AUTO_RETAIN: "0",
});
expect(cfg.autoRecall).toBe(false);
expect(cfg.autoRetain).toBe(false);
});
});
describe("isHindsightConfigured", () => {
it("returns true when an apiUrl is set", () => {
const cfg = loadHindsightConfig(Settings.isolated({ "hindsight.apiUrl": "http://x" }), {});
expect(isHindsightConfigured(cfg)).toBe(true);
});
it("returns false when apiUrl is missing", () => {
const cfg = loadHindsightConfig(Settings.isolated({ "hindsight.apiUrl": "" }), {
HINDSIGHT_API_URL: "",
});
expect(isHindsightConfigured(cfg)).toBe(false);
});
});
@@ -3,10 +3,6 @@ import { describe, expect, it } from "bun:test";
describe("Buffer.toBase64", async () => {
await import("../src/utils/image-resize");
it("should be defined", () => {
expect(Buffer.prototype.toBase64).toBeDefined();
});
it("should return a base64 string", () => {
const buffer = Buffer.from("Hello, world!");
expect(buffer.toBase64()).toBe("SGVsbG8sIHdvcmxkIQ==");
@@ -7,16 +7,6 @@ describe("ensureSupportedImageInput", () => {
vi.restoreAllMocks();
});
test("returns supported image input unchanged", async () => {
const convertToPngSpy = vi.spyOn(imageConvert, "convertToPng");
const input = { type: "image" as const, data: "abc", mimeType: "image/png" };
const result = await ensureSupportedImageInput(input);
expect(result).toEqual(input);
expect(convertToPngSpy).not.toHaveBeenCalled();
});
test("converts unsupported image input to png", async () => {
const convertToPngSpy = vi
.spyOn(imageConvert, "convertToPng")
@@ -54,20 +54,4 @@ describe("ModelRegistry LM Studio Fixes", () => {
expect(available.some(m => m.provider === "ollama")).toBe(true);
expect(available.some(m => m.provider === "lm-studio")).toBe(true);
});
test("lm-studio discovery handles trailing slashes in baseUrl correctly", async () => {
let _requestedUrl = "";
using _hook = hookFetch(input => {
const url = String(input);
// Only track URLs from our test endpoints; ignore concurrent built-in provider discovery
if (url.includes("127.0.0.1:1234") || url.includes("127.0.0.1:9999") || url.startsWith("not a url")) {
_requestedUrl = url;
return new Response(JSON.stringify({ data: [{ id: "model-1" }] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response(null, { status: 404 });
});
});
});
@@ -62,17 +62,6 @@ describe("isValidVersionForCache", () => {
// ── getCachedPluginPath ──────────────────────────────────────────────────────
describe("getCachedPluginPath", () => {
it("returns a deterministic path with ___ separators", () => {
const p = getCachedPluginPath("/cache", "my-market", "my-plugin", "1.0.0");
expect(p).toBe("/cache/my-market___my-plugin___1.0.0");
});
it("is independent of cacheDir content — pure path construction", () => {
const p1 = getCachedPluginPath("/a", "m", "p", "1");
const p2 = getCachedPluginPath("/b", "m", "p", "1");
expect(path.basename(p1)).toBe(path.basename(p2));
});
it("throws on invalid marketplace name (uppercase)", () => {
expect(() => getCachedPluginPath("/cache", "My-Market", "plugin", "1.0.0")).toThrow(/Invalid marketplace name/);
});
@@ -195,13 +195,6 @@ describe("MarketplaceManager", () => {
);
});
it("installPlugin calls clearPluginRootsCache", async () => {
await ctx.manager.addMarketplace(FIXTURE_DIR);
const before = ctx.clearCount();
await ctx.manager.installPlugin("hello-plugin", "test-marketplace");
expect(ctx.clearCount()).toBe(before + 1);
});
// ── Uninstall ──────────────────────────────────────────────────────────
it("uninstallPlugin → cache removed + deregistered", async () => {
@@ -224,14 +217,6 @@ describe("MarketplaceManager", () => {
await expect(ctx.manager.uninstallPlugin("no-at-sign")).rejects.toThrow(/Invalid plugin ID format/);
});
it("uninstallPlugin calls clearPluginRootsCache", async () => {
await ctx.manager.addMarketplace(FIXTURE_DIR);
await ctx.manager.installPlugin("hello-plugin", "test-marketplace");
const before = ctx.clearCount();
await ctx.manager.uninstallPlugin("hello-plugin@test-marketplace");
expect(ctx.clearCount()).toBe(before + 1);
});
// ── setPluginEnabled ───────────────────────────────────────────────────
it("setPluginEnabled → persisted in registry", async () => {
@@ -252,14 +237,6 @@ describe("MarketplaceManager", () => {
await expect(ctx.manager.setPluginEnabled("ghost@nowhere", true)).rejects.toThrow(/not installed/);
});
it("setPluginEnabled calls clearPluginRootsCache", async () => {
await ctx.manager.addMarketplace(FIXTURE_DIR);
await ctx.manager.installPlugin("hello-plugin", "test-marketplace");
const before = ctx.clearCount();
await ctx.manager.setPluginEnabled("hello-plugin@test-marketplace", false);
expect(ctx.clearCount()).toBe(before + 1);
});
// ── version fallback ───────────────────────────────────────────────────
it("installPlugin falls back to plugin.json version when catalog version is missing", async () => {
@@ -191,14 +191,6 @@ describe("parseInternalUrl — protocol field", () => {
expect(parseInternalUrl("local://x").protocol).toBe("local:");
});
it("extracts rule: protocol", () => {
expect(parseInternalUrl("rule://x").protocol).toBe("rule:");
});
it("extracts artifact: protocol", () => {
expect(parseInternalUrl("artifact://x").protocol).toBe("artifact:");
});
it("extracts protocol from fallback-parsed URL", () => {
// This URL fails new URL() due to colon-as-port
expect(parseInternalUrl("skill://a:b").protocol).toBe("skill:");
@@ -1,53 +0,0 @@
import { describe, expect, it } from "bun:test";
import { buildPluginDirRoot } from "@oh-my-pi/pi-coding-agent/discovery/plugin-dir-roots";
describe("buildPluginDirRoot", () => {
it("builds root with manifest name", () => {
const root = buildPluginDirRoot("/path/to/my-plugin", "custom-name");
expect(root).toEqual({
id: "custom-name@__local__",
marketplace: "__local__",
plugin: "custom-name",
version: "local",
path: "/path/to/my-plugin",
scope: "user",
});
});
it("falls back to directory basename when no manifest name", () => {
const root = buildPluginDirRoot("/path/to/my-plugin");
expect(root.plugin).toBe("my-plugin");
expect(root.id).toBe("my-plugin@__local__");
});
it("falls back to directory basename when manifest name is undefined", () => {
const root = buildPluginDirRoot("/some/dir/cool-plugin", undefined);
expect(root.plugin).toBe("cool-plugin");
expect(root.id).toBe("cool-plugin@__local__");
});
it("uses __local__ marketplace", () => {
const root = buildPluginDirRoot("/any/path", "test");
expect(root.marketplace).toBe("__local__");
});
it("uses local version string", () => {
const root = buildPluginDirRoot("/any/path", "test");
expect(root.version).toBe("local");
});
it("sets scope to user", () => {
const root = buildPluginDirRoot("/any/path", "test");
expect(root.scope).toBe("user");
});
it("preserves absolute path", () => {
const root = buildPluginDirRoot("/absolute/path/to/plugin", "test");
expect(root.path).toBe("/absolute/path/to/plugin");
});
it("constructs id as pluginName@__local__", () => {
const root = buildPluginDirRoot("/p", "my-tool");
expect(root.id).toBe("my-tool@__local__");
});
});
@@ -103,14 +103,6 @@ describe("roots response shape", () => {
expect(result.roots[0].name).toBe("project");
});
it("produces valid file:// URI on Windows-style paths", () => {
// path.basename and pathToFileURL are platform-dependent for
// Windows paths; only assert the URI format, not the name.
const result = getRoots("C:\\Users\\dev\\myproject");
expect(result.roots[0].uri).toMatch(/^file:\/\/\//);
expect(result.roots[0].name).toBeTruthy();
});
it("handles paths with spaces", () => {
const result = getRoots("/home/user/my project");
expect(result.roots[0].uri).toContain("my%20project");
@@ -11,16 +11,6 @@ describe("resolveMemoryBackend", () => {
_resetSettingsForTest();
});
it("returns the off backend when memory.backend is off", () => {
const settings = Settings.isolated({ "memory.backend": "off" });
expect(resolveMemoryBackend(settings).id).toBe("off");
});
it("returns the local backend when memory.backend is local", () => {
const settings = Settings.isolated({ "memory.backend": "local", "memories.enabled": false });
expect(resolveMemoryBackend(settings).id).toBe("local");
});
it("returns the hindsight backend when memory.backend is hindsight, regardless of legacy memories.enabled", () => {
const a = Settings.isolated({ "memory.backend": "hindsight", "memories.enabled": false });
const b = Settings.isolated({ "memory.backend": "hindsight", "memories.enabled": true });
@@ -95,15 +95,6 @@ describe("ModelRegistry runtime provider registration", () => {
expect(registry.find(providerName, modelId)?.headers?.[headerName]).toBe(headerValue);
}
test("loads built-in GitLab Duo models and OAuth provider metadata", () => {
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const model = registry.find("gitlab-duo", "claude-sonnet-4-5-20250929");
expect(model).toBeDefined();
expect(model?.api).toBe("anthropic-messages");
expect(getOAuthProviders().some(provider => provider.id === "gitlab-duo")).toBe(true);
});
test("validates provider config before mutating custom API state", () => {
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const beforeAnthropicCount = registry.getAll().filter(model => model.provider === "anthropic").length;
@@ -125,27 +116,6 @@ describe("ModelRegistry runtime provider registration", () => {
expect(afterAnthropicCount).toBe(beforeAnthropicCount);
});
test("merges provider/model headers and adds Authorization when authHeader is enabled", () => {
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const config: ProviderConfigInput = {
baseUrl: "https://runtime.example.com/v1",
apiKey: "RUNTIME_KEY",
api: "openai-completions",
authHeader: true,
headers: { "X-Provider": "provider-header" },
models: [{ ...baseModel, headers: { "X-Model": "model-header" } }],
};
registry.registerProvider("runtime-provider", config, "ext://runtime");
const model = registry.find("runtime-provider", "runtime-model");
expect(model).toBeDefined();
expect(model?.headers?.Authorization).toBe("Bearer RUNTIME_KEY");
expect(model?.headers?.["X-Provider"]).toBe("provider-header");
expect(model?.headers?.["X-Model"]).toBe("model-header");
});
test("registerProvider applies headers-only overrides to existing provider models across refresh", async () => {
const registry = new ModelRegistry(authStorage, modelsJsonPath);
const providerName = "anthropic";
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { Effort, type Model, type OpenAICompat, type ThinkingConfig, writeModelCache } from "@oh-my-pi/pi-ai";
import { kNoAuth, MODEL_ROLES, ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { kNoAuth, ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { hookFetch, Snowflake } from "@oh-my-pi/pi-utils";
@@ -14,11 +14,6 @@ describe("ModelRegistry", () => {
let cacheDbPath: string;
let authStorage: AuthStorage;
test("commit role includes a visible badge tag", () => {
expect(MODEL_ROLES.commit.tag).toBe("COMMIT");
expect(MODEL_ROLES.commit.color).toBe("dim");
});
beforeEach(async () => {
_resetSettingsForTest();
tempDir = path.join(os.tmpdir(), `pi-test-model-registry-${Snowflake.next()}`);
@@ -56,44 +56,6 @@ describe("ModelSelector role badge thinking display", () => {
}
});
test("renders per-role thinking labels with inherit mode to avoid badge ambiguity", async () => {
installTestTheme();
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Expected bundled model anthropic/claude-sonnet-4-5");
const settings = Settings.isolated({
modelRoles: {
default: `${model.provider}/${model.id}`,
smol: `${model.provider}/${model.id}:minimal`,
slow: `${model.provider}/${model.id}`,
plan: `${model.provider}/${model.id}:high`,
commit: `${model.provider}/${model.id}:medium`,
},
});
const selector = createSelector(model, settings);
await Bun.sleep(0);
installTestTheme();
const rendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(rendered).toContain("DEFAULT (inherit)");
expect(rendered).toContain("SMOL (min)");
expect(rendered).toContain("SLOW (inherit)");
expect(rendered).toContain("PLAN (high)");
expect(rendered).toContain("COMMIT (medium)");
expect(rendered).not.toContain("Role Thinking:");
selector.handleInput("\n");
installTestTheme();
const menuRendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(menuRendered).toContain("Set as DEFAULT (Default)");
expect(menuRendered).toContain("Set as SMOL (Fast)");
expect(menuRendered).toContain("Set as SLOW (Thinking)");
expect(menuRendered).toContain("Set as PLAN (Architect)");
expect(menuRendered).toContain("Set as COMMIT (Commit)");
});
test("shows custom roles from cycleOrder/modelRoles and honors built-in metadata overrides", async () => {
installTestTheme();
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
@@ -85,29 +85,6 @@ describe("BtwController", () => {
expect(controller.hasActiveRequest()).toBe(true);
});
it("streams text deltas through onTextDelta into the panel", async () => {
const deltas: string[] = [];
const runEphemeralTurn = vi.fn(async (args: RunEphemeralTurnArgs) => {
args.onTextDelta?.("Hel");
args.onTextDelta?.("lo");
return { replyText: "Hello", assistantMessage: createAssistantMessage("Hello") };
});
const ctx = makeCtx(makeFakeSession(runEphemeralTurn));
const controller = new BtwController(ctx);
await controller.start("Hi?");
await Promise.resolve();
await Promise.resolve();
// Use the captured deltas to verify the callback is wired through.
const callArg = runEphemeralTurn.mock.calls[0]?.[0];
expect(callArg).toBeDefined();
callArg?.onTextDelta?.("X");
deltas.push("X");
expect(deltas).toEqual(["X"]);
expect(controller.hasActiveRequest()).toBe(true);
});
it("replaces a previous request by aborting it before issuing the next runEphemeralTurn", async () => {
const signals: AbortSignal[] = [];
let firstRelease!: () => void;
@@ -1,11 +1,11 @@
import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
import type { TextContent, UserMessage } from "@oh-my-pi/pi-ai";
import { Container } from "@oh-my-pi/pi-tui";
import { EventController } from "@oh-my-pi/pi-coding-agent/modes/controllers/event-controller";
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types";
import { UiHelpers } from "@oh-my-pi/pi-coding-agent/modes/utils/ui-helpers";
import type { CustomMessage } from "@oh-my-pi/pi-coding-agent/session/messages";
import { Container } from "@oh-my-pi/pi-tui";
beforeAll(() => {
initTheme();
@@ -1,38 +0,0 @@
import { afterEach, describe, expect, it } from "bun:test";
import { getThemeByName } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { renderOutputBlock } from "@oh-my-pi/pi-coding-agent/tui/output-block";
import { ImageProtocol, TERMINAL } from "@oh-my-pi/pi-tui";
type MutableTerminalInfo = {
imageProtocol: ImageProtocol | null;
};
const terminal = TERMINAL as unknown as MutableTerminalInfo;
describe("renderOutputBlock", () => {
const originalProtocol = TERMINAL.imageProtocol;
afterEach(() => {
terminal.imageProtocol = originalProtocol;
});
it("passes SIXEL lines through without trimming or padding", async () => {
terminal.imageProtocol = ImageProtocol.Sixel;
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const sixel = "\x1bPqabc\x1b\\";
const lines = renderOutputBlock(
{
width: 40,
sections: [{ label: "Output", lines: ["regular line", sixel] }],
},
uiTheme,
);
expect(lines.filter(line => line === sixel)).toHaveLength(1);
const regularLine = lines.find(line => line.includes("regular line"));
expect(regularLine).toBeDefined();
expect(regularLine).not.toBe("regular line");
});
});
@@ -1,14 +0,0 @@
import { describe, expect, it } from "bun:test";
import { prompt } from "@oh-my-pi/pi-utils";
import planModeApprovedPrompt from "../../src/prompts/system/plan-mode-approved.md" with { type: "text" };
describe("plan-mode-approved prompt", () => {
it("includes final plan artifact path in injected execution prompt", () => {
const rendered = prompt.render(planModeApprovedPrompt, {
planContent: "1. Do work",
finalPlanFilePath: "local://WP_MIGRATION_PLAN.md",
});
expect(rendered).toContain("local://WP_MIGRATION_PLAN.md");
});
});
@@ -9,12 +9,6 @@ const TEST_CONFIG: CliConfig = {
};
describe("Plugin command scope parsing", () => {
it("accepts project scope", async () => {
const command = new Plugin(["install", "--scope", "project"], TEST_CONFIG);
const { flags } = await command.parse(Plugin);
expect(flags.scope).toBe("project");
});
it("rejects invalid scope values", async () => {
const command = new Plugin(["install", "--scope", "porject"], TEST_CONFIG);
await expect(command.parse(Plugin)).rejects.toThrow(/Expected --scope to be one of: user, project/);
@@ -18,14 +18,6 @@ import { parseCommandArgs, substituteArgs } from "@oh-my-pi/pi-coding-agent/util
// ============================================================================
describe("substituteArgs", () => {
test("should replace $ARGUMENTS with all args joined", () => {
expect(substituteArgs("Test: $ARGUMENTS", ["a", "b", "c"])).toBe("Test: a b c");
});
test("should replace $@ with all args joined", () => {
expect(substituteArgs("Test: $@", ["a", "b", "c"])).toBe("Test: a b c");
});
test("should support $@ slicing with start offset", () => {
expect(substituteArgs("Test: $@[2]", ["a", "b", "c"])).toBe("Test: b c");
});
@@ -66,18 +58,6 @@ describe("substituteArgs", () => {
expect(substituteArgs("$1: $@", ["prefix", "a", "b"])).toBe("prefix: prefix a b");
});
test("should handle empty arguments array with $ARGUMENTS", () => {
expect(substituteArgs("Test: $ARGUMENTS", [])).toBe("Test: ");
});
test("should handle empty arguments array with $@", () => {
expect(substituteArgs("Test: $@", [])).toBe("Test: ");
});
test("should handle empty arguments array with $1", () => {
expect(substituteArgs("Test: $1", [])).toBe("Test: ");
});
test("should handle multiple occurrences of $ARGUMENTS", () => {
expect(substituteArgs("$ARGUMENTS and $ARGUMENTS", ["a", "b"])).toBe("a b and a b");
});
@@ -116,14 +96,6 @@ describe("substituteArgs", () => {
expect(substituteArgs("$ARGUMENTS", ["first arg", "second arg"])).toBe("first arg second arg");
});
test("should handle single argument with $ARGUMENTS", () => {
expect(substituteArgs("Test: $ARGUMENTS", ["only"])).toBe("Test: only");
});
test("should handle single argument with $@", () => {
expect(substituteArgs("Test: $@", ["only"])).toBe("Test: only");
});
test("should handle $0 (zero index)", () => {
expect(substituteArgs("$0", ["a", "b"])).toBe("");
});
@@ -140,49 +112,16 @@ describe("substituteArgs", () => {
expect(substituteArgs("pre$@", ["a", "b"])).toBe("prea b");
});
test("should handle empty arguments in middle of list", () => {
expect(substituteArgs("$ARGUMENTS", ["a", "", "c"])).toBe("a c");
});
test("should handle trailing and leading spaces in arguments", () => {
expect(substituteArgs("$ARGUMENTS", [" leading ", "trailing "])).toBe(" leading trailing ");
});
test("should handle argument containing pattern partially", () => {
expect(substituteArgs("Prefix $ARGUMENTS suffix", ["ARGUMENTS"])).toBe("Prefix ARGUMENTS suffix");
});
test("should handle non-matching patterns", () => {
expect(substituteArgs("$A $$ $ $ARGS", ["a"])).toBe("$A $$ $ $ARGS");
});
test("should handle case variations (case-sensitive)", () => {
expect(substituteArgs("$arguments $Arguments $ARGUMENTS", ["a", "b"])).toBe("$arguments $Arguments a b");
});
test("should handle both syntaxes in same command with same result", () => {
const args = ["x", "y", "z"];
const result1 = substituteArgs("$@ and $ARGUMENTS", args);
const result2 = substituteArgs("$ARGUMENTS and $@", args);
expect(result1).toBe(result2);
expect(result1).toBe("x y z and x y z");
});
test("should handle very long argument lists", () => {
const args = Array.from({ length: 100 }, (_, i) => `arg${i}`);
const result = substituteArgs("$ARGUMENTS", args);
expect(result).toBe(args.join(" "));
});
test("should handle numbered placeholders with single digit", () => {
expect(substituteArgs("$1 $2 $3", ["a", "b", "c"])).toBe("a b c");
});
test("should handle numbered placeholders with multiple digits", () => {
const args = Array.from({ length: 15 }, (_, i) => `val${i}`);
expect(substituteArgs("$10 $12 $15", args)).toBe("val9 val11 val14");
});
test("should handle escaped dollar signs (literal backslash preserved)", () => {
// Note: No escape mechanism exists - backslash is treated literally
expect(substituteArgs("Price: \\$100", [])).toBe("Price: \\");
@@ -257,14 +196,6 @@ describe("parseCommandArgs", () => {
// Note: This implementation doesn't handle escaped quotes - backslash is literal
expect(parseCommandArgs('"quoted \\"text\\""')).toEqual(["quoted \\text\\"]);
});
test("should handle trailing spaces", () => {
expect(parseCommandArgs("a b c ")).toEqual(["a", "b", "c"]);
});
test("should handle leading spaces", () => {
expect(parseCommandArgs(" a b c")).toEqual(["a", "b", "c"]);
});
});
// ============================================================================
@@ -1,21 +0,0 @@
import { describe, expect, it } from "bun:test";
describe("review request prompt", () => {
it("renders the additional instructions block from the template", async () => {
const template = await Bun.file(new URL("../../src/prompts/review-request.md", import.meta.url)).text();
expect(template).toContain("{{#if additionalInstructions}}");
expect(template).toContain("### Additional Instructions");
expect(template).toContain("{{additionalInstructions}}");
expect(template).toContain("{{/if}}");
});
it("keeps the additional instructions suffix out of TypeScript", async () => {
const source = await Bun.file(
new URL("../../src/extensibility/custom-commands/bundled/review/index.ts", import.meta.url),
).text();
expect(source).not.toContain("### Additional Instructions");
expect(source).not.toContain("appendInstructions(");
});
});
@@ -1,82 +0,0 @@
import { describe, expect, it } from "bun:test";
import { type PendingExtensionRequest, requestRpcEditor } from "@oh-my-pi/pi-coding-agent/modes/rpc/rpc-mode";
import type { RpcExtensionUIRequest } from "@oh-my-pi/pi-coding-agent/modes/rpc/rpc-types";
function isExtensionUiRequest(obj: RpcExtensionUIRequest | object): obj is RpcExtensionUIRequest {
return "type" in obj && obj.type === "extension_ui_request";
}
describe("requestRpcEditor", () => {
it("serializes promptStyle on editor requests", async () => {
const pendingRequests = new Map<string, PendingExtensionRequest>();
const requests: RpcExtensionUIRequest[] = [];
const promise = requestRpcEditor(
pendingRequests,
obj => {
if (isExtensionUiRequest(obj)) {
requests.push(obj);
}
},
"Enter your response:",
"draft",
undefined,
{ promptStyle: true },
);
expect(requests).toHaveLength(1);
const request = requests[0];
if (!request || request.method !== "editor") {
throw new Error("Expected an editor request");
}
expect(request.promptStyle).toBe(true);
expect(request.prefill).toBe("draft");
const pending = pendingRequests.get(request.id);
if (!pending) {
throw new Error("Expected a pending request");
}
pending.resolve({ type: "extension_ui_response", id: request.id, value: "custom response" });
await expect(promise).resolves.toBe("custom response");
expect(pendingRequests.size).toBe(0);
});
it("resolves editor requests on abort and clears pending state", async () => {
const pendingRequests = new Map<string, PendingExtensionRequest>();
const requests: RpcExtensionUIRequest[] = [];
const controller = new AbortController();
const promise = requestRpcEditor(
pendingRequests,
obj => {
if (isExtensionUiRequest(obj)) {
requests.push(obj);
}
},
"Enter your response:",
undefined,
{ signal: controller.signal },
{ promptStyle: true },
);
expect(requests).toHaveLength(1);
const request = requests[0];
if (!request || request.method !== "editor") {
throw new Error("Expected an editor request");
}
expect(request.promptStyle).toBe(true);
expect(pendingRequests.has(request.id)).toBe(true);
controller.abort();
expect(requests).toHaveLength(2);
const cancelRequest = requests[1];
if (!cancelRequest || cancelRequest.method !== "cancel") {
throw new Error("Expected a cancel request");
}
expect(cancelRequest.targetId).toBe(request.id);
await expect(promise).resolves.toBeUndefined();
expect(pendingRequests.has(request.id)).toBe(false);
});
});
@@ -7,12 +7,6 @@ import { SecretObfuscator } from "../src/secrets/obfuscator";
import { compileSecretRegex } from "../src/secrets/regex";
describe("compileSecretRegex", () => {
it("compiles pattern with explicit flags and enforces global scanning", () => {
const regex = compileSecretRegex("api[_-]?key\\s*=\\s*\\w+", "gi");
expect(regex.source).toBe("api[_-]?key\\s*=\\s*\\w+");
expect(regex.flags).toBe("gi");
});
it("adds global flag when not provided", () => {
const regex = compileSecretRegex("api[_-]?key\\s*=\\s*\\w+", "i");
expect(regex.source).toBe("api[_-]?key\\s*=\\s*\\w+");
@@ -1,19 +0,0 @@
import { describe, expect, it } from "bun:test";
import { getSessionAccentHex } from "../src/utils/session-color";
import { formatSessionTerminalTitle } from "../src/utils/title-generator";
describe("getSessionAccentHex", () => {
it("returns a stable hex for the same name", () => {
expect(getSessionAccentHex("Named session")).toBe(getSessionAccentHex("Named session"));
});
});
describe("formatSessionTerminalTitle", () => {
it("uses the session name when present", () => {
expect(formatSessionTerminalTitle("Manual title", "/work/pi")).toBe("π: Manual title");
});
it("falls back to the cwd basename when the session name is missing", () => {
expect(formatSessionTerminalTitle(undefined, "/work/pi")).toBe("π: pi");
});
});
@@ -24,29 +24,6 @@ describe("loadEntriesFromFile", () => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("returns empty array for non-existent file", async () => {
const entries = await loadEntriesFromFile(path.join(tempDir, "nonexistent.jsonl"));
expect(entries).toEqual([]);
});
it("returns empty array for empty file", async () => {
const file = path.join(tempDir, "empty.jsonl");
fs.writeFileSync(file, "");
expect(await loadEntriesFromFile(file)).toEqual([]);
});
it("returns empty array for file without valid session header", async () => {
const file = path.join(tempDir, "no-header.jsonl");
fs.writeFileSync(file, '{"type":"message","id":"1"}\n');
expect(await loadEntriesFromFile(file)).toEqual([]);
});
it("returns empty array for malformed JSON", async () => {
const file = path.join(tempDir, "malformed.jsonl");
fs.writeFileSync(file, "not json\n");
expect(await loadEntriesFromFile(file)).toEqual([]);
});
it("loads valid session file", async () => {
const file = path.join(tempDir, "valid.jsonl");
fs.writeFileSync(
@@ -85,25 +62,6 @@ describe("findMostRecentSession", () => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("returns null for empty directory", async () => {
expect(await findMostRecentSession(tempDir)).toBeNull();
});
it("returns null for non-existent directory", async () => {
expect(await findMostRecentSession(path.join(tempDir, "nonexistent"))).toBeNull();
});
it("ignores non-jsonl files", async () => {
fs.writeFileSync(path.join(tempDir, "file.txt"), "hello");
fs.writeFileSync(path.join(tempDir, "file.json"), "{}");
expect(await findMostRecentSession(tempDir)).toBeNull();
});
it("ignores jsonl files without valid session header", async () => {
fs.writeFileSync(path.join(tempDir, "invalid.jsonl"), '{"type":"message"}\n');
expect(await findMostRecentSession(tempDir)).toBeNull();
});
it("returns single valid session file", async () => {
const file = path.join(tempDir, "session.jsonl");
fs.writeFileSync(file, '{"type":"session","id":"abc","timestamp":"2025-01-01T00:00:00Z","cwd":"/tmp"}\n');
@@ -1,35 +0,0 @@
import { describe, expect, it } from "bun:test";
import { getProviderDetails, type Model } from "@oh-my-pi/pi-ai";
import { renderProviderSection } from "@oh-my-pi/pi-coding-agent/modes/controllers/command-controller";
describe("session provider section", () => {
it("renders codex provider details with transport fields", () => {
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const details = getProviderDetails({
model,
sessionId: "session-1",
authMode: "oauth",
});
const output = renderProviderSection(details, { fg: (_color: string, text: string) => text });
expect(output).toContain("Name:");
expect(output).toContain("openai-codex");
expect(output).toContain("Transport:");
expect(output).toContain("WebSocket:");
expect(output).toContain("Reuse:");
expect(output).toContain("Auth:");
});
});
@@ -95,17 +95,4 @@ describe("/force slash command", () => {
expect(harness.showStatus).not.toHaveBeenCalled();
expect(harness.setText).toHaveBeenCalledWith("");
});
it("does not pass through prompt when tool validation fails", async () => {
const harness = createRuntimeHarness({
setForcedToolChoice: () => {
throw new Error('Tool "write" is not currently active.');
},
});
const result = await executeBuiltinSlashCommand("/force:write fix stuff", harness.runtime);
expect(result).toBe(true);
expect(harness.showError).toHaveBeenCalledWith('Tool "write" is not currently active.');
});
});
@@ -32,14 +32,6 @@ describe("parseGitHubRepo", () => {
expect(parseGitHubRepo("https://gitlab.com/user/repo.git")).toBeNull();
});
test("returns null for empty string", () => {
expect(parseGitHubRepo("")).toBeNull();
});
test("returns null for malformed URL", () => {
expect(parseGitHubRepo("not-a-url")).toBeNull();
});
test("handles GitHub Enterprise-style URLs (no match)", () => {
expect(parseGitHubRepo("https://github.corp.com/org/repo.git")).toBeNull();
});
@@ -3,9 +3,6 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import {
DEFAULT_MAX_BYTES,
DEFAULT_MAX_COLUMN,
DEFAULT_MAX_LINES,
formatHeadTruncationNotice,
formatTailTruncationNotice,
OutputSink,
@@ -41,14 +38,6 @@ afterEach(async () => {
else Bun.env.PI_ALLOW_SIXEL_PASSTHROUGH = originalAllowPassthrough;
});
describe("streaming-output exports", () => {
test("exports expected default limits", () => {
expect(DEFAULT_MAX_LINES).toBe(3000);
expect(DEFAULT_MAX_BYTES).toBe(50 * 1024);
expect(DEFAULT_MAX_COLUMN).toBe(1024);
});
});
describe("truncateTailBytes", () => {
test("returns source when already under limit", () => {
const text = "hello";
@@ -1,87 +0,0 @@
import { describe, expect, it } from "bun:test";
import { getThemeByName } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { taskToolRenderer } from "../../src/task/render";
import type { TaskToolDetails } from "../../src/task/types";
describe("taskToolRenderer report_finding safety", () => {
it("renders progress without crashing when report_finding payload is malformed", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const details: TaskToolDetails = {
projectAgentsDir: null,
results: [],
totalDurationMs: 42,
progress: [
{
index: 0,
id: "1-Reviewer",
agent: "reviewer",
agentSource: "bundled",
status: "running",
task: "Review patch",
recentTools: [],
recentOutput: [],
toolCount: 1,
tokens: 0,
durationMs: 42,
extractedToolData: {
report_finding: [{}],
},
},
],
};
const rendered = taskToolRenderer.renderResult(
{
content: [{ type: "text", text: "" }],
details,
},
{ expanded: false, isPartial: true },
uiTheme,
);
expect(() => rendered.render(120)).not.toThrow();
});
it("renders abort reason inline for aborted subagent results", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const details: TaskToolDetails = {
projectAgentsDir: null,
results: [
{
index: 0,
id: "1-Reviewer",
agent: "reviewer",
agentSource: "bundled",
task: "Review patch",
exitCode: 1,
output: "",
stderr: "",
truncated: false,
durationMs: 42,
tokens: 0,
aborted: true,
abortReason: "blocked by permissions",
},
],
totalDurationMs: 42,
};
const rendered = taskToolRenderer.renderResult(
{
content: [{ type: "text", text: "" }],
details,
},
{ expanded: false, isPartial: false },
uiTheme,
);
const lines = rendered.render(120);
expect(lines.join("\n")).toContain("blocked by permissions");
});
});
@@ -6,49 +6,6 @@ const forced = { type: "tool", name: "write" } as const;
const forcedRead = { type: "tool", name: "read" } as const;
describe("ToolChoiceQueue", () => {
it("returns undefined when empty", () => {
const q = new ToolChoiceQueue();
expect(q.nextToolChoice()).toBeUndefined();
});
it("pushOnce yields once then exhausts", () => {
const q = new ToolChoiceQueue();
q.pushOnce(forced, { label: "a" });
expect(q.nextToolChoice()).toEqual(forced);
q.resolve();
expect(q.nextToolChoice()).toBeUndefined();
});
it("pushSequence yields in order then exhausts", () => {
const q = new ToolChoiceQueue();
q.pushSequence([forced, "none"], { label: "seq" });
expect(q.nextToolChoice()).toEqual(forced);
q.resolve();
expect(q.nextToolChoice()).toBe("none");
q.resolve();
expect(q.nextToolChoice()).toBeUndefined();
});
it("now:true prepends to head", () => {
const q = new ToolChoiceQueue();
q.pushOnce(forced, { label: "first" });
q.pushOnce(forcedRead, { label: "urgent", now: true });
expect(q.nextToolChoice()).toEqual(forcedRead);
q.resolve();
expect(q.nextToolChoice()).toEqual(forced);
});
it("multiple directives drain in FIFO order", () => {
const q = new ToolChoiceQueue();
q.pushOnce(forced, { label: "a" });
q.pushOnce(forcedRead, { label: "b" });
expect(q.nextToolChoice()).toEqual(forced);
q.resolve();
expect(q.nextToolChoice()).toEqual(forcedRead);
q.resolve();
expect(q.nextToolChoice()).toBeUndefined();
});
describe("resolve callback", () => {
it("fires onResolved with the served choice", () => {
const q = new ToolChoiceQueue();
@@ -61,11 +18,6 @@ describe("ToolChoiceQueue", () => {
q.resolve();
expect(resolved).toEqual([{ choice: forced }]);
});
it("does not fire onResolved when queue is empty", () => {
const q = new ToolChoiceQueue();
q.resolve(); // no-op, nothing in-flight
});
});
describe("reject callback", () => {
@@ -102,39 +54,6 @@ describe("ToolChoiceQueue", () => {
expect(q.nextToolChoice()).toBeUndefined();
});
it("default (no callback) drops the yield", () => {
const q = new ToolChoiceQueue();
q.pushOnce(forced, { label: "a" });
expect(q.nextToolChoice()).toEqual(forced);
q.reject("aborted");
expect(q.nextToolChoice()).toBeUndefined();
});
it("reject is a no-op when nothing is in-flight", () => {
const q = new ToolChoiceQueue();
q.pushOnce(forced, {
label: "a",
onRejected: () => "requeue",
});
q.reject("aborted"); // no-op, nothing yielded yet
expect(q.nextToolChoice()).toEqual(forced);
});
it("passes the correct reason to onRejected", () => {
const q = new ToolChoiceQueue();
const reasons: string[] = [];
q.pushOnce(forced, {
label: "a",
onRejected: info => {
reasons.push(info.reason);
return "drop";
},
});
q.nextToolChoice();
q.reject("error");
expect(reasons).toEqual(["error"]);
});
it("requeued directive preserves onRejected so it can re-requeue across aborts", () => {
const q = new ToolChoiceQueue();
let rejectCount = 0;
@@ -60,14 +60,6 @@ async function getToolMetadata(): Promise<Map<string, { loadMode?: string; summa
return metadata;
}
describe("BUILTIN_TOOLS public factory map", () => {
it("exposes callable tool factories (back-compat for external SDK callers)", () => {
// External callers may invoke BUILTIN_TOOLS.read(session) directly. Verify the value
// is a function, not a metadata object wrapping a factory.
expect(typeof BUILTIN_TOOLS.read).toBe("function");
expect(typeof BUILTIN_TOOLS.bash).toBe("function");
expect(typeof BUILTIN_TOOLS.edit).toBe("function");
});
it("sets loading fields on tool definitions without wrapping factories", async () => {
const metadata = await getToolMetadata();
const missing = Object.keys(BUILTIN_TOOLS).filter(name => metadata.get(name)?.loadMode === undefined);
@@ -76,48 +68,6 @@ describe("BUILTIN_TOOLS public factory map", () => {
});
describe("built-in tool loadMode annotations", () => {
it("marks read, bash, edit, and search_tool_bm25 as essential", async () => {
const metadata = await getToolMetadata();
expect(metadata.get("read")?.loadMode).toBe("essential");
expect(metadata.get("bash")?.loadMode).toBe("essential");
expect(metadata.get("edit")?.loadMode).toBe("essential");
expect(metadata.get("search_tool_bm25")?.loadMode).toBe("essential");
});
it("marks non-essential tools as discoverable", async () => {
const discoverableExpected = [
"ast_grep",
"ast_edit",
"render_mermaid",
"ask",
"debug",
"eval",
"calc",
"ssh",
"github",
"find",
"search",
"lsp",
"inspect_image",
"browser",
"checkpoint",
"rewind",
"task",
"job",
"recipe",
"irc",
"todo_write",
"web_search",
"write",
"retain",
"recall",
"reflect",
];
const metadata = await getToolMetadata();
const missing = discoverableExpected.filter(name => metadata.get(name)?.loadMode !== "discoverable");
expect(missing).toEqual([]);
});
it("provides a summary for every discoverable tool", async () => {
const missing: string[] = [];
const metadata = await getToolMetadata();
@@ -130,14 +80,6 @@ describe("built-in tool loadMode annotations", () => {
});
});
describe("DEFAULT_ESSENTIAL_TOOL_NAMES", () => {
it("contains the expected defaults", () => {
expect(DEFAULT_ESSENTIAL_TOOL_NAMES).toContain("read");
expect(DEFAULT_ESSENTIAL_TOOL_NAMES).toContain("bash");
expect(DEFAULT_ESSENTIAL_TOOL_NAMES).toContain("edit");
});
});
describe("computeEssentialBuiltinNames", () => {
it("returns DEFAULT_ESSENTIAL_TOOL_NAMES when override is empty", () => {
const settings = Settings.isolated({});
@@ -174,26 +116,6 @@ describe("computeEssentialBuiltinNames", () => {
});
describe("tools.discoveryMode settings schema", () => {
it("defaults to off", () => {
const settings = Settings.isolated({});
expect(settings.get("tools.discoveryMode")).toBe("off");
});
it("accepts mcp-only", () => {
const settings = Settings.isolated({ "tools.discoveryMode": "mcp-only" });
expect(settings.get("tools.discoveryMode")).toBe("mcp-only");
});
it("accepts all", () => {
const settings = Settings.isolated({ "tools.discoveryMode": "all" });
expect(settings.get("tools.discoveryMode")).toBe("all");
});
it("tools.essentialOverride defaults to empty array", () => {
const settings = Settings.isolated({});
expect(settings.get("tools.essentialOverride")).toEqual([]);
});
it("back-compat: mcp.discoveryMode still accepted", () => {
const settings = Settings.isolated({ "mcp.discoveryMode": true });
expect(settings.get("mcp.discoveryMode")).toBe(true);
@@ -26,11 +26,6 @@ describe("persistence back-compat: buildDiscoverableMCPSearchIndex wraps generic
},
];
it("returns correct document count", () => {
const index = buildDiscoverableMCPSearchIndex(legacyMCPTools);
expect(index.documents).toHaveLength(2);
});
it("maps description → summary in the index", () => {
const index = buildDiscoverableMCPSearchIndex(legacyMCPTools);
// The documents contain DiscoverableTool objects with .summary, not .description
@@ -6,45 +6,6 @@ import { Settings } from "../../src/config/settings";
// without needing to spin up a full AgentSession or subagent.
// ─────────────────────────────────────────────────────────────────────────────
describe("tools.discoveryMode subagent inheritance via settings", () => {
it("'off' propagates to child as 'off'", () => {
const parentSettings = Settings.isolated({ "tools.discoveryMode": "off" });
// Subagent inherits the same settings object (in task/executor.ts, subagentSettings is
// derived from parent settings). The setting value should be preserved.
const child = Settings.isolated({ "tools.discoveryMode": parentSettings.get("tools.discoveryMode") });
expect(child.get("tools.discoveryMode")).toBe("off");
});
it("'mcp-only' propagates to child as 'mcp-only'", () => {
const parentSettings = Settings.isolated({ "tools.discoveryMode": "mcp-only" });
const child = Settings.isolated({ "tools.discoveryMode": parentSettings.get("tools.discoveryMode") });
expect(child.get("tools.discoveryMode")).toBe("mcp-only");
});
it("'all' propagates to child as 'all'", () => {
const parentSettings = Settings.isolated({ "tools.discoveryMode": "all" });
const child = Settings.isolated({ "tools.discoveryMode": parentSettings.get("tools.discoveryMode") });
expect(child.get("tools.discoveryMode")).toBe("all");
});
it("mcp.discoveryMode=true propagates to child as back-compat", () => {
const parentSettings = Settings.isolated({ "mcp.discoveryMode": true });
const child = Settings.isolated({ "mcp.discoveryMode": parentSettings.get("mcp.discoveryMode") });
expect(child.get("mcp.discoveryMode")).toBe(true);
});
it("explicit toolNames override discovery — child with toolNames=['read'] ignores discovery mode", () => {
// If a subagent definition specifies explicit tools, those take precedence
// over the discovery mode. This is enforced in task/executor.ts by only
// building the toolNames list from the agent.tools if present.
const toolNames = ["read"];
// When toolNames is explicit, only those tools are used regardless of discovery mode
expect(toolNames).toContain("read");
expect(toolNames).not.toContain("find");
expect(toolNames).not.toContain("search");
});
});
describe("effective discovery mode resolution", () => {
function resolveEffectiveMode(settings: Settings): "off" | "mcp-only" | "all" {
const toolsMode = settings.get("tools.discoveryMode");
@@ -80,12 +80,6 @@ describe("assertEditableFileContent", () => {
});
describe("assertEditableFile", () => {
it("detects auto-generated filename patterns", async () => {
const filePath = path.join(tempDir, "zz_generated.deepcopy.go");
await Bun.write(filePath, "package generated");
await expect(assertEditableFile(filePath)).rejects.toBeInstanceOf(ToolError);
});
it("detects content marker from file prefix", async () => {
const filePath = path.join(tempDir, "service.ts");
await Bun.write(filePath, "// Code generated by sqlc. DO NOT EDIT.\nexport const foo = 1;");
@@ -35,12 +35,6 @@ describe("ExitPlanModeTool", () => {
};
}
it("requires title in schema", () => {
const tool = new ExitPlanModeTool(createSession());
const schema = tool.parameters as { required?: string[] };
expect(schema.required).toContain("title");
});
it("normalizes title to .md final plan path", async () => {
const tool = new ExitPlanModeTool(createSession());
const result = await tool.execute("call-1", { title: "WP_MIGRATION_PLAN" });
@@ -1,194 +0,0 @@
import { describe, expect, it } from "bun:test";
import { sanitizeText } from "@oh-my-pi/pi-natives";
import { getThemeByName } from "../../src/modes/theme/theme";
import type { GhToolDetails } from "../../src/tools/gh";
import { githubToolRenderer } from "../../src/tools/gh-renderer";
import { toolRenderers } from "../../src/tools/renderers";
describe("githubToolRenderer", () => {
it("renders a compact ghw-style run summary", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const result: {
content: Array<{ type: string; text?: string }>;
details?: GhToolDetails;
isError?: boolean;
} = {
content: [{ type: "text", text: "llm-visible text stays unchanged" }],
details: {
watch: {
mode: "run",
state: "watching",
repo: "v12-security/v12x",
run: {
id: 23856332053,
workflowName: "CI",
branch: "dev",
jobs: [
{
id: 1,
name: "Workflow Lint",
status: "completed",
conclusion: "success",
durationSeconds: 55,
},
{
id: 2,
name: "Frontend Checks",
status: "in_progress",
durationSeconds: 40,
},
{
id: 3,
name: "Rust Tests",
status: "queued",
durationSeconds: 5,
},
],
},
},
},
};
const component = githubToolRenderer.renderResult(result, { expanded: false, isPartial: true }, uiTheme);
const rendered = sanitizeText(component.render(64).join("\n"));
expect(toolRenderers.github).toBeDefined();
expect(rendered).toContain("watching run #23856332053 on v12-security/v12x");
expect(rendered).toContain("CI dev #23856332053");
expect(rendered).toContain(`${uiTheme.status.success} Workflow Lint`);
expect(rendered).toContain(`${uiTheme.status.enabled} Frontend Checks`);
expect(rendered).toContain(`${uiTheme.status.shadowed} Rust Tests`);
expect(rendered).toContain("55s");
expect(rendered).toContain("40s");
expect(rendered).toContain("5s");
expect(rendered).not.toContain("llm-visible text stays unchanged");
});
it("shows failed log tails without dumping the full log when collapsed", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const result: {
content: Array<{ type: string; text?: string }>;
details?: GhToolDetails;
isError?: boolean;
} = {
content: [{ type: "text", text: "full markdown result" }],
details: {
watch: {
mode: "run",
state: "completed",
repo: "owner/repo",
run: {
id: 77,
workflowName: "CI",
branch: "feature/bugfix",
conclusion: "failure",
jobs: [
{
id: 202,
name: "test",
status: "completed",
conclusion: "failure",
durationSeconds: 360,
},
],
},
failedLogs: [
{
runId: 77,
workflowName: "CI",
jobName: "test",
available: true,
tail: ["alpha", "beta", "gamma", "delta", "epsilon", "zeta"].join("\n"),
},
],
},
},
};
const component = githubToolRenderer.renderResult(result, { expanded: false, isPartial: false }, uiTheme);
const rendered = sanitizeText(component.render(72).join("\n"));
expect(rendered).toContain("failed logs");
expect(rendered).toContain("delta");
expect(rendered).toContain("epsilon");
expect(rendered).toContain("zeta");
expect(rendered).not.toContain("alpha");
expect(rendered).toContain("more log lines");
});
it("renders issue_view as a status header with collapsed body and expand hint", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const bodyLines = Array.from({ length: 30 }, (_, i) => `line ${i + 1}`);
const result = {
content: [
{
type: "text",
text: ["# Issue #903: Bug report", "State: OPEN", "", "## Body", "", ...bodyLines].join("\n"),
},
],
};
const component = githubToolRenderer.renderResult(result, { expanded: false, isPartial: false }, uiTheme, {
op: "issue_view",
issue: "903",
repo: "owner/repo",
});
const rendered = sanitizeText(component.render(80).join("\n"));
expect(rendered).toContain("GitHub Issue");
expect(rendered).toContain("#903");
expect(rendered).toContain("owner/repo");
expect(rendered).toContain("# Issue #903: Bug report");
expect(rendered).toContain("more lines");
expect(rendered).not.toContain("line 30");
});
it("renders issue_view fully when expanded", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const bodyLines = Array.from({ length: 30 }, (_, i) => `line ${i + 1}`);
const result = {
content: [{ type: "text", text: bodyLines.join("\n") }],
};
const component = githubToolRenderer.renderResult(result, { expanded: true, isPartial: false }, uiTheme, {
op: "issue_view",
issue: "https://github.com/owner/repo/issues/903",
});
const rendered = sanitizeText(component.render(80).join("\n"));
expect(rendered).toContain("#903");
expect(rendered).toContain("line 1");
expect(rendered).toContain("line 30");
expect(rendered).not.toContain("more lines");
});
it("truncates each line to the available width to avoid overflow", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const longLine = "x".repeat(500);
const result = { content: [{ type: "text", text: longLine }] };
const component = githubToolRenderer.renderResult(result, { expanded: false, isPartial: false }, uiTheme, {
op: "issue_view",
issue: "1",
});
const lines = component.render(60);
for (const line of lines) {
expect(sanitizeText(line).length).toBeLessThanOrEqual(60);
}
});
});
@@ -267,84 +267,6 @@ describe("tool schema validation (post-sanitization)", () => {
expect(allViolations).toEqual([]);
});
it("no sanitized schema contains $schema declaration", async () => {
const session = createTestSession();
const tools = await createTools(session);
for (const tool of tools) {
const schema = tool.parameters;
if (!schema) continue;
const sanitized = sanitizeSchemaForGoogle(schema);
const violations = validateSchema(sanitized, tool.name).filter(v => v.key === "$schema");
expect(violations).toEqual([]);
}
});
it("no sanitized schema contains $ref or $defs", async () => {
const session = createTestSession();
const tools = await createTools(session);
for (const tool of tools) {
const schema = tool.parameters;
if (!schema) continue;
const sanitized = sanitizeSchemaForGoogle(schema);
const violations = validateSchema(sanitized, tool.name).filter(v => v.key === "$ref" || v.key === "$defs");
expect(violations).toEqual([]);
}
});
it("no sanitized schema contains Draft 2020-12 specific features", async () => {
const session = createTestSession();
const tools = await createTools(session);
const draft2020Features = [
"prefixItems",
"$dynamicRef",
"$dynamicAnchor",
"unevaluatedProperties",
"unevaluatedItems",
];
for (const tool of tools) {
const schema = tool.parameters;
if (!schema) continue;
const sanitized = sanitizeSchemaForGoogle(schema);
const violations = validateSchema(sanitized, tool.name).filter(v => draft2020Features.includes(v.key));
expect(violations).toEqual([]);
}
});
it("sanitization removes const (converts to enum)", async () => {
const session = createTestSession();
const tools = await createTools(session);
for (const tool of tools) {
const schema = tool.parameters;
if (!schema) continue;
const sanitized = sanitizeSchemaForGoogle(schema);
const violations = validateSchema(sanitized, tool.name).filter(v => v.key === "const");
expect(violations).toEqual([]);
}
});
it("no sanitized schema contains examples field", async () => {
const session = createTestSession();
const tools = await createTools(session);
for (const tool of tools) {
const schema = tool.parameters;
if (!schema) continue;
const sanitized = sanitizeSchemaForGoogle(schema);
const violations = validateSchema(sanitized, tool.name).filter(v => v.key === "examples");
expect(violations).toEqual([]);
}
});
it("hidden tools also have valid sanitized schemas", async () => {
const session = createTestSession();
@@ -365,41 +287,6 @@ describe("tool schema validation (post-sanitization)", () => {
}
}
});
it("logs warnings for potentially problematic features (non-blocking)", async () => {
const session = createTestSession();
const tools = await createTools(session);
const warnings: { tool: string; violations: SchemaViolation[] }[] = [];
for (const tool of tools) {
const schema = tool.parameters;
if (!schema) continue;
const sanitized = sanitizeSchemaForGoogle(schema);
const violations = validateSchema(sanitized, tool.name);
const toolWarnings = violations.filter(v => v.severity === "warning");
if (toolWarnings.length > 0) {
warnings.push({ tool: tool.name, violations: toolWarnings });
}
}
// Log warnings but don't fail - these are advisory
if (warnings.length > 0) {
const message = warnings
.map(({ tool, violations }) => {
const details = violations.map(v => ` - ${v.path}: ${v.key} = ${JSON.stringify(v.value)}`).join("\n");
return `${tool}:\n${details}`;
})
.join("\n\n");
console.log(`Schema warnings (non-blocking):\n\n${message}`);
}
// This test passes regardless - warnings are informational
expect(true).toBe(true);
});
});
describe("validateSchema helper", () => {
@@ -452,12 +339,6 @@ describe("validateSchema helper", () => {
expect(warning?.severity).toBe("warning");
});
it("does not warn on additionalProperties: true", () => {
const schema = { type: "object", additionalProperties: true };
const violations = validateSchema(schema);
expect(violations.some(v => v.key === "additionalProperties")).toBe(false);
});
it("warns on format keyword", () => {
const schema = { type: "string", format: "uri" };
const violations = validateSchema(schema);
@@ -490,17 +371,4 @@ describe("validateSchema helper", () => {
const violations = validateSchema(schema);
expect(violations.some(v => v.key === "const")).toBe(true);
});
it("returns empty array for valid schema", () => {
const schema = {
type: "object",
properties: {
name: { type: "string", description: "User name" },
age: { type: "number", minimum: 0 },
},
required: ["name"],
};
const violations = validateSchema(schema);
expect(violations.filter(v => v.severity === "error")).toEqual([]);
});
});
@@ -3,9 +3,12 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { ArtifactProtocolHandler } from "@oh-my-pi/pi-coding-agent/internal-urls/artifact-protocol";
import { LocalProtocolHandler } from "@oh-my-pi/pi-coding-agent/internal-urls/local-protocol";
import { InternalUrlRouter } from "@oh-my-pi/pi-coding-agent/internal-urls/router";
import {
addArtifactsDirSource,
InternalUrlRouter,
resetInternalUrlStateForTests,
setLocalOptions,
} from "@oh-my-pi/pi-coding-agent/internal-urls";
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { SearchTool } from "@oh-my-pi/pi-coding-agent/tools/search";
@@ -24,10 +27,17 @@ describe("SearchTool internal URL resolution", () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "grep-test-"));
artifactsDir = path.join(tmpDir, "artifacts");
await fs.mkdir(artifactsDir);
resetInternalUrlStateForTests();
InternalUrlRouter.resetForTests();
addArtifactsDirSource(() => artifactsDir);
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
resetInternalUrlStateForTests();
InternalUrlRouter.resetForTests();
});
function createSession(overrides: Partial<ToolSession> = {}): ToolSession {
@@ -41,18 +51,11 @@ describe("SearchTool internal URL resolution", () => {
};
}
function createRouterWithArtifacts(): InternalUrlRouter {
const router = new InternalUrlRouter();
router.register(new ArtifactProtocolHandler({ getArtifactsDir: () => artifactsDir }));
return router;
}
it("resolves artifact:// URL to backing file and greps it", async () => {
const content = "line one\nfound the needle here\nline three\n";
await Bun.write(path.join(artifactsDir, "5.bash.log"), content);
const router = createRouterWithArtifacts();
const session = createSession({ internalRouter: router });
const session = createSession();
const tool = new SearchTool(session);
const result = await tool.execute("test-call", {
@@ -68,8 +71,7 @@ describe("SearchTool internal URL resolution", () => {
const content = "ERROR: connection refused\nWARN: timeout\nERROR: disk full\nINFO: ok\n";
await Bun.write(path.join(artifactsDir, "3.python.log"), content);
const router = createRouterWithArtifacts();
const session = createSession({ internalRouter: router });
const session = createSession();
const tool = new SearchTool(session);
const result = await tool.execute("test-call", {
@@ -85,31 +87,18 @@ describe("SearchTool internal URL resolution", () => {
});
it("throws when internal URL has no sourcePath", async () => {
const router = new InternalUrlRouter();
router.register({
scheme: "agent",
immutable: true,
async resolve() {
return {
url: "agent://0",
content: "some content",
contentType: "text/plain" as const,
};
},
});
const session = createSession({ internalRouter: router });
const session = createSession();
const tool = new SearchTool(session);
expect(tool.execute("test-call", { pattern: "foo", paths: ["agent://0"] })).rejects.toThrow(
"Cannot search internal URL without a backing file",
expect(tool.execute("test-call", { pattern: "foo", paths: ["artifact://999"] })).rejects.toThrow(
"Artifact 999 not found",
);
});
it("falls back to normal path resolution when no internalRouter", async () => {
await Bun.write(path.join(tmpDir, "test.txt"), "hello world\n");
const session = createSession(); // no internalRouter
const session = createSession();
const tool = new SearchTool(session);
const result = await tool.execute("test-call", {
@@ -124,8 +113,7 @@ describe("SearchTool internal URL resolution", () => {
it("falls back to normal resolution for non-internal URLs", async () => {
await Bun.write(path.join(tmpDir, "data.log"), "some data here\n");
const router = createRouterWithArtifacts();
const session = createSession({ internalRouter: router });
const session = createSession();
const tool = new SearchTool(session);
const result = await tool.execute("test-call", {
@@ -141,8 +129,7 @@ describe("SearchTool internal URL resolution", () => {
const content = "alpha line\nbeta needle line\ngamma line\n";
await Bun.write(path.join(artifactsDir, "9.bash.log"), content);
const router = createRouterWithArtifacts();
const session = createSession({ internalRouter: router, hasEditTool: true });
const session = createSession({ hasEditTool: true });
const tool = new SearchTool(session);
const result = await tool.execute("test-call", {
@@ -161,14 +148,9 @@ describe("SearchTool internal URL resolution", () => {
await fs.mkdir(localRoot, { recursive: true });
await Bun.write(path.join(localRoot, "plan.md"), "alpha line\nbeta needle line\ngamma line\n");
const router = new InternalUrlRouter();
router.register(
new LocalProtocolHandler({
getArtifactsDir: () => artifactsDir,
getSessionId: () => "session",
}),
);
const session = createSession({ internalRouter: router, hasEditTool: true });
setLocalOptions({ getArtifactsDir: () => artifactsDir, getSessionId: () => "session" });
const session = createSession({ hasEditTool: true });
const tool = new SearchTool(session);
const result = await tool.execute("test-call", {
@@ -187,8 +169,7 @@ describe("SearchTool internal URL resolution", () => {
await Bun.write(path.join(artifactsDir, "11.bash.log"), content);
await Bun.write(path.join(tmpDir, "mixed.txt"), "mixed needle line\n");
const router = createRouterWithArtifacts();
const session = createSession({ internalRouter: router, hasEditTool: true });
const session = createSession({ hasEditTool: true });
const tool = new SearchTool(session);
const result = await tool.execute("test-call", {
@@ -203,8 +184,7 @@ describe("SearchTool internal URL resolution", () => {
});
it("throws on nonexistent artifact ID", async () => {
const router = createRouterWithArtifacts();
const session = createSession({ internalRouter: router });
const session = createSession();
const tool = new SearchTool(session);
expect(tool.execute("test-call", { pattern: "foo", paths: ["artifact://999"] })).rejects.toThrow(
@@ -1,11 +1,10 @@
import { describe, expect, it } from "bun:test";
import { getThemeByName } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { Settings } from "../../src/config/settings";
// Back-compat import check — these re-exports from mcp/discoverable-tool-metadata should still work
import { buildDiscoverableMCPSearchIndex, type DiscoverableMCPTool } from "../../src/mcp/discoverable-tool-metadata";
import type { DiscoverableMCPSearchIndex, DiscoverableTool } from "../../src/tool-discovery/tool-index";
import type { ToolSession } from "../../src/tools/index";
import { SearchToolBm25Tool, searchToolBm25Renderer } from "../../src/tools/search-tool-bm25";
import { SearchToolBm25Tool } from "../../src/tools/search-tool-bm25";
type TestDiscoverableTool = DiscoverableTool;
@@ -145,187 +144,6 @@ describe("SearchToolBm25Tool", () => {
]);
});
it("renders a titled discovery summary instead of the raw tool name", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const renderedCall = searchToolBm25Renderer.renderCall(
{ query: "github issue", limit: 2 },
{ expanded: false, isPartial: false },
uiTheme,
);
expect(renderedCall.render(120).join("\n")).toContain("Tool Discovery");
expect(renderedCall.render(120).join("\n")).not.toContain("search_tool_bm25");
const renderedResult = searchToolBm25Renderer.renderResult(
{
content: [{ type: "text", text: "" }],
details: {
query: "github issue",
limit: 2,
total_tools: 3,
activated_tools: ["mcp__github_create_issue"],
active_selected_tools: ["mcp__github_create_issue"],
tools: [
{
name: "mcp__github_create_issue",
label: "github/create_issue",
description: "Create a GitHub issue in the selected repository",
server_name: "github",
mcp_tool_name: "create_issue",
schema_keys: ["owner", "repo", "title", "body"],
score: 1.234567,
},
],
},
},
{ expanded: false, isPartial: false },
uiTheme,
);
const renderedText = renderedResult.render(120).join("\n");
expect(renderedText).toContain("Tool Discovery");
expect(renderedText).toContain("github/create_issue");
expect(renderedText).toContain("1 active");
expect(renderedText).toContain("limit:2");
expect(renderedText).not.toContain("keys:");
expect(renderedText).not.toContain("search_tool_bm25");
});
it("truncates fallback discovery text before rendering", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const longLine = "Long discovery output ".repeat(20);
const renderedResult = searchToolBm25Renderer.renderResult(
{
content: [{ type: "text", text: longLine }],
},
{ expanded: false, isPartial: false },
uiTheme,
);
const renderedText = renderedResult.render(200).join("\n");
expect(renderedText).toContain("Tool Discovery");
expect(renderedText).toContain("Long discovery output Long discovery output");
expect(renderedText).not.toContain(longLine);
});
it("tolerates partially streamed render-call arguments", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const renderedCall = searchToolBm25Renderer.renderCall(
{} as never,
{ expanded: false, isPartial: true },
uiTheme,
);
expect(renderedCall.render(120).join("\n")).toContain("(empty query)");
});
it("sanitizes MCP metadata before rendering discovery output", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const renderedResult = searchToolBm25Renderer.renderResult(
{
content: [{ type: "text", text: "" }],
details: {
query: "github\tissue",
limit: 2,
total_tools: 1,
activated_tools: ["mcp__github_create_issue"],
active_selected_tools: ["mcp__github_create_issue"],
tools: [
{
name: "mcp__github_create_issue",
label: "github\t/create_issue",
description: "Create\ta GitHub issue",
server_name: "git\thub",
mcp_tool_name: "create_issue",
schema_keys: ["owner", "repo"],
score: 1.234567,
},
],
},
},
{ expanded: true, isPartial: false },
uiTheme,
);
const renderedText = renderedResult.render(120).join("\n");
expect(renderedText).not.toContain("\t");
expect(renderedText).toContain("github issue");
expect(renderedText).toContain("git hub");
expect(renderedText).toContain("Create a GitHub issue");
});
it("shows at most five tools in collapsed renderer output", async () => {
const theme = await getThemeByName("dark");
expect(theme).toBeDefined();
const uiTheme = theme!;
const tools = Array.from({ length: 6 }, (_, index) => ({
name: `mcp__github_tool_${index + 1}`,
label: `github/tool_${index + 1}`,
description: `GitHub tool ${index + 1}`,
server_name: "github",
mcp_tool_name: `tool_${index + 1}`,
schema_keys: ["owner", "repo"],
score: 1 - index * 0.01,
}));
const rendered = searchToolBm25Renderer.renderResult(
{
content: [{ type: "text", text: "" }],
details: {
query: "github tools",
limit: 8,
total_tools: 6,
activated_tools: tools.map(tool => tool.name),
active_selected_tools: tools.map(tool => tool.name),
tools,
},
},
{ expanded: false, isPartial: false },
uiTheme,
);
const renderedText = rendered.render(120).join("\n");
expect(renderedText).toContain("github/tool_5");
expect(renderedText).not.toContain("github/tool_6");
expect(renderedText).toContain("1 more tool");
});
it("defaults to 8 results and lets callers override the limit", async () => {
const manyTools: DiscoverableTool[] = Array.from({ length: 10 }, (_, index) =>
mcpTool(
`mcp__github_tool_${index + 1}`,
"github",
`tool_${index + 1}`,
`GitHub tool ${index + 1} for repository workflows`,
["owner", "repo", `field_${index + 1}`],
),
);
const tool = new SearchToolBm25Tool(createSession(manyTools));
const defaultResult = await tool.execute("call-default", { query: "github" });
expect(defaultResult.details?.limit).toBe(8);
expect(defaultResult.details?.tools).toHaveLength(8);
expect(defaultResult.details?.active_selected_tools).toHaveLength(8);
const defaultContent = defaultResult.content[0];
expect(defaultContent).toBeDefined();
expect(defaultContent).toEqual({
type: "text",
text: JSON.stringify({
query: "github",
activated_tools: defaultResult.details?.activated_tools,
match_count: 8,
total_tools: 10,
}),
});
const limitedTool = new SearchToolBm25Tool(createSession(manyTools));
const limitedResult = await limitedTool.execute("call-limited", { query: "github", limit: 3 });
expect(limitedResult.details?.limit).toBe(3);
expect(limitedResult.details?.tools).toHaveLength(3);
expect(limitedResult.details?.active_selected_tools).toHaveLength(3);
});
it("returns ranked matches and unions activated tools across repeated searches", async () => {
const session = createSession(discoverableTools);
const tool = new SearchToolBm25Tool(session);
@@ -8,11 +8,6 @@ import type { RenderResult } from "@oh-my-pi/pi-coding-agent/web/scrapers/types"
const SKIP = !Bun.env.WEB_FETCH_INTEGRATION;
describe.skipIf(SKIP)("handleSemanticScholar", () => {
it("returns null for non-S2 URLs", async () => {
const result = await handleSemanticScholar("https://example.com", 10);
expect(result).toBeNull();
});
it("fetches a known paper", async () => {
// "Attention Is All You Need" paper
const result = await handleSemanticScholar(
@@ -92,11 +87,6 @@ describe.skipIf(SKIP)("handlePubMed", () => {
return cachedKnownPubMed;
};
it("returns null for non-PubMed URLs", async () => {
const result = await handlePubMed("https://example.com", 10);
expect(result).toBeNull();
});
it("fetches a known article from pubmed.ncbi.nlm.nih.gov", async () => {
// PMID 33782455 - COVID-19 vaccine paper
const result = await fetchKnownPubMed();
@@ -149,11 +139,6 @@ describe.skipIf(SKIP)("handlePubMed", () => {
});
describe.skipIf(SKIP)("handleArxiv", () => {
it("returns null for non-arXiv URLs", async () => {
const result = await handleArxiv("https://example.com", 10000);
expect(result).toBeNull();
});
it("fetches a known paper", async () => {
// "Attention Is All You Need" paper
const result = await handleArxiv("https://arxiv.org/abs/1706.03762", 30000);
@@ -209,11 +194,6 @@ describe.skipIf(SKIP)("handleArxiv", () => {
});
describe.skipIf(SKIP)("handleIacr", () => {
it("returns null for non-IACR URLs", async () => {
const result = await handleIacr("https://example.com", 10000);
expect(result).toBeNull();
});
it("fetches a known ePrint", async () => {
// Using a well-known paper
const result = await handleIacr("https://eprint.iacr.org/2023/123", 30000);
@@ -86,16 +86,6 @@ describe.skipIf(SKIP)("handleGitHub", () => {
expect(result).toBeDefined();
});
it("fetches pull request", async () => {
const result = await handleGitHub("https://github.com/facebook/react/pull/1", 20000);
if (result !== null) {
expect(result.method).toBe("github-pr");
expect(result.contentType).toBe("text/markdown");
expect(result.content.length).toBeGreaterThan(0);
}
expect(result).toBeDefined();
});
it("fetches issues list", async () => {
const result = await handleGitHub("https://github.com/facebook/react/issues", 20000);
if (result !== null) {
@@ -106,69 +96,12 @@ describe.skipIf(SKIP)("handleGitHub", () => {
expect(result).toBeDefined();
});
it("handles repository with underscore in name", async () => {
const result = await handleGitHub("https://github.com/rust-lang/rust-analyzer", 20000);
if (result !== null) {
expect(result.method).toBe("github-repo");
}
expect(result).toBeDefined();
});
it("handles repository with dash in name", async () => {
const result = await handleGitHub("https://github.com/vercel/next.js", 20000);
if (result !== null) {
expect(result.method).toBe("github-repo");
}
expect(result).toBeDefined();
});
it("returns null for invalid URL structure", async () => {
const result = await handleGitHub("https://github.com/", 10000);
expect(result).toBeNull();
});
it("returns null for single path segment", async () => {
const result = await handleGitHub("https://github.com/facebook", 10000);
expect(result).toBeNull();
});
it("handles pulls list endpoint", async () => {
const result = await handleGitHub("https://github.com/facebook/react/pulls", 20000);
// Should be handled as pulls list but currently falls back to null
// This tests the actual behavior
expect(result).toBeDefined();
});
it("fetches file with path containing multiple directories", async () => {
const result = await handleGitHub(
"https://github.com/facebook/react/blob/main/packages/react/package.json",
20000,
);
expect(result).not.toBeNull();
expect(result?.method).toBe("github-raw");
});
it("fetches deeply nested directory", async () => {
const result = await handleGitHub("https://github.com/facebook/react/tree/main/packages/react/src", 20000);
if (result !== null) {
expect(result.method).toBe("github-tree");
}
expect(result).toBeDefined();
});
it("returns null for discussion URLs", async () => {
const result = await handleGitHub("https://github.com/facebook/react/discussions", 10000);
// Discussions not fully implemented, should return null
expect(result).toBeDefined();
});
it("handles trailing slash in repository URL", async () => {
const result = await handleGitHub("https://github.com/facebook/react/", 20000);
if (result !== null) {
expect(result.method).toBe("github-repo");
}
expect(result).toBeDefined();
});
});
// =============================================================================
@@ -9,16 +9,6 @@ import { handleRubyGems } from "@oh-my-pi/pi-coding-agent/web/scrapers/rubygems"
const SKIP = !Bun.env.WEB_FETCH_INTEGRATION;
describe.skipIf(SKIP)("handleBrew", () => {
it("returns null for non-Homebrew URLs", async () => {
const result = await handleBrew("https://example.com", 20);
expect(result).toBeNull();
});
it("returns null for non-package Homebrew URLs", async () => {
const result = await handleBrew("https://formulae.brew.sh/", 20);
expect(result).toBeNull();
});
it("fetches wget formula", async () => {
const result = await handleBrew("https://formulae.brew.sh/formula/wget", 20);
expect(result).not.toBeNull();
@@ -43,16 +33,6 @@ describe.skipIf(SKIP)("handleBrew", () => {
});
describe.skipIf(SKIP)("handleAur", () => {
it("returns null for non-AUR URLs", async () => {
const result = await handleAur("https://example.com", 20);
expect(result).toBeNull();
});
it("returns null for non-package AUR URLs", async () => {
const result = await handleAur("https://aur.archlinux.org/", 20);
expect(result).toBeNull();
});
it("fetches yay package", async () => {
const result = await handleAur("https://aur.archlinux.org/packages/yay", 20);
expect(result).not.toBeNull();
@@ -67,16 +47,6 @@ describe.skipIf(SKIP)("handleAur", () => {
});
describe.skipIf(SKIP)("handleRubyGems", () => {
it("returns null for non-RubyGems URLs", async () => {
const result = await handleRubyGems("https://example.com", 20);
expect(result).toBeNull();
});
it("returns null for non-gem RubyGems URLs", async () => {
const result = await handleRubyGems("https://rubygems.org/", 20);
expect(result).toBeNull();
});
it("fetches rails gem", async () => {
const result = await handleRubyGems("https://rubygems.org/gems/rails", 20);
expect(result).not.toBeNull();
@@ -90,16 +60,6 @@ describe.skipIf(SKIP)("handleRubyGems", () => {
});
describe.skipIf(SKIP)("handleNuGet", () => {
it("returns null for non-NuGet URLs", async () => {
const result = await handleNuGet("https://example.com", 20);
expect(result).toBeNull();
});
it("returns null for non-package NuGet URLs", async () => {
const result = await handleNuGet("https://www.nuget.org/", 20);
expect(result).toBeNull();
});
it("fetches Newtonsoft.Json package", async () => {
const result = await handleNuGet("https://www.nuget.org/packages/Newtonsoft.Json", 20);
expect(result).not.toBeNull();
@@ -113,16 +73,6 @@ describe.skipIf(SKIP)("handleNuGet", () => {
});
describe.skipIf(SKIP)("handlePackagist", () => {
it("returns null for non-Packagist URLs", async () => {
const result = await handlePackagist("https://example.com", 20);
expect(result).toBeNull();
});
it("returns null for non-package Packagist URLs", async () => {
const result = await handlePackagist("https://packagist.org/", 20);
expect(result).toBeNull();
});
it("fetches laravel/framework package", async () => {
const result = await handlePackagist("https://packagist.org/packages/laravel/framework", 20);
expect(result).not.toBeNull();
@@ -136,16 +86,6 @@ describe.skipIf(SKIP)("handlePackagist", () => {
});
describe.skipIf(SKIP)("handleMaven", () => {
it("returns null for non-Maven URLs", async () => {
const result = await handleMaven("https://example.com", 20);
expect(result).toBeNull();
});
it("returns null for non-artifact Maven URLs", async () => {
const result = await handleMaven("https://search.maven.org/", 20);
expect(result).toBeNull();
});
it("fetches commons-lang3 artifact from search.maven.org", async () => {
const result = await handleMaven("https://search.maven.org/artifact/org.apache.commons/commons-lang3", 20);
expect(result).not.toBeNull();
@@ -35,57 +35,6 @@ describe.skipIf(SKIP)("handleMastodon", () => {
{ timeout: 30000 },
);
it(
"fetches a Mastodon post",
async () => {
// Gargron's post ID 1 - the first ever Mastodon post
const result = await handleMastodon("https://mastodon.social/@Gargron/1", 20);
// Post 1 may not exist anymore; check gracefully
if (result !== null) {
expect(result.method).toBe("mastodon");
expect(result.contentType).toBe("text/markdown");
expect(result.content).toContain("Post by");
expect(result.content).toContain("@Gargron");
expect(result.fetchedAt).toBeTruthy();
expect(result.truncated).toBeDefined();
expect(result.notes?.[0]).toContain("Mastodon API");
}
},
{ timeout: 30000 },
);
it(
"handles a stable pinned post",
async () => {
// Use a well-known post from mastodon.social - Gargron's announcement post
const result = await handleMastodon("https://mastodon.social/@Gargron/109318821117356215", 20);
// May not exist, check gracefully
if (result !== null) {
expect(result.method).toBe("mastodon");
expect(result.contentType).toBe("text/markdown");
expect(result.content).toContain("@Gargron");
expect(result.content).toContain("replies");
expect(result.content).toContain("boosts");
expect(result.content).toContain("favorites");
expect(result.fetchedAt).toBeTruthy();
}
},
{ timeout: 30000 },
);
it(
"includes recent posts in profile",
async () => {
const result = await handleMastodon("https://mastodon.social/@Gargron", 20);
expect(result).not.toBeNull();
// May include recent posts section
if (result?.content?.includes("## Recent Posts")) {
expect(result.content).toMatch(/###\s+\w+/); // Date header
}
},
{ timeout: 30000 },
);
it("returns null for non-Mastodon instance with @user pattern", async () => {
// A site that has @user pattern but isn't Mastodon
const result = await handleMastodon("https://twitter.com/@jack", 20);
@@ -140,53 +89,4 @@ describe.skipIf(SKIP)("handleBluesky", () => {
},
{ timeout: 30000 },
);
it(
"fetches a Bluesky post",
async () => {
// A post from bsky.app - use a well-known stable post
const result = await handleBluesky("https://bsky.app/profile/bsky.app/post/3juzlwllznd24", 20);
// Post may not exist, check gracefully
if (result !== null) {
expect(result.method).toBe("bluesky-api");
expect(result.contentType).toBe("text/markdown");
expect(result.content).toContain("# Bluesky Post");
expect(result.content).toContain("@bsky.app");
expect(result.fetchedAt).toBeTruthy();
expect(result.truncated).toBeDefined();
expect(result.notes?.[0]).toContain("AT URI");
}
},
{ timeout: 30000 },
);
it(
"includes post stats",
async () => {
const result = await handleBluesky("https://bsky.app/profile/bsky.app/post/3juzlwllznd24", 20);
// Stats include likes, reposts, replies
if (result?.content) {
// Should have some engagement markers
const hasStats =
result.content.includes("❤️") || result.content.includes("🔁") || result.content.includes("💬");
expect(hasStats || result.content.includes("# Bluesky Post")).toBe(true);
}
},
{ timeout: 30000 },
);
it(
"handles www.bsky.app URLs",
async () => {
const result = await handleBluesky("https://www.bsky.app/profile/bsky.app", 20);
expect(result).not.toBeNull();
expect(result?.method).toBe("bluesky-api");
},
{ timeout: 30000 },
);
it("returns null for invalid profile handle", async () => {
const result = await handleBluesky("https://bsky.app/profile/", 20);
expect(result).toBeNull();
});
});
@@ -6,11 +6,6 @@ import { handleTwitter } from "@oh-my-pi/pi-coding-agent/web/scrapers/twitter";
const SKIP = !Bun.env.WEB_FETCH_INTEGRATION;
describe.skipIf(SKIP)("handleTwitter", () => {
it("returns null for non-Twitter URLs", async () => {
const result = await handleTwitter("https://example.com", 10);
expect(result).toBeNull();
});
it(
"handles twitter.com status URLs",
async () => {
@@ -47,26 +42,6 @@ describe.skipIf(SKIP)("handleTwitter", () => {
{ timeout: 30000 },
);
it(
"handles www.twitter.com URLs",
async () => {
const result = await handleTwitter("https://www.twitter.com/twitter/status/1", 10000);
expect(result).not.toBeNull();
expect(result?.method).toMatch(/^twitter/);
},
{ timeout: 30000 },
);
it(
"handles www.x.com URLs",
async () => {
const result = await handleTwitter("https://www.x.com/twitter/status/1", 10000);
expect(result).not.toBeNull();
expect(result?.method).toMatch(/^twitter/);
},
{ timeout: 30000 },
);
it(
"may fail due to Nitter availability",
async () => {
@@ -84,11 +59,6 @@ describe.skipIf(SKIP)("handleTwitter", () => {
});
describe.skipIf(SKIP)("handleReddit", () => {
it("returns null for non-Reddit URLs", async () => {
const result = await handleReddit("https://example.com", 10);
expect(result).toBeNull();
});
it("fetches subreddit", async () => {
const result = await handleReddit("https://www.reddit.com/r/programming/", 20000);
expect(result).not.toBeNull();
@@ -110,58 +80,9 @@ describe.skipIf(SKIP)("handleReddit", () => {
expect(result.notes).toContain("Fetched via Reddit JSON API");
}
});
it("includes comments in post when available", async () => {
const result = await handleReddit("https://www.reddit.com/r/programming/", 20000);
// Comments test - just verify structure if post with comments is found
if (result?.content?.includes("## Top Comments")) {
expect(result.content).toContain("### u/");
expect(result.content).toContain("points");
}
});
it("handles old.reddit.com", async () => {
const result = await handleReddit("https://old.reddit.com/r/programming/", 20000);
expect(result).not.toBeNull();
expect(result?.method).toBe("reddit");
expect(result?.contentType).toBe("text/markdown");
expect(result?.content).toContain("# r/");
expect(result?.notes).toContain("Fetched via Reddit JSON API");
});
it("handles reddit.com without www", async () => {
const result = await handleReddit("https://reddit.com/r/programming/", 20000);
expect(result).not.toBeNull();
expect(result?.method).toBe("reddit");
});
it("handles URLs with query parameters", async () => {
const result = await handleReddit("https://www.reddit.com/r/programming/?sort=top", 20000);
expect(result).not.toBeNull();
expect(result?.method).toBe("reddit");
expect(result?.content).toContain("# r/");
});
it("returns null for malformed Reddit URLs", async () => {
const result = await handleReddit("https://www.reddit.com/invalid", 20000);
// May return null or empty result
if (result !== null) {
expect(result.content).toBeDefined();
}
});
});
describe.skipIf(SKIP)("handleStackOverflow", () => {
it("returns null for non-SO URLs", async () => {
const result = await handleStackOverflow("https://example.com", 10);
expect(result).toBeNull();
});
it("returns null for SO URLs without question ID", async () => {
const result = await handleStackOverflow("https://stackoverflow.com/", 10);
expect(result).toBeNull();
});
it("fetches a known question", async () => {
// Use a well-known question that definitely exists
const result = await handleStackOverflow(
@@ -180,38 +101,6 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
}
});
it("includes answers", async () => {
const result = await handleStackOverflow(
"https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster",
20000,
);
if (result?.content?.includes("## Answers")) {
expect(result.content).toContain("### Score:");
}
});
it("shows accepted answer marker when present", async () => {
const result = await handleStackOverflow(
"https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster",
20000,
);
// Some questions may have accepted answers
if (result?.content?.includes("(Accepted)")) {
expect(result.content).toContain("## Answers");
}
});
it("handles stackoverflow.com", async () => {
const result = await handleStackOverflow(
"https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array",
20000,
);
expect(result).not.toBeNull();
expect(result?.method).toBe("stackexchange");
expect(result?.content).toContain("# ");
expect(result?.content).toContain("## Question");
});
it("handles other StackExchange sites", async () => {
const result = await handleStackOverflow("https://math.stackexchange.com/questions/1000/", 20000);
// API may fail, check gracefully
@@ -222,38 +111,4 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
expect(result.notes).toContain("Fetched via Stack Exchange API");
}
});
it("extracts question ID from URL", async () => {
const result = await handleStackOverflow(
"https://stackoverflow.com/questions/1234567/some-long-question-title",
20000,
);
// Should attempt to fetch, may or may not exist
// Either returns valid result or null
if (result !== null) {
expect(result.method).toBe("stackoverflow");
}
});
it("handles URLs without trailing slash", async () => {
const result = await handleStackOverflow("https://stackoverflow.com/questions/11227809", 20000);
// API may fail, check gracefully
if (result !== null) {
expect(result.method).toBe("stackexchange");
}
});
it("includes question metadata", async () => {
const result = await handleStackOverflow(
"https://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster",
20000,
);
// API may fail, check gracefully
if (result !== null) {
expect(result.content).toContain("**Score:");
expect(result.content).toContain("**Answers:");
expect(result.content).toContain("**Tags:");
expect(result.content).toContain("**Asked by:");
}
});
});
@@ -91,14 +91,6 @@ describe.skipIf(SKIP)("handleStackOverflow", () => {
});
// Test with www. prefix
it("handles www.stackoverflow.com URLs", async () => {
const result = await handleStackOverflow(
"https://www.stackoverflow.com/questions/218384/what-is-a-nullpointerexception",
20,
);
expect(result).not.toBeNull();
expect(result?.method).toBe("stackexchange");
});
// Verify response structure
it("returns complete response structure", async () => {
@@ -1,131 +0,0 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { hookFetch } from "@oh-my-pi/pi-utils";
import { searchAnthropic } from "../../src/web/search/providers/anthropic";
type CapturedRequest = {
url: string;
headers: RequestInit["headers"];
body: Record<string, unknown> | null;
};
const WEB_SEARCH_BETA = "web-search-2025-03-05";
const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
function makeAnthropicResponse() {
return {
id: "msg_test_123",
model: "claude-haiku-4-5",
content: [{ type: "text", text: "Test answer" }],
usage: {
input_tokens: 12,
output_tokens: 7,
server_tool_use: { web_search_requests: 1 },
},
};
}
function getHeaderCaseInsensitive(headers: RequestInit["headers"], name: string): string | undefined {
if (!headers) return undefined;
if (headers instanceof Headers) {
return headers.get(name) ?? undefined;
}
if (Array.isArray(headers)) {
const match = headers.find(([key]) => key.toLowerCase() === name.toLowerCase());
return match?.[1];
}
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === name.toLowerCase()) {
return value as string;
}
}
return undefined;
}
describe("searchAnthropic headers", () => {
const originalSearchApiKey = process.env.ANTHROPIC_SEARCH_API_KEY;
const originalSearchBaseUrl = process.env.ANTHROPIC_SEARCH_BASE_URL;
const originalApiKey = process.env.ANTHROPIC_API_KEY;
const originalBaseUrl = process.env.ANTHROPIC_BASE_URL;
let capturedRequest: CapturedRequest | null = null;
beforeEach(() => {
capturedRequest = null;
delete process.env.ANTHROPIC_API_KEY;
delete process.env.ANTHROPIC_BASE_URL;
process.env.ANTHROPIC_SEARCH_BASE_URL = ANTHROPIC_BASE_URL;
});
afterEach(() => {
capturedRequest = null;
if (originalSearchApiKey === undefined) {
delete process.env.ANTHROPIC_SEARCH_API_KEY;
} else {
process.env.ANTHROPIC_SEARCH_API_KEY = originalSearchApiKey;
}
if (originalSearchBaseUrl === undefined) {
delete process.env.ANTHROPIC_SEARCH_BASE_URL;
} else {
process.env.ANTHROPIC_SEARCH_BASE_URL = originalSearchBaseUrl;
}
if (originalApiKey === undefined) {
delete process.env.ANTHROPIC_API_KEY;
} else {
process.env.ANTHROPIC_API_KEY = originalApiKey;
}
if (originalBaseUrl === undefined) {
delete process.env.ANTHROPIC_BASE_URL;
} else {
process.env.ANTHROPIC_BASE_URL = originalBaseUrl;
}
});
function mockFetch(responseBody: unknown): Disposable {
return hookFetch((url, init) => {
capturedRequest = {
url: typeof url === "string" ? url : url.toString(),
headers: init?.headers,
body: init?.body ? JSON.parse(init.body as string) : null,
};
return new Response(JSON.stringify(responseBody), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
}
it("includes web-search beta header and sends API key in X-Api-Key mode", async () => {
process.env.ANTHROPIC_SEARCH_API_KEY = "sk-ant-api-test";
using _hook = mockFetch(makeAnthropicResponse());
await searchAnthropic({ query: "test api key mode" });
expect(capturedRequest).not.toBeNull();
expect(capturedRequest?.url).toBe(`${ANTHROPIC_BASE_URL}/v1/messages?beta=true`);
expect(getHeaderCaseInsensitive(capturedRequest?.headers, "anthropic-beta")).toContain(WEB_SEARCH_BETA);
expect(getHeaderCaseInsensitive(capturedRequest?.headers, "x-api-key")).toBe("sk-ant-api-test");
expect(getHeaderCaseInsensitive(capturedRequest?.headers, "authorization")).toBeUndefined();
expect(capturedRequest?.body?.tools).toEqual([{ type: "web_search_20250305", name: "web_search" }]);
});
it("includes web-search beta header and sends OAuth token in Authorization mode", async () => {
process.env.ANTHROPIC_SEARCH_API_KEY = "sk-ant-oat-test";
using _hook = mockFetch(makeAnthropicResponse());
await searchAnthropic({ query: "test oauth mode" });
expect(capturedRequest).not.toBeNull();
expect(getHeaderCaseInsensitive(capturedRequest?.headers, "anthropic-beta")).toContain(WEB_SEARCH_BETA);
expect(getHeaderCaseInsensitive(capturedRequest?.headers, "authorization")).toBe("Bearer sk-ant-oat-test");
expect(getHeaderCaseInsensitive(capturedRequest?.headers, "x-api-key")).toBeUndefined();
});
});
@@ -49,11 +49,6 @@ describe("buildExaRequestBody", () => {
});
});
it("includes contents.summary with the search query", () => {
const body = buildExaRequestBody({ query: "how does React work" });
expect(body.contents).toEqual({ summary: { query: "how does React work" } });
});
it("applies num_results override", () => {
const body = buildExaRequestBody({ query: "q", num_results: 5 });
expect(body.numResults).toBe(5);
@@ -132,21 +127,6 @@ describe("synthesizeAnswer", () => {
expect(answer).toBe("**A**: Summary A\n\n**B**: Summary B");
});
it("limits to MAX_ANSWER_SUMMARIES (3) results", () => {
const results = [
{ title: "A", url: "https://a.com", summary: "SA" },
{ title: "B", url: "https://b.com", summary: "SB" },
{ title: "C", url: "https://c.com", summary: "SC" },
{ title: "D", url: "https://d.com", summary: "SD" },
{ title: "E", url: "https://e.com", summary: "SE" },
];
const answer = synthesizeAnswer(results)!;
expect(answer.split("\n\n")).toHaveLength(3);
expect(answer).toContain("**A**: SA");
expect(answer).toContain("**C**: SC");
expect(answer).not.toContain("**D**");
});
it("skips results with missing summaries but includes ones that have them", () => {
const results = [
{ title: "NoSummary", url: "https://no.com", summary: null },
@@ -1,7 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
import { hookFetch } from "@oh-my-pi/pi-utils";
import { AgentStorage } from "../../src/session/agent-storage";
import { getSearchProviderLabel, resolveProviderChain, SEARCH_PROVIDER_ORDER } from "../../src/web/search/provider";
import { searchTavily } from "../../src/web/search/providers/tavily";
import type { SearchProviderError } from "../../src/web/search/types";
@@ -15,13 +14,6 @@ describe("Tavily web search provider", () => {
delete process.env.TAVILY_API_KEY;
});
it("registers tavily in the provider registry and fallback order", async () => {
expect(SEARCH_PROVIDER_ORDER).toContain("tavily");
expect(getSearchProviderLabel("tavily")).toBe("Tavily");
const providers = await resolveProviderChain("tavily");
expect(providers[0]?.id).toBe("tavily");
});
it("maps Tavily responses into SearchResponse and forwards recency filters", async () => {
let requestBody: Record<string, unknown> | null = null;
@@ -35,18 +35,6 @@ function getSuccessDataSchema(parameters: Record<string, unknown>): Record<strin
}
describe("YieldTool", () => {
it("exposes top-level object parameters with required result union", () => {
const tool = new YieldTool(createSession());
const schema = tool.parameters as {
type?: string;
properties?: Record<string, unknown>;
required?: string[];
};
expect(schema.type).toBe("object");
expect(Object.keys(schema.properties ?? {})).toEqual(["result"]);
expect(schema.required).toEqual(["result"]);
});
it("accepts success payload with data", async () => {
const tool = new YieldTool(createSession());
const result = await tool.execute("call-1", { result: { data: { ok: true } } } as never);
-4
View File
@@ -1150,8 +1150,6 @@ export interface ShellExecuteOptions {
env?: Record<string, string>
/** Environment variables to apply once per session. */
sessionEnv?: Record<string, string>
/** Run the command attached to a PTY. */
pty?: boolean
/** Timeout in milliseconds before cancelling the command. */
timeoutMs?: number
/** Optional snapshot file to source on session creation. */
@@ -1180,8 +1178,6 @@ export interface ShellRunOptions {
cwd?: string
/** Environment variables to apply for this command only. */
env?: Record<string, string>
/** Run the command attached to a PTY. */
pty?: boolean
/** Timeout in milliseconds before cancelling the command. */
timeoutMs?: number
/** Abort signal for cancelling the operation. */
+1 -18
View File
@@ -16,7 +16,7 @@ import * as path from "node:path";
const nativeDir = path.join(import.meta.dir, "..", "native");
const indexJsPath = path.join(nativeDir, "index.js");
const indexDtsPath = path.join(nativeDir, "index.d.ts");
const packageJsonPath = path.join(import.meta.dir, "..", "package.json");
const _packageJsonPath = path.join(import.meta.dir, "..", "package.json");
const PUBLIC_SYMBOL_RE = /^export declare (?:class|function|enum) (\w+)/gm;
@@ -39,12 +39,6 @@ function esmExportsName(js: string, name: string): boolean {
}
describe("issue 892: pi-natives public surface", () => {
it("routes ESM package consumers to the generated ESM loader", async () => {
const packageJson = await Bun.file(packageJsonPath).json();
expect(packageJson.type).toBe("module");
expect(packageJson.exports["."].import).toBe("./native/index.js");
});
it("declares every public .d.ts symbol as an explicit ESM export", async () => {
const [js, symbols] = await Promise.all([Bun.file(indexJsPath).text(), readPublicSymbols()]);
expect(symbols.length).toBeGreaterThan(0);
@@ -52,15 +46,4 @@ describe("issue 892: pi-natives public surface", () => {
const missing = symbols.filter(name => !esmExportsName(js, name));
expect(missing).toEqual([]);
});
it("exports ProcessStatus with the runtime shape consumers depend on", async () => {
// Mirror the failing consumer's package import (packages/utils/src/procmgr.ts,
// packages/coding-agent/src/tools/browser/attach.ts).
const mod = await import("@oh-my-pi/pi-natives");
const processStatusValues: Record<"Running" | "Exited", string> = {
Running: mod.ProcessStatus.Running,
Exited: mod.ProcessStatus.Exited,
};
expect(processStatusValues).toEqual({ Running: "running", Exited: "exited" });
});
});
-86
View File
@@ -28,54 +28,12 @@ describe("StdinBuffer", () => {
}
describe("Regular Characters", () => {
it("should pass through regular characters immediately", () => {
processInput("a");
expect(emittedSequences).toEqual(["a"]);
});
it("should pass through multiple regular characters", () => {
processInput("abc");
expect(emittedSequences).toEqual(["a", "b", "c"]);
});
it("should handle unicode characters", () => {
processInput("hello \u4e16\u754c");
expect(emittedSequences).toEqual(["h", "e", "l", "l", "o", " ", "\u4e16", "\u754c"]);
});
});
describe("Complete Escape Sequences", () => {
it("should pass through complete mouse SGR sequences", () => {
const mouseSeq = "\x1b[<35;20;5m";
processInput(mouseSeq);
expect(emittedSequences).toEqual([mouseSeq]);
});
it("should pass through complete arrow key sequences", () => {
const upArrow = "\x1b[A";
processInput(upArrow);
expect(emittedSequences).toEqual([upArrow]);
});
it("should pass through complete function key sequences", () => {
const f1 = "\x1b[11~";
processInput(f1);
expect(emittedSequences).toEqual([f1]);
});
it("should pass through meta key sequences", () => {
const metaA = "\x1ba";
processInput(metaA);
expect(emittedSequences).toEqual([metaA]);
});
it("should pass through SS3 sequences", () => {
const ss3 = "\x1bOA";
processInput(ss3);
expect(emittedSequences).toEqual([ss3]);
});
});
describe("Partial Escape Sequences", () => {
it("should buffer incomplete mouse SGR sequence", () => {
processInput("\x1b");
@@ -130,21 +88,6 @@ describe("StdinBuffer", () => {
});
describe("Mixed Content", () => {
it("should handle characters followed by escape sequence", () => {
processInput("abc\x1b[A");
expect(emittedSequences).toEqual(["a", "b", "c", "\x1b[A"]);
});
it("should handle escape sequence followed by characters", () => {
processInput("\x1b[Aabc");
expect(emittedSequences).toEqual(["\x1b[A", "a", "b", "c"]);
});
it("should handle multiple complete sequences", () => {
processInput("\x1b[A\x1b[B\x1b[C");
expect(emittedSequences).toEqual(["\x1b[A", "\x1b[B", "\x1b[C"]);
});
it("should handle partial sequence with preceding characters", () => {
processInput("abc\x1b[<35");
expect(emittedSequences).toEqual(["a", "b", "c"]);
@@ -156,18 +99,6 @@ describe("StdinBuffer", () => {
});
describe("Kitty Keyboard Protocol", () => {
it("should handle Kitty CSI u press events", () => {
// Press 'a' in Kitty protocol
processInput("\x1b[97u");
expect(emittedSequences).toEqual(["\x1b[97u"]);
});
it("should handle Kitty CSI u release events", () => {
// Release 'a' in Kitty protocol
processInput("\x1b[97;1:3u");
expect(emittedSequences).toEqual(["\x1b[97;1:3u"]);
});
it("should handle batched Kitty press and release", () => {
// Press 'a', release 'a' batched together (common over SSH)
processInput("\x1b[97u\x1b[97;1:3u");
@@ -180,29 +111,12 @@ describe("StdinBuffer", () => {
expect(emittedSequences).toEqual(["\x1b[97u", "\x1b[97;1:3u", "\x1b[98u", "\x1b[98;1:3u"]);
});
it("should handle Kitty arrow keys with event type", () => {
// Up arrow press with event type
processInput("\x1b[1;1:1A");
expect(emittedSequences).toEqual(["\x1b[1;1:1A"]);
});
it("should handle Kitty functional keys with event type", () => {
// Delete key release
processInput("\x1b[3;1:3~");
expect(emittedSequences).toEqual(["\x1b[3;1:3~"]);
});
it("should handle plain characters mixed with Kitty sequences", () => {
// Plain 'a' followed by Kitty release
processInput("a\x1b[97;1:3u");
expect(emittedSequences).toEqual(["a", "\x1b[97;1:3u"]);
});
it("should handle Kitty sequence followed by plain characters", () => {
processInput("\x1b[97ua");
expect(emittedSequences).toEqual(["\x1b[97u", "a"]);
});
it("should handle rapid typing simulation with Kitty protocol", () => {
// Simulates typing "hi" quickly with releases interleaved
processInput("\x1b[104u\x1b[104;1:3u\x1b[105u\x1b[105;1:3u");
@@ -1,12 +0,0 @@
import { describe, expect, it } from "bun:test";
import { formatContent } from "../src/formatter";
describe("formatContent", () => {
it("pins .js files to the flow parser (no fallback to babel-ts)", async () => {
const tsOnlySyntaxInJs = "namespace Foo { export const value = 1; }\n";
const result = await formatContent("fixture.js", tsOnlySyntaxInJs);
expect(result.didFormat).toBe(false);
expect(result.formatted).toBe(tsOnlySyntaxInJs);
});
});
+1 -10
View File
@@ -4,7 +4,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { getProjectDir, setProjectDir } from "../src/dirs";
import { Snowflake } from "../src/snowflake";
import { getDefaultTabWidth, getIndentation, setDefaultTabWidth } from "../src/tab-spacing";
import { getIndentation, setDefaultTabWidth } from "../src/tab-spacing";
describe("spacing", () => {
let tempDir = "";
@@ -24,15 +24,6 @@ describe("spacing", () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it("uses configurable default tab width", () => {
expect(getDefaultTabWidth()).toBe(3);
expect(" ".repeat(getIndentation())).toBe(" ");
setDefaultTabWidth(5);
expect(getDefaultTabWidth()).toBe(5);
expect(" ".repeat(getIndentation())).toBe(" ");
});
it("resolves editorconfig rules for file path and falls back to default", async () => {
const filePath = path.join(tempDir, "src", "feature.ts");
await fs.mkdir(path.dirname(filePath), { recursive: true });