diff --git a/packages/ai/test/auth-storage-codex-selection.test.ts b/packages/ai/test/auth-storage-codex-selection.test.ts index 358143c80..365e7da0b 100644 --- a/packages/ai/test/auth-storage-codex-selection.test.ts +++ b/packages/ai/test/auth-storage-codex-selection.test.ts @@ -1746,36 +1746,39 @@ describe("AuthStorage codex oauth ranking", () => { test.each([ ["gpt-5.6-terra", "free", "enterprise"], ["gpt-5.6-terra-pro", "go", "pro"], - ])("%s keeps a less-used %s account in ordinary ranking ahead of %s", async (modelId, lowUsagePlan, highUsagePlan) => { - if (!authStorage) throw new Error("test setup failed"); + ])( + "%s keeps a less-used %s account in ordinary ranking ahead of %s", + async (modelId, lowUsagePlan, highUsagePlan) => { + if (!authStorage) throw new Error("test setup failed"); - await authStorage.set("openai-codex", [ - { type: "oauth", ...createCredential("acct-low-usage", "low-usage@example.com") }, - { type: "oauth", ...createCredential("acct-high-usage", "high-usage@example.com") }, - ]); + await authStorage.set("openai-codex", [ + { type: "oauth", ...createCredential("acct-low-usage", "low-usage@example.com") }, + { type: "oauth", ...createCredential("acct-high-usage", "high-usage@example.com") }, + ]); - usageByAccount.set( - "acct-low-usage", - createCodexUsageReport({ - accountId: "acct-low-usage", - primary: { usedFraction: 0.01, resetInMs: 30 * 60 * 1000 }, - secondary: { usedFraction: 0.01, resetInMs: 6 * 24 * 60 * 60 * 1000 }, - metadata: { planType: lowUsagePlan, email: "low-usage@example.com" }, - }), - ); - usageByAccount.set( - "acct-high-usage", - createCodexUsageReport({ - accountId: "acct-high-usage", - primary: { usedFraction: 0.8, resetInMs: 30 * 60 * 1000 }, - secondary: { usedFraction: 0.8, resetInMs: 6 * 24 * 60 * 60 * 1000 }, - metadata: { planType: highUsagePlan, email: "high-usage@example.com" }, - }), - ); + usageByAccount.set( + "acct-low-usage", + createCodexUsageReport({ + accountId: "acct-low-usage", + primary: { usedFraction: 0.01, resetInMs: 30 * 60 * 1000 }, + secondary: { usedFraction: 0.01, resetInMs: 6 * 24 * 60 * 60 * 1000 }, + metadata: { planType: lowUsagePlan, email: "low-usage@example.com" }, + }), + ); + usageByAccount.set( + "acct-high-usage", + createCodexUsageReport({ + accountId: "acct-high-usage", + primary: { usedFraction: 0.8, resetInMs: 30 * 60 * 1000 }, + secondary: { usedFraction: 0.8, resetInMs: 6 * 24 * 60 * 60 * 1000 }, + metadata: { planType: highUsagePlan, email: "high-usage@example.com" }, + }), + ); - const apiKey = await authStorage.getApiKey("openai-codex", undefined, { modelId }); - expect(apiKey).toBe("api-acct-low-usage"); - }); + const apiKey = await authStorage.getApiKey("openai-codex", undefined, { modelId }); + expect(apiKey).toBe("api-acct-low-usage"); + }, + ); test("keeps an eligible Codex session credential when usage headroom makes its sibling rank better", async () => { if (!authStorage) throw new Error("test setup failed"); @@ -2362,62 +2365,65 @@ describe("AuthStorage codex oauth ranking", () => { test.each([ ["gpt-5.6-sol", 0.06, 0.09, true, false, 1, 1, false, true, "spark"], ["gpt-5.3-codex-spark", 1, 1, false, true, 0.06, 0.09, true, false, "chat"], - ] as const)("reports %s healthy after splitting a legacy shared block when only its meter has headroom", async (modelId, chatPrimary, chatSecondary, chatAllowed, chatLimitReached, sparkPrimary, sparkSecondary, sparkAllowed, sparkLimitReached, remainingBlockScope) => { - if (!authStorage || !store?.listCredentialBlocks) throw new Error("test setup failed"); - await authStorage.set("openai-codex", [ - { type: "oauth", ...createCredential("acct-legacy-meter", "legacy-meter@example.com") }, - ]); - const [row] = store.listAuthCredentials("openai-codex"); - if (!row) throw new Error("expected credential row"); - const blockedUntilMs = Date.now() + WEEK_MS; - insertLegacyCodexSharedBlock( - dbPath, - row.id, - blockedUntilMs, - Math.floor((Date.now() - STALE_BLOCK_GUARD_MS) / 1000), - ); - usageByAccount.set( - "acct-legacy-meter", - addSparkUsage( - createCodexUsageReport({ - accountId: "acct-legacy-meter", - primary: { usedFraction: chatPrimary, resetInMs: FIVE_HOUR_MS }, - secondary: { usedFraction: chatSecondary, resetInMs: WEEK_MS }, - metadata: { - allowed: chatAllowed, - limitReached: chatLimitReached, - planType: "pro", - email: "legacy-meter@example.com", + ] as const)( + "reports %s healthy after splitting a legacy shared block when only its meter has headroom", + async (modelId, chatPrimary, chatSecondary, chatAllowed, chatLimitReached, sparkPrimary, sparkSecondary, sparkAllowed, sparkLimitReached, remainingBlockScope) => { + if (!authStorage || !store?.listCredentialBlocks) throw new Error("test setup failed"); + await authStorage.set("openai-codex", [ + { type: "oauth", ...createCredential("acct-legacy-meter", "legacy-meter@example.com") }, + ]); + const [row] = store.listAuthCredentials("openai-codex"); + if (!row) throw new Error("expected credential row"); + const blockedUntilMs = Date.now() + WEEK_MS; + insertLegacyCodexSharedBlock( + dbPath, + row.id, + blockedUntilMs, + Math.floor((Date.now() - STALE_BLOCK_GUARD_MS) / 1000), + ); + usageByAccount.set( + "acct-legacy-meter", + addSparkUsage( + createCodexUsageReport({ accountId: "acct-legacy-meter", + primary: { usedFraction: chatPrimary, resetInMs: FIVE_HOUR_MS }, + secondary: { usedFraction: chatSecondary, resetInMs: WEEK_MS }, + metadata: { + allowed: chatAllowed, + limitReached: chatLimitReached, + planType: "pro", + email: "legacy-meter@example.com", + accountId: "acct-legacy-meter", + }, + }), + sparkPrimary, + sparkSecondary, + { allowed: sparkAllowed, limitReached: sparkLimitReached }, + ), + ); + + const health = await authStorage.getModelUsageHealth("openai-codex", { + modelId, + reserveFraction: 0.1, + }); + + expect(health).toMatchObject({ + state: "healthy", + accounts: [ + { + credentialId: row.id, + credentialType: "oauth", + state: "healthy", }, - }), - sparkPrimary, - sparkSecondary, - { allowed: sparkAllowed, limitReached: sparkLimitReached }, - ), - ); - - const health = await authStorage.getModelUsageHealth("openai-codex", { - modelId, - reserveFraction: 0.1, - }); - - expect(health).toMatchObject({ - state: "healthy", - accounts: [ - { - credentialId: row.id, - credentialType: "oauth", - state: "healthy", - }, - ], - }); - expect(health.accounts[0]?.remainingFraction).toBeCloseTo(0.91, 10); - expect(store.listCredentialBlocks([row.id]).map(block => [block.blockScope, block.blockedUntilMs])).toEqual([ - [remainingBlockScope, blockedUntilMs], - ]); - expect(readLegacyCodexSharedBlock(dbPath, row.id)).toBe(blockedUntilMs); - }); + ], + }); + expect(health.accounts[0]?.remainingFraction).toBeCloseTo(0.91, 10); + expect(store.listCredentialBlocks([row.id]).map(block => [block.blockScope, block.blockedUntilMs])).toEqual([ + [remainingBlockScope, blockedUntilMs], + ]); + expect(readLegacyCodexSharedBlock(dbPath, row.id)).toBe(blockedUntilMs); + }, + ); test("deletes only the recovered persisted Codex meter block", async () => { if (!authStorage || !store?.upsertCredentialBlock || !store.getCredentialBlock) { diff --git a/packages/ai/test/openai-codex-responses-lite.test.ts b/packages/ai/test/openai-codex-responses-lite.test.ts index bc54b79b4..3f4ff7a74 100644 --- a/packages/ai/test/openai-codex-responses-lite.test.ts +++ b/packages/ai/test/openai-codex-responses-lite.test.ts @@ -201,25 +201,24 @@ describe("openai-codex reasoning.context", () => { // gpt-5.1-codex / gpt-5.3-codex / gpt-5.3-codex-spark reject `all_turns` // ("Unsupported value: 'all_turns' is not supported with this model"). - it.each([ - "gpt-5.1-codex", - "gpt-5.3-codex", - "gpt-5.3-codex-spark", - ])("omits the all_turns default for pre-5.4 model %s", async modelId => { - const model = createCodexModel(modelId); + it.each(["gpt-5.1-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark"])( + "omits the all_turns default for pre-5.4 model %s", + async modelId => { + const model = createCodexModel(modelId); - const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" }); - expect(defaulted.reasoning).toBeDefined(); - expect(defaulted.reasoning?.context).toBeUndefined(); - expect("context" in (defaulted.reasoning ?? {})).toBe(false); + const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" }); + expect(defaulted.reasoning).toBeDefined(); + expect(defaulted.reasoning?.context).toBeUndefined(); + expect("context" in (defaulted.reasoning ?? {})).toBe(false); - // A supported override (current_turn/auto) is still honored. - const overridden = await transformRequestBody({ model: model.id }, model, { - reasoningEffort: "medium", - reasoningContext: "current_turn", - }); - expect(overridden.reasoning?.context).toBe("current_turn"); - }); + // A supported override (current_turn/auto) is still honored. + const overridden = await transformRequestBody({ model: model.id }, model, { + reasoningEffort: "medium", + reasoningContext: "current_turn", + }); + expect(overridden.reasoning?.context).toBe("current_turn"); + }, + ); it("suppresses an explicit all_turns override on a pre-5.4 model", async () => { const model = createCodexModel("gpt-5.3-codex-spark"); @@ -255,24 +254,23 @@ describe("openai-codex reasoning.summary", () => { // gpt-5.1-codex / gpt-5.3-codex / gpt-5.3-codex-spark reject `reasoning.summary` // ("Unsupported parameter: 'reasoning.summary' is not supported with this model"). - it.each([ - "gpt-5.1-codex", - "gpt-5.3-codex", - "gpt-5.3-codex-spark", - ])("omits reasoning.summary for pre-5.4 model %s", async modelId => { - const model = createCodexModel(modelId); + it.each(["gpt-5.1-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark"])( + "omits reasoning.summary for pre-5.4 model %s", + async modelId => { + const model = createCodexModel(modelId); - const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" }); - expect(defaulted.reasoning).toBeDefined(); - expect("summary" in (defaulted.reasoning ?? {})).toBe(false); + const defaulted = await transformRequestBody({ model: model.id }, model, { reasoningEffort: "medium" }); + expect(defaulted.reasoning).toBeDefined(); + expect("summary" in (defaulted.reasoning ?? {})).toBe(false); - // Even an explicit summary level is suppressed on unsupported ids. - const forced = await transformRequestBody({ model: model.id }, model, { - reasoningEffort: "medium", - reasoningSummary: "detailed", - }); - expect("summary" in (forced.reasoning ?? {})).toBe(false); - }); + // Even an explicit summary level is suppressed on unsupported ids. + const forced = await transformRequestBody({ model: model.id }, model, { + reasoningEffort: "medium", + reasoningSummary: "detailed", + }); + expect("summary" in (forced.reasoning ?? {})).toBe(false); + }, + ); }); describe("openai-codex Responses Lite input shaping", () => { @@ -377,23 +375,21 @@ describe("openai-codex Responses Lite input shaping", () => { expect(disabled.tools).toBeUndefined(); }); - it.each([ - "gpt-5.3-codex-spark", - "gpt-5.6-luna", - "gpt-5.6-terra", - "gpt-5.6-sol", - ])("preserves a forced computer function through Lite for %s", async modelId => { - const model = createCodexModel(modelId); - const computer = { type: "function", name: "computer", parameters: { type: "object" } }; - const other = { type: "function", name: "read", parameters: { type: "object" } }; - const body = await transformRequestBody( - { model: model.id, tools: [computer, other], tool_choice: { type: "function", name: "computer" } }, - model, - { responsesLite: true }, - ); - expect(body.input?.[0]).toEqual({ type: "additional_tools", role: "developer", tools: [computer] }); - expect(body.tool_choice).toBe("required"); - }); + it.each(["gpt-5.3-codex-spark", "gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"])( + "preserves a forced computer function through Lite for %s", + async modelId => { + const model = createCodexModel(modelId); + const computer = { type: "function", name: "computer", parameters: { type: "object" } }; + const other = { type: "function", name: "read", parameters: { type: "object" } }; + const body = await transformRequestBody( + { model: model.id, tools: [computer, other], tool_choice: { type: "function", name: "computer" } }, + model, + { responsesLite: true }, + ); + expect(body.input?.[0]).toEqual({ type: "additional_tools", role: "developer", tools: [computer] }); + expect(body.tool_choice).toBe("required"); + }, + ); it("moves instructions and tools into input items under lite", async () => { const model = createCodexModel("gpt-5.6-terra"); diff --git a/packages/catalog/test/issue-1617-repro.test.ts b/packages/catalog/test/issue-1617-repro.test.ts index 101a2f93c..e80ae88da 100644 --- a/packages/catalog/test/issue-1617-repro.test.ts +++ b/packages/catalog/test/issue-1617-repro.test.ts @@ -38,23 +38,23 @@ describe("opencode-zen/-go resolver routes MiniMax M3 to openai-completions (iss const npmAnthropic: ModelsDevModel = { provider: { npm: "@ai-sdk/anthropic" }, tool_call: true }; describe("opencode-zen", () => { - test.each([ - ["minimax-m3"], - ["minimax-m3-free"], - ])("%s resolves to openai-completions on /v1/chat/completions", modelId => { - const resolved = zenDescriptor?.resolveApi?.(modelId, npmAnthropic); - expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_ZEN_BASE }); - }); + test.each([["minimax-m3"], ["minimax-m3-free"]])( + "%s resolves to openai-completions on /v1/chat/completions", + modelId => { + const resolved = zenDescriptor?.resolveApi?.(modelId, npmAnthropic); + expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_ZEN_BASE }); + }, + ); }); describe("opencode-go", () => { - test.each([ - ["minimax-m3"], - ["minimax-m3-free"], - ])("%s resolves to openai-completions on /v1/chat/completions", modelId => { - const resolved = goDescriptor?.resolveApi?.(modelId, npmAnthropic); - expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_GO_BASE }); - }); + test.each([["minimax-m3"], ["minimax-m3-free"]])( + "%s resolves to openai-completions on /v1/chat/completions", + modelId => { + const resolved = goDescriptor?.resolveApi?.(modelId, npmAnthropic); + expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_GO_BASE }); + }, + ); }); test("opencode-zen /v1/models refresh routes a freshly-discovered M3 to openai-completions", async () => { diff --git a/packages/catalog/test/issue-887-repro.test.ts b/packages/catalog/test/issue-887-repro.test.ts index 09f445c0e..e91d728a4 100644 --- a/packages/catalog/test/issue-887-repro.test.ts +++ b/packages/catalog/test/issue-887-repro.test.ts @@ -26,14 +26,13 @@ describe("opencode-go resolver routes 404-ing ids to openai-completions (issue # // would route them to /v1/messages on opencode.ai/zen/go which 404s. const npmAnthropic: ModelsDevModel = { provider: { npm: "@ai-sdk/anthropic" }, tool_call: true }; - test.each([ - ["minimax-m2.7"], - ["qwen3.5-plus"], - ["qwen3.6-plus"], - ])("%s resolves to openai-completions on /v1/chat/completions", modelId => { - const resolved = descriptor?.resolveApi?.(modelId, npmAnthropic); - expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_GO_BASE }); - }); + test.each([["minimax-m2.7"], ["qwen3.5-plus"], ["qwen3.6-plus"]])( + "%s resolves to openai-completions on /v1/chat/completions", + modelId => { + const resolved = descriptor?.resolveApi?.(modelId, npmAnthropic); + expect(resolved).toEqual({ api: "openai-completions", baseUrl: OPENCODE_GO_BASE }); + }, + ); test("minimax-m2.5 (control: works empirically) also resolves to openai-completions", () => { // models.dev currently lists minimax-m2.5 without an explicit provider.npm, diff --git a/packages/catalog/test/litellm-provider.test.ts b/packages/catalog/test/litellm-provider.test.ts index ea2f2c3a7..24183fd24 100644 --- a/packages/catalog/test/litellm-provider.test.ts +++ b/packages/catalog/test/litellm-provider.test.ts @@ -428,61 +428,60 @@ describe("LiteLLM provider discovery", () => { expect(models?.find(model => model.id === "params-tools")?.supportsTools).toBe(true); }); - test.each([ - ["all-team-models"], - ["all-proxy-models"], - ["no-default-models"], - ])("falls back from %s placeholder to v2 model info", async sentinelModelId => { - const calls: string[] = []; - const fetchMock = vi.fn(async (input: string | URL | Request) => { - const url = inputUrl(input); - calls.push(url); - if (url === MODELS_DEV_URL) { - return Response.json({}); - } - if (url === "http://primary:4000/model_group/info") { - return Response.json({ data: [makeLiteLLMSentinelPlaceholder(sentinelModelId)] }); - } - if (url === "http://primary:4000/v2/model/info") { - return Response.json({ - data: [ - { - model_name: "example-real-model", - model_info: { - max_input_tokens: 200_000, - max_output_tokens: 12_000, - supports_vision: false, - supports_reasoning: true, + test.each([["all-team-models"], ["all-proxy-models"], ["no-default-models"]])( + "falls back from %s placeholder to v2 model info", + async sentinelModelId => { + const calls: string[] = []; + const fetchMock = vi.fn(async (input: string | URL | Request) => { + const url = inputUrl(input); + calls.push(url); + if (url === MODELS_DEV_URL) { + return Response.json({}); + } + if (url === "http://primary:4000/model_group/info") { + return Response.json({ data: [makeLiteLLMSentinelPlaceholder(sentinelModelId)] }); + } + if (url === "http://primary:4000/v2/model/info") { + return Response.json({ + data: [ + { + model_name: "example-real-model", + model_info: { + max_input_tokens: 200_000, + max_output_tokens: 12_000, + supports_vision: false, + supports_reasoning: true, + }, }, - }, - ], - }); - } - if (url === "http://primary:4000/v1/models") { - throw new Error("/v1/models should not be called when v2 metadata succeeds"); - } - throw new Error(`Unexpected URL: ${url}`); - }) as FetchImpl; - const options = litellmModelManagerOptions({ - apiKey: "sk-rich", - baseUrl: "http://primary:4000/v1", - fetch: fetchMock, - }); + ], + }); + } + if (url === "http://primary:4000/v1/models") { + throw new Error("/v1/models should not be called when v2 metadata succeeds"); + } + throw new Error(`Unexpected URL: ${url}`); + }) as FetchImpl; + const options = litellmModelManagerOptions({ + apiKey: "sk-rich", + baseUrl: "http://primary:4000/v1", + fetch: fetchMock, + }); - const models = await options.fetchDynamicModels?.(); + const models = await options.fetchDynamicModels?.(); - expect(calls).toContain("http://primary:4000/model_group/info"); - expect(calls).toContain("http://primary:4000/v2/model/info"); - expect(calls).not.toContain("http://primary:4000/v1/models"); - expect(models?.map(model => model.id)).toEqual(["example-real-model"]); - expect(models?.[0]).toMatchObject({ - id: "example-real-model", - contextWindow: 200_000, - maxTokens: 12_000, - input: ["text"], - reasoning: true, - }); - }); + expect(calls).toContain("http://primary:4000/model_group/info"); + expect(calls).toContain("http://primary:4000/v2/model/info"); + expect(calls).not.toContain("http://primary:4000/v1/models"); + expect(models?.map(model => model.id)).toEqual(["example-real-model"]); + expect(models?.[0]).toMatchObject({ + id: "example-real-model", + contextWindow: 200_000, + maxTokens: 12_000, + input: ["text"], + reasoning: true, + }); + }, + ); test("filters all-team-models placeholder from mixed model_group info", async () => { const calls: string[] = []; diff --git a/packages/coding-agent/test/advisor/advisor.test.ts b/packages/coding-agent/test/advisor/advisor.test.ts index daf75dba0..66d76114a 100644 --- a/packages/coding-agent/test/advisor/advisor.test.ts +++ b/packages/coding-agent/test/advisor/advisor.test.ts @@ -4222,60 +4222,60 @@ describe("advisor", () => { expect(promptInputs[1]).toContain("keep me"); }); - it.each([ - "success", - "error", - ] as const)("releases blocked %s hooks so reset can run replacement work", async hookKind => { - const hookStarted = Promise.withResolvers(); - const releaseHook = Promise.withResolvers(); - const replacementPromptStarted = Promise.withResolvers(); - let promptCalls = 0; - let hookCalls = 0; - const blockHook = async () => { - if (++hookCalls !== 1) return; - hookStarted.resolve(); - await releaseHook.promise; - }; - const agent: AdvisorAgent = { - prompt: async () => { - promptCalls++; - if (promptCalls === 1 && hookKind === "error") throw new Error("provider failure"); - if (promptCalls === 2) replacementPromptStarted.resolve(); - }, - abort: () => {}, - reset: () => {}, - state: { messages: [] }, - }; - const runtime = new AdvisorRuntime(agent, { - snapshotMessages: () => [], - enqueueAdvice: () => {}, - ...(hookKind === "success" - ? { onTurnSuccess: blockHook } - : { - onTurnError: async () => { - await blockHook(); - return false; - }, - }), - }); + it.each(["success", "error"] as const)( + "releases blocked %s hooks so reset can run replacement work", + async hookKind => { + const hookStarted = Promise.withResolvers(); + const releaseHook = Promise.withResolvers(); + const replacementPromptStarted = Promise.withResolvers(); + let promptCalls = 0; + let hookCalls = 0; + const blockHook = async () => { + if (++hookCalls !== 1) return; + hookStarted.resolve(); + await releaseHook.promise; + }; + const agent: AdvisorAgent = { + prompt: async () => { + promptCalls++; + if (promptCalls === 1 && hookKind === "error") throw new Error("provider failure"); + if (promptCalls === 2) replacementPromptStarted.resolve(); + }, + abort: () => {}, + reset: () => {}, + state: { messages: [] }, + }; + const runtime = new AdvisorRuntime(agent, { + snapshotMessages: () => [], + enqueueAdvice: () => {}, + ...(hookKind === "success" + ? { onTurnSuccess: blockHook } + : { + onTurnError: async () => { + await blockHook(); + return false; + }, + }), + }); - runtime.onTurnEnd([{ role: "user", content: "old session", timestamp: 1 } as AgentMessage]); - await hookStarted.promise; - const pause = runtime.pauseForSessionTransition(); - const pausedQuickly = await Promise.race([pause.then(() => true), Bun.sleep(50).then(() => false)]); - runtime.reset(); - runtime.onTurnEnd([{ role: "user", content: "replacement session", timestamp: 2 } as AgentMessage]); - const replacementRan = await Promise.race([ - replacementPromptStarted.promise.then(() => true), - Bun.sleep(50).then(() => false), - ]); - releaseHook.resolve(); - await pause; - runtime.dispose(); + runtime.onTurnEnd([{ role: "user", content: "old session", timestamp: 1 } as AgentMessage]); + await hookStarted.promise; + const pause = runtime.pauseForSessionTransition(); + const pausedQuickly = await Promise.race([pause.then(() => true), Bun.sleep(50).then(() => false)]); + runtime.reset(); + runtime.onTurnEnd([{ role: "user", content: "replacement session", timestamp: 2 } as AgentMessage]); + const replacementRan = await Promise.race([ + replacementPromptStarted.promise.then(() => true), + Bun.sleep(50).then(() => false), + ]); + releaseHook.resolve(); + await pause; + runtime.dispose(); - expect(pausedQuickly).toBe(true); - expect(replacementRan).toBe(true); - }); + expect(pausedQuickly).toBe(true); + expect(replacementRan).toBe(true); + }, + ); it("aborts retry backoff before pausing for a session transition", async () => { const recoveryStarted = Promise.withResolvers(); const agent: AdvisorAgent = { diff --git a/packages/coding-agent/test/agent-session-acp-permission.test.ts b/packages/coding-agent/test/agent-session-acp-permission.test.ts index 6e44e630a..b02f7141c 100644 --- a/packages/coding-agent/test/agent-session-acp-permission.test.ts +++ b/packages/coding-agent/test/agent-session-acp-permission.test.ts @@ -849,56 +849,57 @@ it("allow_always: caches decision and calls bridge only once for subsequent exec expect(bashTool.executeCalls).toBe(2); }); -it.each( - boundaryCases, -)("%s permission decisions prompt again after a successful %s session boundary", async (decision, transition) => { - const bashTool = makeFakeTool("bash"); - const bridge = makeBridge({ outcome: "selected", optionId: decision, kind: decision }); - const permissionSpy = spyOn(bridge, "requestPermission"); - session = await createSession([bashTool], bridge, {}, { persist: true }); +it.each(boundaryCases)( + "%s permission decisions prompt again after a successful %s session boundary", + async (decision, transition) => { + const bashTool = makeFakeTool("bash"); + const bridge = makeBridge({ outcome: "selected", optionId: decision, kind: decision }); + const permissionSpy = spyOn(bridge, "requestPermission"); + session = await createSession([bashTool], bridge, {}, { persist: true }); - await session.setActiveToolsByName(["bash"]); - const wrappedBash = session.agent.state.tools.find(tool => tool.name === "bash"); - if (!wrappedBash) throw new Error("Expected wrapped bash tool"); + await session.setActiveToolsByName(["bash"]); + const wrappedBash = session.agent.state.tools.find(tool => tool.name === "bash"); + if (!wrappedBash) throw new Error("Expected wrapped bash tool"); - for (let callIndex = 0; callIndex < 2; callIndex++) { - if (callIndex === 1) { - if (transition === "new") { - expect(await session.newSession()).toBe(true); + for (let callIndex = 0; callIndex < 2; callIndex++) { + if (callIndex === 1) { + if (transition === "new") { + expect(await session.newSession()).toBe(true); + } else { + const targetId = `permission-target-${Bun.nanoseconds()}`; + const targetPath = `${tempDir.path()}/${targetId}.jsonl`; + await Bun.write( + targetPath, + `${JSON.stringify({ + type: "session", + version: 3, + id: targetId, + timestamp: new Date().toISOString(), + cwd: tempDir.path(), + })}\n`, + ); + expect(await session.switchSession(targetPath)).toBe(true); + } + } + + const execution = wrappedBash.execute( + `call-${callIndex}`, + { command: "echo boundary" }, + undefined, + undefined as never, + undefined as never, + ); + if (decision === "reject_always") { + await expect(execution).rejects.toThrow(/rejected by user/); } else { - const targetId = `permission-target-${Bun.nanoseconds()}`; - const targetPath = `${tempDir.path()}/${targetId}.jsonl`; - await Bun.write( - targetPath, - `${JSON.stringify({ - type: "session", - version: 3, - id: targetId, - timestamp: new Date().toISOString(), - cwd: tempDir.path(), - })}\n`, - ); - expect(await session.switchSession(targetPath)).toBe(true); + await execution; } } - const execution = wrappedBash.execute( - `call-${callIndex}`, - { command: "echo boundary" }, - undefined, - undefined as never, - undefined as never, - ); - if (decision === "reject_always") { - await expect(execution).rejects.toThrow(/rejected by user/); - } else { - await execution; - } - } - - expect(permissionSpy).toHaveBeenCalledTimes(2); - expect(bashTool.executeCalls).toBe(decision === "allow_always" ? 2 : 0); -}); + expect(permissionSpy).toHaveBeenCalledTimes(2); + expect(bashTool.executeCalls).toBe(decision === "allow_always" ? 2 : 0); + }, +); // --------------------------------------------------------------------------- // 4. Read tool not gated: bridge never called even when bridge is set diff --git a/packages/coding-agent/test/agent-session-bash-session-ownership.test.ts b/packages/coding-agent/test/agent-session-bash-session-ownership.test.ts index fd056dd73..46e02e995 100644 --- a/packages/coding-agent/test/agent-session-bash-session-ownership.test.ts +++ b/packages/coding-agent/test/agent-session-bash-session-ownership.test.ts @@ -203,68 +203,67 @@ describe("AgentSession bash session ownership", () => { ).toBe(true); }); - it.each([ - "new", - "switch", - "branch", - ] as const)("records a late bash result in its original session after %s", async transition => { - const sessionDir = path.join(tempDir.path(), "sessions"); - const { completion, emitUserBash, extensionRunner } = createGatedBashRunner(); - createSession(SessionManager.create(tempDir.path(), sessionDir), extensionRunner); - const oldSessionFile = await seedPersistedSession(); - const oldSessionId = session.sessionId; + it.each(["new", "switch", "branch"] as const)( + "records a late bash result in its original session after %s", + async transition => { + const sessionDir = path.join(tempDir.path(), "sessions"); + const { completion, emitUserBash, extensionRunner } = createGatedBashRunner(); + createSession(SessionManager.create(tempDir.path(), sessionDir), extensionRunner); + const oldSessionFile = await seedPersistedSession(); + const oldSessionId = session.sessionId; - const bashPromise = session.executeBash("old-session-command"); - expect(emitUserBash).toHaveBeenCalledTimes(1); + const bashPromise = session.executeBash("old-session-command"); + expect(emitUserBash).toHaveBeenCalledTimes(1); - switch (transition) { - case "new": - await session.newSession(); - break; - case "switch": { - const targetManager = SessionManager.create(tempDir.path(), sessionDir); - targetManager.appendMessage({ role: "user", content: "target", timestamp: Date.now() }); - targetManager.appendMessage(createAssistantMessage("target reply")); - await targetManager.ensureOnDisk(); - const targetFile = targetManager.getSessionFile(); - if (!targetFile) throw new Error("Expected target session file"); - await targetManager.close(); - await session.switchSession(targetFile); - break; + switch (transition) { + case "new": + await session.newSession(); + break; + case "switch": { + const targetManager = SessionManager.create(tempDir.path(), sessionDir); + targetManager.appendMessage({ role: "user", content: "target", timestamp: Date.now() }); + targetManager.appendMessage(createAssistantMessage("target reply")); + await targetManager.ensureOnDisk(); + const targetFile = targetManager.getSessionFile(); + if (!targetFile) throw new Error("Expected target session file"); + await targetManager.close(); + await session.switchSession(targetFile); + break; + } + case "branch": { + const userEntry = session.sessionManager + .getEntries() + .find(entry => entry.type === "message" && entry.message.role === "user"); + if (!userEntry) throw new Error("Expected user entry for branch"); + await session.branch(userEntry.id); + break; + } } - case "branch": { - const userEntry = session.sessionManager - .getEntries() - .find(entry => entry.type === "message" && entry.message.role === "user"); - if (!userEntry) throw new Error("Expected user entry for branch"); - await session.branch(userEntry.id); - break; - } - } - expect(session.sessionId).not.toBe(oldSessionId); - completion.resolve({ result: bashResult }); - await bashPromise; + expect(session.sessionId).not.toBe(oldSessionId); + completion.resolve({ result: bashResult }); + await bashPromise; - expect( - session.messages.some( - message => message.role === "bashExecution" && message.command === "old-session-command", - ), - ).toBe(false); + expect( + session.messages.some( + message => message.role === "bashExecution" && message.command === "old-session-command", + ), + ).toBe(false); - const oldSession = await SessionManager.open(oldSessionFile, sessionDir, undefined, { - initialCwd: tempDir.path(), - suppressBreadcrumb: true, - }); - additionalManagers.push(oldSession); - const oldMessages = oldSession.getBranch().flatMap(entry => (entry.type === "message" ? [entry.message] : [])); - expect(oldMessages.slice(-3).map(message => message.role)).toEqual(["user", "assistant", "bashExecution"]); - expect(oldMessages.at(-1)).toMatchObject({ - role: "bashExecution", - command: "old-session-command", - output: "old-output", - }); - }); + const oldSession = await SessionManager.open(oldSessionFile, sessionDir, undefined, { + initialCwd: tempDir.path(), + suppressBreadcrumb: true, + }); + additionalManagers.push(oldSession); + const oldMessages = oldSession.getBranch().flatMap(entry => (entry.type === "message" ? [entry.message] : [])); + expect(oldMessages.slice(-3).map(message => message.role)).toEqual(["user", "assistant", "bashExecution"]); + expect(oldMessages.at(-1)).toMatchObject({ + role: "bashExecution", + command: "old-session-command", + output: "old-output", + }); + }, + ); it("stores minimized bash output with the originating session", async () => { const sessionDir = path.join(tempDir.path(), "sessions"); diff --git a/packages/coding-agent/test/interactive-mode-plan-review.test.ts b/packages/coding-agent/test/interactive-mode-plan-review.test.ts index 49ae9e43a..a2fb1fa55 100644 --- a/packages/coding-agent/test/interactive-mode-plan-review.test.ts +++ b/packages/coding-agent/test/interactive-mode-plan-review.test.ts @@ -1878,14 +1878,13 @@ describe("InteractiveMode plan review rendering", () => { // `#approvePlan`'s `finally`. No aborted message_end is required to consume it, // so a stranded flag could otherwise silence the next unrelated abort. One // parametrized case per outcome keeps ok/cancelled/failed each covered. - it.each([ - "ok", - "cancelled", - "failed", - ] as const)("B1-B3: Approve and compact context + %s outcome → flag cleared by finally", async outcome => { - await approveWithCompact(outcome); - expect(session.isPlanInternalAbortPending).toBe(false); - }); + it.each(["ok", "cancelled", "failed"] as const)( + "B1-B3: Approve and compact context + %s outcome → flag cleared by finally", + async outcome => { + await approveWithCompact(outcome); + expect(session.isPlanInternalAbortPending).toBe(false); + }, + ); it("B4: Approve and compact context + handleCompactCommand throws → showError surfaces the failure AND flag cleared by finally before the outer catch", async () => { // `handlePlanApproval` wraps `#approvePlan` in a try/catch diff --git a/packages/coding-agent/test/internal-urls/memory-protocol.test.ts b/packages/coding-agent/test/internal-urls/memory-protocol.test.ts index f8b1b36cf..6bd8f71c8 100644 --- a/packages/coding-agent/test/internal-urls/memory-protocol.test.ts +++ b/packages/coding-agent/test/internal-urls/memory-protocol.test.ts @@ -315,27 +315,27 @@ describe("MemoryProtocolHandler", () => { }); }); - it.each([ - "memory://root/skills/**/../*.md", - "memory://root/skills/**/%2e%2e/*.md", - ])("rejects traversal in a memory glob suffix: %s", async pattern => { - await withMemoryFixture(async ({ cwd }) => { - await expect(createGlobTool(cwd).execute("memory-glob-traversal", { path: pattern })).rejects.toThrow( - /traversal/i, - ); - }); - }); + it.each(["memory://root/skills/**/../*.md", "memory://root/skills/**/%2e%2e/*.md"])( + "rejects traversal in a memory glob suffix: %s", + async pattern => { + await withMemoryFixture(async ({ cwd }) => { + await expect(createGlobTool(cwd).execute("memory-glob-traversal", { path: pattern })).rejects.toThrow( + /traversal/i, + ); + }); + }, + ); - it.each([ - "memory://root/skills/**/demo%2fnested/*.md", - "memory://root/skills/**/demo%5cnested/*.md", - ])("rejects encoded separators in a memory glob suffix: %s", async pattern => { - await withMemoryFixture(async ({ cwd }) => { - await expect(createGlobTool(cwd).execute("memory-glob-separator", { path: pattern })).rejects.toThrow( - /encoded path separator/i, - ); - }); - }); + it.each(["memory://root/skills/**/demo%2fnested/*.md", "memory://root/skills/**/demo%5cnested/*.md"])( + "rejects encoded separators in a memory glob suffix: %s", + async pattern => { + await withMemoryFixture(async ({ cwd }) => { + await expect(createGlobTool(cwd).execute("memory-glob-separator", { path: pattern })).rejects.toThrow( + /encoded path separator/i, + ); + }); + }, + ); it("throws clear error for missing files", async () => { await withMemoryFixture(async () => { diff --git a/packages/coding-agent/test/task/task-preflight.test.ts b/packages/coding-agent/test/task/task-preflight.test.ts index d1c017234..97c70880b 100644 --- a/packages/coding-agent/test/task/task-preflight.test.ts +++ b/packages/coding-agent/test/task/task-preflight.test.ts @@ -97,22 +97,19 @@ describe("task async preflight", () => { spawns: "scout", expectation: "Cannot spawn 'task'", }, - ])("returns $name policy errors before registering an async job", async ({ - name, - params, - settings, - spawns, - expectation, - }) => { - mockDiscovery(); - const jobs = manager(); - const tool = await TaskTool.create(createSession({ manager: jobs, settings, spawns })); + ])( + "returns $name policy errors before registering an async job", + async ({ name, params, settings, spawns, expectation }) => { + mockDiscovery(); + const jobs = manager(); + const tool = await TaskTool.create(createSession({ manager: jobs, settings, spawns })); - const result = await tool.execute("preflight", params as TaskParams); + const result = await tool.execute("preflight", params as TaskParams); - expect(textOf(result)).toContain(expectation); - expect(jobs.getJob(name)).toBeUndefined(); - }); + expect(textOf(result)).toContain(expectation); + expect(jobs.getJob(name)).toBeUndefined(); + }, + ); it("rejects an invalid async batch atomically before dispatching any item", async () => { mockDiscovery(); diff --git a/packages/coding-agent/test/title-generator.test.ts b/packages/coding-agent/test/title-generator.test.ts index 58ff02529..a8a8317a1 100644 --- a/packages/coding-agent/test/title-generator.test.ts +++ b/packages/coding-agent/test/title-generator.test.ts @@ -427,25 +427,24 @@ describe("title generator", () => { expect(title).toBe("Fix login button on mobile"); }); - it.each([ - "Here's a thinking process:", - "Thinking process:", - "Reasoning process:", - ])("rejects a markerless prose thinking preamble: %s", async responseText => { - const model = getModelFor("deepseek", "deepseek-v4-pro"); - vi.spyOn(ai, "completeSimple").mockResolvedValue({ - stopReason: "stop", - content: [{ type: "text", text: responseText }], - } as never); + it.each(["Here's a thinking process:", "Thinking process:", "Reasoning process:"])( + "rejects a markerless prose thinking preamble: %s", + async responseText => { + const model = getModelFor("deepseek", "deepseek-v4-pro"); + vi.spyOn(ai, "completeSimple").mockResolvedValue({ + stopReason: "stop", + content: [{ type: "text", text: responseText }], + } as never); - const title = await generateSessionTitle( - "the login button is broken on mobile", - createRegistry(model), - createSettings(model), - ); + const title = await generateSessionTitle( + "the login button is broken on mobile", + createRegistry(model), + createSettings(model), + ); - expect(title).toBeNull(); - }); + expect(title).toBeNull(); + }, + ); it("preserves a markerless title that mentions a tag", async () => { const model = getModelFor("deepseek", "deepseek-v4-pro"); diff --git a/packages/coding-agent/test/tools/bash-interceptor.test.ts b/packages/coding-agent/test/tools/bash-interceptor.test.ts index 07ab41939..d9309ccb8 100644 --- a/packages/coding-agent/test/tools/bash-interceptor.test.ts +++ b/packages/coding-agent/test/tools/bash-interceptor.test.ts @@ -114,27 +114,21 @@ describe("default echo/printf redirect rule", () => { describe("default hub start rules", () => { const tools = ["hub"]; - it.each([ - "bun run dev", - "vite --host 0.0.0.0", - "lldb ./app", - "bun test --watch", - "nohup server", - "server &", - ])("routes %s to hub start", command => { - const result = checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES); - expect(result.block).toBe(true); - expect(result.suggestedTool).toBe("hub"); - }); + it.each(["bun run dev", "vite --host 0.0.0.0", "lldb ./app", "bun test --watch", "nohup server", "server &"])( + "routes %s to hub start", + command => { + const result = checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES); + expect(result.block).toBe(true); + expect(result.suggestedTool).toBe("hub"); + }, + ); - it.each([ - "git diff -w", - "docker compose up -d", - "bun test", - "printf 'server &'", - ])("does not misclassify finite command %s", command => { - expect(checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES).block).toBe(false); - }); + it.each(["git diff -w", "docker compose up -d", "bun test", "printf 'server &'"])( + "does not misclassify finite command %s", + command => { + expect(checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES).block).toBe(false); + }, + ); }); describe("BashTool argument validation", () => {