From 975941aba4869315af33a1a0222fb6295bd2eaee Mon Sep 17 00:00:00 2001 From: can1357 Date: Tue, 12 May 2026 03:47:43 +0200 Subject: [PATCH] chore: remove garbage tests --- packages/agent/test/agent.test.ts | 156 +--- .../test/alibaba-coding-plan-provider.test.ts | 34 - packages/ai/test/anthropic-alignment.test.ts | 55 -- packages/ai/test/api-registry.test.ts | 8 - packages/ai/test/apply-patch-freeform.test.ts | 5 - packages/ai/test/empty.test.ts | 763 ------------------ ...ub-copilot-claude-messages-routing.test.ts | 49 -- .../ai/test/gitlab-duo-model-mapping.test.ts | 20 - .../google-gemini-cli-3x-thinking.test.ts | 5 - packages/ai/test/issue-887-repro.test.ts | 6 - packages/ai/test/kilo-provider.test.ts | 41 - packages/ai/test/model-thinking.test.ts | 48 -- .../ai/test/ollama-cloud-provider.test.ts | 14 - .../openai-responses-cache-affinity.test.ts | 25 +- packages/ai/test/rate-limit-utils.test.ts | 16 - packages/ai/test/zen.test.ts | 26 - .../test/agent-session-retry-fallback.test.ts | 83 -- .../agent-session-user-shortcut-hooks.test.ts | 26 - packages/coding-agent/test/args.test.ts | 288 ------- .../test/async-job-manager.test.ts | 14 - .../test/autocomplete-max-visible.test.ts | 10 - .../coding-agent/test/block-images.test.ts | 32 - .../test/compaction-hooks-example.test.ts | 70 -- packages/coding-agent/test/compaction.test.ts | 50 -- packages/coding-agent/test/config-cli.test.ts | 34 - .../coding-agent/test/config-spacing.test.ts | 12 - .../test/core/js-executor.test.ts | 11 - .../test/core/python-kernel-env.test.ts | 14 - .../test/core/python-prelude.test.ts | 73 -- .../test/cycle-order-custom-roles.test.ts | 32 - .../test/debug/log-viewer.test.ts | 6 - .../test/edit-streaming-preview.test.ts | 18 - .../custom-commands/ci-green.test.ts | 6 - .../test/hindsight-config.test.ts | 89 -- .../coding-agent/test/image-b64poly.test.ts | 4 - .../test/image-input-normalization.test.ts | 10 - .../coding-agent/test/lm-studio-fix.test.ts | 16 - .../test/marketplace/cache.test.ts | 11 - .../test/marketplace/manager.test.ts | 23 - .../marketplace/parse-internal-url.test.ts | 8 - .../test/marketplace/plugin-dir-roots.test.ts | 53 -- .../coding-agent/test/mcp-roots-list.test.ts | 8 - .../test/memory-backend-resolve.test.ts | 10 - .../model-registry-runtime-provider.test.ts | 30 - .../coding-agent/test/model-registry.test.ts | 7 +- ...model-selector-role-badge-thinking.test.ts | 38 - .../modes/controllers/btw-controller.test.ts | 23 - .../event-controller-message-start.test.ts | 2 +- .../coding-agent/test/output-block.test.ts | 38 - .../plan-mode-approved-prompt.test.ts | 14 - .../coding-agent/test/plugin-command.test.ts | 6 - .../test/prompt-templates.test.ts | 69 -- .../test/prompts/review-request.test.ts | 21 - .../test/rpc-mode-extension-ui.test.ts | 82 -- .../test/secrets-obfuscator.test.ts | 6 - .../coding-agent/test/session-color.test.ts | 19 - .../session-manager/file-operations.test.ts | 42 - .../test/session-provider-section.test.ts | 35 - .../test/slash-commands/force.test.ts | 13 - .../test/status-line-git-utils.test.ts | 8 - .../test/streaming-output.test.ts | 11 - .../test/task/render-report-finding.test.ts | 87 -- .../test/tool-choice-queue.test.ts | 81 -- .../test/tool-discovery/initial-tools.test.ts | 78 -- .../test/tool-discovery/persistence.test.ts | 5 - .../test/tool-discovery/subagent.test.ts | 39 - .../test/tools/auto-generated-guard.test.ts | 6 - .../test/tools/exit-plan-mode.test.ts | 6 - .../test/tools/gh-renderer.test.ts | 194 ----- .../test/tools/schema-validation.test.ts | 132 --- .../test/tools/search-internal-urls.test.ts | 72 +- .../test/tools/search-tool-bm25.test.ts | 184 +---- .../test/tools/web-scrapers/academic.test.ts | 20 - .../tools/web-scrapers/git-hosting.test.ts | 67 -- .../web-scrapers/package-managers.test.ts | 60 -- .../web-scrapers/social-extended.test.ts | 100 --- .../test/tools/web-scrapers/social.test.ts | 145 ---- .../tools/web-scrapers/stackexchange.test.ts | 8 - .../test/tools/web-search-anthropic.test.ts | 131 --- .../test/tools/web-search-exa.test.ts | 20 - .../test/tools/web-search-tavily.test.ts | 8 - .../coding-agent/test/tools/yield.test.ts | 12 - packages/natives/native/index.d.ts | 4 - packages/natives/test/issue-892-repro.test.ts | 19 +- packages/tui/test/stdin-buffer.test.ts | 86 -- .../test/formatter.test.ts | 12 - packages/utils/test/spacing.test.ts | 11 +- 87 files changed, 33 insertions(+), 4300 deletions(-) delete mode 100644 packages/ai/test/alibaba-coding-plan-provider.test.ts delete mode 100644 packages/ai/test/empty.test.ts delete mode 100644 packages/ai/test/github-copilot-claude-messages-routing.test.ts delete mode 100644 packages/ai/test/gitlab-duo-model-mapping.test.ts delete mode 100644 packages/ai/test/kilo-provider.test.ts delete mode 100644 packages/ai/test/zen.test.ts delete mode 100644 packages/coding-agent/test/args.test.ts delete mode 100644 packages/coding-agent/test/compaction-hooks-example.test.ts delete mode 100644 packages/coding-agent/test/core/python-prelude.test.ts delete mode 100644 packages/coding-agent/test/cycle-order-custom-roles.test.ts delete mode 100644 packages/coding-agent/test/hindsight-config.test.ts delete mode 100644 packages/coding-agent/test/marketplace/plugin-dir-roots.test.ts delete mode 100644 packages/coding-agent/test/output-block.test.ts delete mode 100644 packages/coding-agent/test/plan-mode/plan-mode-approved-prompt.test.ts delete mode 100644 packages/coding-agent/test/prompts/review-request.test.ts delete mode 100644 packages/coding-agent/test/rpc-mode-extension-ui.test.ts delete mode 100644 packages/coding-agent/test/session-color.test.ts delete mode 100644 packages/coding-agent/test/session-provider-section.test.ts delete mode 100644 packages/coding-agent/test/task/render-report-finding.test.ts delete mode 100644 packages/coding-agent/test/tools/gh-renderer.test.ts delete mode 100644 packages/coding-agent/test/tools/web-search-anthropic.test.ts delete mode 100644 packages/typescript-edit-benchmark/test/formatter.test.ts diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index b82a5e81d..eb37be0d9 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -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 }; diff --git a/packages/ai/test/alibaba-coding-plan-provider.test.ts b/packages/ai/test/alibaba-coding-plan-provider.test.ts deleted file mode 100644 index 50a5b6cfe..000000000 --- a/packages/ai/test/alibaba-coding-plan-provider.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index 252f9dfb0..60b51e6f1 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -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, diff --git a/packages/ai/test/api-registry.test.ts b/packages/ai/test/api-registry.test.ts index 31306df97..f62840076 100644 --- a/packages/ai/test/api-registry.test.ts +++ b/packages/ai/test/api-registry.test.ts @@ -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( diff --git a/packages/ai/test/apply-patch-freeform.test.ts b/packages/ai/test/apply-patch-freeform.test.ts index ca37a473c..3665712e4 100644 --- a/packages/ai/test/apply-patch-freeform.test.ts +++ b/packages/ai/test/apply-patch-freeform.test.ts @@ -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); }); diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts deleted file mode 100644 index 0c29787c3..000000000 --- a/packages/ai/test/empty.test.ts +++ /dev/null @@ -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(llm: Model, options: OptionsForApi = {}) { - // 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(llm: Model, options: OptionsForApi = {}) { - // 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(llm: Model, options: OptionsForApi = {}) { - // 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(llm: Model, options: OptionsForApi = {}) { - // 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 }, - ); - }); -}); diff --git a/packages/ai/test/github-copilot-claude-messages-routing.test.ts b/packages/ai/test/github-copilot-claude-messages-routing.test.ts deleted file mode 100644 index 9c42922df..000000000 --- a/packages/ai/test/github-copilot-claude-messages-routing.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/ai/test/gitlab-duo-model-mapping.test.ts b/packages/ai/test/gitlab-duo-model-mapping.test.ts deleted file mode 100644 index 8815a68c0..000000000 --- a/packages/ai/test/gitlab-duo-model-mapping.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/ai/test/google-gemini-cli-3x-thinking.test.ts b/packages/ai/test/google-gemini-cli-3x-thinking.test.ts index 06f73e7d2..9e1caf756 100644 --- a/packages/ai/test/google-gemini-cli-3x-thinking.test.ts +++ b/packages/ai/test/google-gemini-cli-3x-thinking.test.ts @@ -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) => { diff --git a/packages/ai/test/issue-887-repro.test.ts b/packages/ai/test/issue-887-repro.test.ts index 6ae43112b..7b4f91350 100644 --- a/packages/ai/test/issue-887-repro.test.ts +++ b/packages/ai/test/issue-887-repro.test.ts @@ -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 diff --git a/packages/ai/test/kilo-provider.test.ts b/packages/ai/test/kilo-provider.test.ts deleted file mode 100644 index 44421840b..000000000 --- a/packages/ai/test/kilo-provider.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/ai/test/model-thinking.test.ts b/packages/ai/test/model-thinking.test.ts index e80c1c355..ac0ac3dab 100644 --- a/packages/ai/test/model-thinking.test.ts +++ b/packages/ai/test/model-thinking.test.ts @@ -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(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; - 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[] = [ diff --git a/packages/ai/test/ollama-cloud-provider.test.ts b/packages/ai/test/ollama-cloud-provider.test.ts index 922fc9349..4c07d567e 100644 --- a/packages/ai/test/ollama-cloud-provider.test.ts +++ b/packages/ai/test/ollama-cloud-provider.test.ts @@ -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); diff --git a/packages/ai/test/openai-responses-cache-affinity.test.ts b/packages/ai/test/openai-responses-cache-affinity.test.ts index cc0d6e994..31a8cb7dc 100644 --- a/packages/ai/test/openai-responses-cache-affinity.test.ts +++ b/packages/ai/test/openai-responses-cache-affinity.test.ts @@ -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" }, - ]); - }); }); diff --git a/packages/ai/test/rate-limit-utils.test.ts b/packages/ai/test/rate-limit-utils.test.ts index 485f5bc53..264a0bb4c 100644 --- a/packages/ai/test/rate-limit-utils.test.ts +++ b/packages/ai/test/rate-limit-utils.test.ts @@ -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); - }); }); diff --git a/packages/ai/test/zen.test.ts b/packages/ai/test/zen.test.ts deleted file mode 100644 index 616ef3fe9..000000000 --- a/packages/ai/test/zen.test.ts +++ /dev/null @@ -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); - }); - }); -}); diff --git a/packages/coding-agent/test/agent-session-retry-fallback.test.ts b/packages/coding-agent/test/agent-session-retry-fallback.test.ts index 654139c4f..dc0cb1e83 100644 --- a/packages/coding-agent/test/agent-session-retry-fallback.test.ts +++ b/packages/coding-agent/test/agent-session-retry-fallback.test.ts @@ -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) { diff --git a/packages/coding-agent/test/agent-session-user-shortcut-hooks.test.ts b/packages/coding-agent/test/agent-session-user-shortcut-hooks.test.ts index dcfe78d7b..989498d4c 100644 --- a/packages/coding-agent/test/agent-session-user-shortcut-hooks.test.ts +++ b/packages/coding-agent/test/agent-session-user-shortcut-hooks.test.ts @@ -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), - }), - ); - }); }); diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts deleted file mode 100644 index 4c4cec434..000000000 --- a/packages/coding-agent/test/args.test.ts +++ /dev/null @@ -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"]); - }); - }); -}); diff --git a/packages/coding-agent/test/async-job-manager.test.ts b/packages/coding-agent/test/async-job-manager.test.ts index 1861d92da..cd910cce9 100644 --- a/packages/coding-agent/test/async-job-manager.test.ts +++ b/packages/coding-agent/test/async-job-manager.test.ts @@ -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(); - }); }); diff --git a/packages/coding-agent/test/autocomplete-max-visible.test.ts b/packages/coding-agent/test/autocomplete-max-visible.test.ts index 3ebd767b9..56fc688a5 100644 --- a/packages/coding-agent/test/autocomplete-max-visible.test.ts +++ b/packages/coding-agent/test/autocomplete-max-visible.test.ts @@ -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); diff --git a/packages/coding-agent/test/block-images.test.ts b/packages/coding-agent/test/block-images.test.ts index cec63ea1d..bb8d8319e 100644 --- a/packages/coding-agent/test/block-images.test.ts +++ b/packages/coding-agent/test/block-images.test.ts @@ -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; diff --git a/packages/coding-agent/test/compaction-hooks-example.test.ts b/packages/coding-agent/test/compaction-hooks-example.test.ts deleted file mode 100644 index c88471bc5..000000000 --- a/packages/coding-agent/test/compaction-hooks-example.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/coding-agent/test/compaction.test.ts b/packages/coding-agent/test/compaction.test.ts index 433e4bfc3..cb3b2f2cb 100644 --- a/packages/coding-agent/test/compaction.test.ts +++ b/packages/coding-agent/test/compaction.test.ts @@ -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); - }); }); // ============================================================================ diff --git a/packages/coding-agent/test/config-cli.test.ts b/packages/coding-agent/test/config-cli.test.ts index 7e60997ff..ae67a50a2 100644 --- a/packages/coding-agent/test/config-cli.test.ts +++ b/packages/coding-agent/test/config-cli.test.ts @@ -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; - - 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(() => {}); diff --git a/packages/coding-agent/test/config-spacing.test.ts b/packages/coding-agent/test/config-spacing.test.ts index 18e3d0de6..bab13e82e 100644 --- a/packages/coding-agent/test/config-spacing.test.ts +++ b/packages/coding-agent/test/config-spacing.test.ts @@ -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); diff --git a/packages/coding-agent/test/core/js-executor.test.ts b/packages/coding-agent/test/core/js-executor.test.ts index 5924aa764..25132d9f2 100644 --- a/packages/coding-agent/test/core/js-executor.test.ts +++ b/packages/coding-agent/test/core/js-executor.test.ts @@ -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 } }, - ]); - }); }); diff --git a/packages/coding-agent/test/core/python-kernel-env.test.ts b/packages/coding-agent/test/core/python-kernel-env.test.ts index 1d56f0f46..9d44d3c15 100644 --- a/packages/coding-agent/test/core/python-kernel-env.test.ts +++ b/packages/coding-agent/test/core/python-kernel-env.test.ts @@ -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 = { - 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"); - }); }); diff --git a/packages/coding-agent/test/core/python-prelude.test.ts b/packages/coding-agent/test/core/python-prelude.test.ts deleted file mode 100644 index 39b4757e8..000000000 --- a/packages/coding-agent/test/core/python-prelude.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/coding-agent/test/cycle-order-custom-roles.test.ts b/packages/coding-agent/test/cycle-order-custom-roles.test.ts deleted file mode 100644 index f28416e16..000000000 --- a/packages/coding-agent/test/cycle-order-custom-roles.test.ts +++ /dev/null @@ -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; - expect(modelTags["custom-fast"]).toEqual({ - name: "Fast Custom", - color: "warning", - }); - }); -}); diff --git a/packages/coding-agent/test/debug/log-viewer.test.ts b/packages/coding-agent/test/debug/log-viewer.test.ts index 121c73102..16bce1360 100644 --- a/packages/coding-agent/test/debug/log-viewer.test.ts +++ b/packages/coding-agent/test/debug/log-viewer.test.ts @@ -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 = [ diff --git a/packages/coding-agent/test/edit-streaming-preview.test.ts b/packages/coding-agent/test/edit-streaming-preview.test.ts index c50086d9e..0a2b4a127 100644 --- a/packages/coding-agent/test/edit-streaming-preview.test.ts +++ b/packages/coding-agent/test/edit-streaming-preview.test.ts @@ -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; diff --git a/packages/coding-agent/test/extensibility/custom-commands/ci-green.test.ts b/packages/coding-agent/test/extensibility/custom-commands/ci-green.test.ts index e2783c5e8..1133601e3 100644 --- a/packages/coding-agent/test/extensibility/custom-commands/ci-green.test.ts +++ b/packages/coding-agent/test/extensibility/custom-commands/ci-green.test.ts @@ -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()); diff --git a/packages/coding-agent/test/hindsight-config.test.ts b/packages/coding-agent/test/hindsight-config.test.ts deleted file mode 100644 index bf24af1bf..000000000 --- a/packages/coding-agent/test/hindsight-config.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/coding-agent/test/image-b64poly.test.ts b/packages/coding-agent/test/image-b64poly.test.ts index 00d975fec..4b2fefbdb 100644 --- a/packages/coding-agent/test/image-b64poly.test.ts +++ b/packages/coding-agent/test/image-b64poly.test.ts @@ -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=="); diff --git a/packages/coding-agent/test/image-input-normalization.test.ts b/packages/coding-agent/test/image-input-normalization.test.ts index 97eb8d1cf..495a3df70 100644 --- a/packages/coding-agent/test/image-input-normalization.test.ts +++ b/packages/coding-agent/test/image-input-normalization.test.ts @@ -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") diff --git a/packages/coding-agent/test/lm-studio-fix.test.ts b/packages/coding-agent/test/lm-studio-fix.test.ts index e16c40432..fdcf5409f 100644 --- a/packages/coding-agent/test/lm-studio-fix.test.ts +++ b/packages/coding-agent/test/lm-studio-fix.test.ts @@ -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 }); - }); - }); }); diff --git a/packages/coding-agent/test/marketplace/cache.test.ts b/packages/coding-agent/test/marketplace/cache.test.ts index b68559e4e..681ca591a 100644 --- a/packages/coding-agent/test/marketplace/cache.test.ts +++ b/packages/coding-agent/test/marketplace/cache.test.ts @@ -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/); }); diff --git a/packages/coding-agent/test/marketplace/manager.test.ts b/packages/coding-agent/test/marketplace/manager.test.ts index 182f17d5b..f974bf602 100644 --- a/packages/coding-agent/test/marketplace/manager.test.ts +++ b/packages/coding-agent/test/marketplace/manager.test.ts @@ -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 () => { diff --git a/packages/coding-agent/test/marketplace/parse-internal-url.test.ts b/packages/coding-agent/test/marketplace/parse-internal-url.test.ts index ca3c456dc..465a4a46a 100644 --- a/packages/coding-agent/test/marketplace/parse-internal-url.test.ts +++ b/packages/coding-agent/test/marketplace/parse-internal-url.test.ts @@ -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:"); diff --git a/packages/coding-agent/test/marketplace/plugin-dir-roots.test.ts b/packages/coding-agent/test/marketplace/plugin-dir-roots.test.ts deleted file mode 100644 index 39f9727d8..000000000 --- a/packages/coding-agent/test/marketplace/plugin-dir-roots.test.ts +++ /dev/null @@ -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__"); - }); -}); diff --git a/packages/coding-agent/test/mcp-roots-list.test.ts b/packages/coding-agent/test/mcp-roots-list.test.ts index dd88e00ce..8a4fe2f9b 100644 --- a/packages/coding-agent/test/mcp-roots-list.test.ts +++ b/packages/coding-agent/test/mcp-roots-list.test.ts @@ -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"); diff --git a/packages/coding-agent/test/memory-backend-resolve.test.ts b/packages/coding-agent/test/memory-backend-resolve.test.ts index 68cdc8aa9..581263bb3 100644 --- a/packages/coding-agent/test/memory-backend-resolve.test.ts +++ b/packages/coding-agent/test/memory-backend-resolve.test.ts @@ -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 }); diff --git a/packages/coding-agent/test/model-registry-runtime-provider.test.ts b/packages/coding-agent/test/model-registry-runtime-provider.test.ts index 75f915452..9538a0e34 100644 --- a/packages/coding-agent/test/model-registry-runtime-provider.test.ts +++ b/packages/coding-agent/test/model-registry-runtime-provider.test.ts @@ -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"; diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 505083651..18961636c 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -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()}`); diff --git a/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts b/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts index 89b609779..d75d9e225 100644 --- a/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts +++ b/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts @@ -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"); diff --git a/packages/coding-agent/test/modes/controllers/btw-controller.test.ts b/packages/coding-agent/test/modes/controllers/btw-controller.test.ts index 01df6ce19..9a9e2e7e8 100644 --- a/packages/coding-agent/test/modes/controllers/btw-controller.test.ts +++ b/packages/coding-agent/test/modes/controllers/btw-controller.test.ts @@ -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; diff --git a/packages/coding-agent/test/modes/controllers/event-controller-message-start.test.ts b/packages/coding-agent/test/modes/controllers/event-controller-message-start.test.ts index 30a529715..50953cc61 100644 --- a/packages/coding-agent/test/modes/controllers/event-controller-message-start.test.ts +++ b/packages/coding-agent/test/modes/controllers/event-controller-message-start.test.ts @@ -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(); diff --git a/packages/coding-agent/test/output-block.test.ts b/packages/coding-agent/test/output-block.test.ts deleted file mode 100644 index bec2419ee..000000000 --- a/packages/coding-agent/test/output-block.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/coding-agent/test/plan-mode/plan-mode-approved-prompt.test.ts b/packages/coding-agent/test/plan-mode/plan-mode-approved-prompt.test.ts deleted file mode 100644 index fe654ab8f..000000000 --- a/packages/coding-agent/test/plan-mode/plan-mode-approved-prompt.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/coding-agent/test/plugin-command.test.ts b/packages/coding-agent/test/plugin-command.test.ts index 709ac52a6..42bd1df3b 100644 --- a/packages/coding-agent/test/plugin-command.test.ts +++ b/packages/coding-agent/test/plugin-command.test.ts @@ -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/); diff --git a/packages/coding-agent/test/prompt-templates.test.ts b/packages/coding-agent/test/prompt-templates.test.ts index 099ba190b..af317fbbd 100644 --- a/packages/coding-agent/test/prompt-templates.test.ts +++ b/packages/coding-agent/test/prompt-templates.test.ts @@ -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"]); - }); }); // ============================================================================ diff --git a/packages/coding-agent/test/prompts/review-request.test.ts b/packages/coding-agent/test/prompts/review-request.test.ts deleted file mode 100644 index a3d636a70..000000000 --- a/packages/coding-agent/test/prompts/review-request.test.ts +++ /dev/null @@ -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("); - }); -}); diff --git a/packages/coding-agent/test/rpc-mode-extension-ui.test.ts b/packages/coding-agent/test/rpc-mode-extension-ui.test.ts deleted file mode 100644 index 23426eaf9..000000000 --- a/packages/coding-agent/test/rpc-mode-extension-ui.test.ts +++ /dev/null @@ -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(); - 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(); - 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); - }); -}); diff --git a/packages/coding-agent/test/secrets-obfuscator.test.ts b/packages/coding-agent/test/secrets-obfuscator.test.ts index 925e78e34..e4ad9b902 100644 --- a/packages/coding-agent/test/secrets-obfuscator.test.ts +++ b/packages/coding-agent/test/secrets-obfuscator.test.ts @@ -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+"); diff --git a/packages/coding-agent/test/session-color.test.ts b/packages/coding-agent/test/session-color.test.ts deleted file mode 100644 index f8184bb3a..000000000 --- a/packages/coding-agent/test/session-color.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/coding-agent/test/session-manager/file-operations.test.ts b/packages/coding-agent/test/session-manager/file-operations.test.ts index f5e64fcbc..96d30aa53 100644 --- a/packages/coding-agent/test/session-manager/file-operations.test.ts +++ b/packages/coding-agent/test/session-manager/file-operations.test.ts @@ -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'); diff --git a/packages/coding-agent/test/session-provider-section.test.ts b/packages/coding-agent/test/session-provider-section.test.ts deleted file mode 100644 index 59d96a8ae..000000000 --- a/packages/coding-agent/test/session-provider-section.test.ts +++ /dev/null @@ -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:"); - }); -}); diff --git a/packages/coding-agent/test/slash-commands/force.test.ts b/packages/coding-agent/test/slash-commands/force.test.ts index b59fc7fdc..88f27751c 100644 --- a/packages/coding-agent/test/slash-commands/force.test.ts +++ b/packages/coding-agent/test/slash-commands/force.test.ts @@ -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.'); - }); }); diff --git a/packages/coding-agent/test/status-line-git-utils.test.ts b/packages/coding-agent/test/status-line-git-utils.test.ts index 4a2a7e90c..3223acfe1 100644 --- a/packages/coding-agent/test/status-line-git-utils.test.ts +++ b/packages/coding-agent/test/status-line-git-utils.test.ts @@ -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(); }); diff --git a/packages/coding-agent/test/streaming-output.test.ts b/packages/coding-agent/test/streaming-output.test.ts index 9454d3fec..675deb2ac 100644 --- a/packages/coding-agent/test/streaming-output.test.ts +++ b/packages/coding-agent/test/streaming-output.test.ts @@ -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"; diff --git a/packages/coding-agent/test/task/render-report-finding.test.ts b/packages/coding-agent/test/task/render-report-finding.test.ts deleted file mode 100644 index 1145bd946..000000000 --- a/packages/coding-agent/test/task/render-report-finding.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/coding-agent/test/tool-choice-queue.test.ts b/packages/coding-agent/test/tool-choice-queue.test.ts index f437df6d2..52a744703 100644 --- a/packages/coding-agent/test/tool-choice-queue.test.ts +++ b/packages/coding-agent/test/tool-choice-queue.test.ts @@ -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; diff --git a/packages/coding-agent/test/tool-discovery/initial-tools.test.ts b/packages/coding-agent/test/tool-discovery/initial-tools.test.ts index 50fbca9ea..d37c3dd9b 100644 --- a/packages/coding-agent/test/tool-discovery/initial-tools.test.ts +++ b/packages/coding-agent/test/tool-discovery/initial-tools.test.ts @@ -60,14 +60,6 @@ async function getToolMetadata(): Promise { - 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); diff --git a/packages/coding-agent/test/tool-discovery/persistence.test.ts b/packages/coding-agent/test/tool-discovery/persistence.test.ts index 5b0f9a7a6..d95fc420d 100644 --- a/packages/coding-agent/test/tool-discovery/persistence.test.ts +++ b/packages/coding-agent/test/tool-discovery/persistence.test.ts @@ -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 diff --git a/packages/coding-agent/test/tool-discovery/subagent.test.ts b/packages/coding-agent/test/tool-discovery/subagent.test.ts index ba5301fbd..eb34191d9 100644 --- a/packages/coding-agent/test/tool-discovery/subagent.test.ts +++ b/packages/coding-agent/test/tool-discovery/subagent.test.ts @@ -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"); diff --git a/packages/coding-agent/test/tools/auto-generated-guard.test.ts b/packages/coding-agent/test/tools/auto-generated-guard.test.ts index 771e55d04..90dbc393a 100644 --- a/packages/coding-agent/test/tools/auto-generated-guard.test.ts +++ b/packages/coding-agent/test/tools/auto-generated-guard.test.ts @@ -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;"); diff --git a/packages/coding-agent/test/tools/exit-plan-mode.test.ts b/packages/coding-agent/test/tools/exit-plan-mode.test.ts index 26c65e6ca..28fe15dd7 100644 --- a/packages/coding-agent/test/tools/exit-plan-mode.test.ts +++ b/packages/coding-agent/test/tools/exit-plan-mode.test.ts @@ -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" }); diff --git a/packages/coding-agent/test/tools/gh-renderer.test.ts b/packages/coding-agent/test/tools/gh-renderer.test.ts deleted file mode 100644 index 3f7071fd5..000000000 --- a/packages/coding-agent/test/tools/gh-renderer.test.ts +++ /dev/null @@ -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); - } - }); -}); diff --git a/packages/coding-agent/test/tools/schema-validation.test.ts b/packages/coding-agent/test/tools/schema-validation.test.ts index 54e93fc07..311ebff61 100644 --- a/packages/coding-agent/test/tools/schema-validation.test.ts +++ b/packages/coding-agent/test/tools/schema-validation.test.ts @@ -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([]); - }); }); diff --git a/packages/coding-agent/test/tools/search-internal-urls.test.ts b/packages/coding-agent/test/tools/search-internal-urls.test.ts index 21be2d6a4..6eb83c5b2 100644 --- a/packages/coding-agent/test/tools/search-internal-urls.test.ts +++ b/packages/coding-agent/test/tools/search-internal-urls.test.ts @@ -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 { @@ -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( diff --git a/packages/coding-agent/test/tools/search-tool-bm25.test.ts b/packages/coding-agent/test/tools/search-tool-bm25.test.ts index 0e22f5010..392b639e3 100644 --- a/packages/coding-agent/test/tools/search-tool-bm25.test.ts +++ b/packages/coding-agent/test/tools/search-tool-bm25.test.ts @@ -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); diff --git a/packages/coding-agent/test/tools/web-scrapers/academic.test.ts b/packages/coding-agent/test/tools/web-scrapers/academic.test.ts index ad15f7994..724faef00 100644 --- a/packages/coding-agent/test/tools/web-scrapers/academic.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/academic.test.ts @@ -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); diff --git a/packages/coding-agent/test/tools/web-scrapers/git-hosting.test.ts b/packages/coding-agent/test/tools/web-scrapers/git-hosting.test.ts index 78a08cb04..d54a9ee47 100644 --- a/packages/coding-agent/test/tools/web-scrapers/git-hosting.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/git-hosting.test.ts @@ -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(); - }); }); // ============================================================================= diff --git a/packages/coding-agent/test/tools/web-scrapers/package-managers.test.ts b/packages/coding-agent/test/tools/web-scrapers/package-managers.test.ts index ac81231fe..4fedcf761 100644 --- a/packages/coding-agent/test/tools/web-scrapers/package-managers.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/package-managers.test.ts @@ -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(); diff --git a/packages/coding-agent/test/tools/web-scrapers/social-extended.test.ts b/packages/coding-agent/test/tools/web-scrapers/social-extended.test.ts index b6a91de53..278aaf529 100644 --- a/packages/coding-agent/test/tools/web-scrapers/social-extended.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/social-extended.test.ts @@ -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(); - }); }); diff --git a/packages/coding-agent/test/tools/web-scrapers/social.test.ts b/packages/coding-agent/test/tools/web-scrapers/social.test.ts index 38d0832b0..05df4cad8 100644 --- a/packages/coding-agent/test/tools/web-scrapers/social.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/social.test.ts @@ -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:"); - } - }); }); diff --git a/packages/coding-agent/test/tools/web-scrapers/stackexchange.test.ts b/packages/coding-agent/test/tools/web-scrapers/stackexchange.test.ts index 5da21c3f4..ae4c9a4fe 100644 --- a/packages/coding-agent/test/tools/web-scrapers/stackexchange.test.ts +++ b/packages/coding-agent/test/tools/web-scrapers/stackexchange.test.ts @@ -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 () => { diff --git a/packages/coding-agent/test/tools/web-search-anthropic.test.ts b/packages/coding-agent/test/tools/web-search-anthropic.test.ts deleted file mode 100644 index 8c698aa06..000000000 --- a/packages/coding-agent/test/tools/web-search-anthropic.test.ts +++ /dev/null @@ -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 | 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(); - }); -}); diff --git a/packages/coding-agent/test/tools/web-search-exa.test.ts b/packages/coding-agent/test/tools/web-search-exa.test.ts index e606a4db2..330328555 100644 --- a/packages/coding-agent/test/tools/web-search-exa.test.ts +++ b/packages/coding-agent/test/tools/web-search-exa.test.ts @@ -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 }, diff --git a/packages/coding-agent/test/tools/web-search-tavily.test.ts b/packages/coding-agent/test/tools/web-search-tavily.test.ts index ea5f8eb5d..86b4981f7 100644 --- a/packages/coding-agent/test/tools/web-search-tavily.test.ts +++ b/packages/coding-agent/test/tools/web-search-tavily.test.ts @@ -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 | null = null; diff --git a/packages/coding-agent/test/tools/yield.test.ts b/packages/coding-agent/test/tools/yield.test.ts index 1a285cb3b..aaec516e6 100644 --- a/packages/coding-agent/test/tools/yield.test.ts +++ b/packages/coding-agent/test/tools/yield.test.ts @@ -35,18 +35,6 @@ function getSuccessDataSchema(parameters: Record): Record { - it("exposes top-level object parameters with required result union", () => { - const tool = new YieldTool(createSession()); - const schema = tool.parameters as { - type?: string; - properties?: Record; - 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); diff --git a/packages/natives/native/index.d.ts b/packages/natives/native/index.d.ts index 32e56d684..74ac86dc8 100644 --- a/packages/natives/native/index.d.ts +++ b/packages/natives/native/index.d.ts @@ -1150,8 +1150,6 @@ export interface ShellExecuteOptions { env?: Record /** Environment variables to apply once per session. */ sessionEnv?: Record - /** 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 - /** Run the command attached to a PTY. */ - pty?: boolean /** Timeout in milliseconds before cancelling the command. */ timeoutMs?: number /** Abort signal for cancelling the operation. */ diff --git a/packages/natives/test/issue-892-repro.test.ts b/packages/natives/test/issue-892-repro.test.ts index 58519d175..5c3400839 100644 --- a/packages/natives/test/issue-892-repro.test.ts +++ b/packages/natives/test/issue-892-repro.test.ts @@ -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" }); - }); }); diff --git a/packages/tui/test/stdin-buffer.test.ts b/packages/tui/test/stdin-buffer.test.ts index 214ed7407..e1cac2aa6 100644 --- a/packages/tui/test/stdin-buffer.test.ts +++ b/packages/tui/test/stdin-buffer.test.ts @@ -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"); diff --git a/packages/typescript-edit-benchmark/test/formatter.test.ts b/packages/typescript-edit-benchmark/test/formatter.test.ts deleted file mode 100644 index bab07891d..000000000 --- a/packages/typescript-edit-benchmark/test/formatter.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/utils/test/spacing.test.ts b/packages/utils/test/spacing.test.ts index 2635d4d48..d540a0825 100644 --- a/packages/utils/test/spacing.test.ts +++ b/packages/utils/test/spacing.test.ts @@ -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 });