diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index 081e4c982..6eefafde5 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -32,7 +32,7 @@ import type { } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { removeSyncWithRetries } from "@oh-my-pi/pi-utils"; -import { withEnv } from "./helpers"; +import { withEnv, withOfficialAnthropicEndpoint } from "./helpers"; const ANTHROPIC_MODEL_SPEC: ModelSpec<"anthropic-messages"> = { id: "claude-sonnet-4-5", @@ -152,6 +152,8 @@ function expectClaudeMetadataUserId(userId: string | undefined, expectedSessionI } } +withOfficialAnthropicEndpoint(); + describe("Anthropic request fingerprint alignment", () => { it("maps Stainless arch values from explicit inputs", () => { expect(mapStainlessArch("x64")).toBe("x64"); diff --git a/packages/ai/test/anthropic-cache-refresh.test.ts b/packages/ai/test/anthropic-cache-refresh.test.ts index 358e439b3..7c778021b 100644 --- a/packages/ai/test/anthropic-cache-refresh.test.ts +++ b/packages/ai/test/anthropic-cache-refresh.test.ts @@ -3,6 +3,7 @@ import { streamSimple } from "@oh-my-pi/pi-ai"; import type { CacheControlEphemeral, MessageCreateParams } from "@oh-my-pi/pi-ai/providers/anthropic-wire"; import type { CacheRetention, Context, FetchImpl, Model, ProviderSessionState } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; +import { withOfficialAnthropicEndpoint } from "./helpers"; const CACHE_REFRESH_DELAY_MS = 5 * 60_000 - 15_000; const CACHE_TOKENS = 1_200; @@ -211,6 +212,8 @@ afterEach(() => { vi.restoreAllMocks(); }); +withOfficialAnthropicEndpoint(); + describe("Anthropic prompt-cache refresh", () => { it("replays max_tokens=0 once per interval and stops after three refreshes", async () => { vi.useFakeTimers(); diff --git a/packages/ai/test/anthropic-fast-mode.test.ts b/packages/ai/test/anthropic-fast-mode.test.ts index d683e9aee..f7ef692f9 100644 --- a/packages/ai/test/anthropic-fast-mode.test.ts +++ b/packages/ai/test/anthropic-fast-mode.test.ts @@ -7,6 +7,7 @@ import { } from "@oh-my-pi/pi-ai/providers/anthropic"; import type { Context, Model, ProviderSessionState, ServiceTier } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; +import { withOfficialAnthropicEndpoint } from "./helpers"; function makeAnthropicModel(id: string): Model<"anthropic-messages"> { return buildModel({ @@ -52,6 +53,8 @@ function capturePayload(model: Model<"anthropic-messages">, opts: CaptureOptions return promise; } +withOfficialAnthropicEndpoint(); + describe("Anthropic priority service tier → speed='fast'", () => { it("sets speed='fast' for Claude Opus 4.7 when serviceTier='priority'", async () => { const payload = (await capturePayload(makeAnthropicModel("claude-opus-4-7"), { diff --git a/packages/ai/test/anthropic-stream-envelope.test.ts b/packages/ai/test/anthropic-stream-envelope.test.ts index 7184a8b3a..b62a56c3b 100644 --- a/packages/ai/test/anthropic-stream-envelope.test.ts +++ b/packages/ai/test/anthropic-stream-envelope.test.ts @@ -13,7 +13,7 @@ import type { import type { AssistantMessageEvent, Context, Model, ModelSpec, ProviderSessionState } from "@oh-my-pi/pi-ai/types"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; import { structuredCloneJSON } from "@oh-my-pi/pi-utils"; -import { withEnv } from "./helpers"; +import { withEnv, withOfficialAnthropicEndpoint } from "./helpers"; const model: Model<"anthropic-messages"> = buildModel({ id: "claude-sonnet-4-5", @@ -351,6 +351,8 @@ afterEach(() => { vi.restoreAllMocks(); }); +withOfficialAnthropicEndpoint(); + describe("anthropic stream envelope handling", () => { it("ignores duplicate message_start envelopes without resetting streamed text", async () => { vi.spyOn(AnthropicMessages.prototype, "create").mockImplementation( diff --git a/packages/ai/test/helpers/index.ts b/packages/ai/test/helpers/index.ts index f8437e4f8..53ea1ce1e 100644 --- a/packages/ai/test/helpers/index.ts +++ b/packages/ai/test/helpers/index.ts @@ -1,3 +1,4 @@ +import { afterEach, beforeEach } from "bun:test"; import * as os from "node:os"; import * as path from "node:path"; import type { Model } from "@oh-my-pi/pi-ai/types"; @@ -114,3 +115,29 @@ async function readAuthGatewayE2EStatus(): Promise { } return { ok: true, token }; } + +/** + * Neutralize `ANTHROPIC_BASE_URL` for the calling test file. + * + * The variable reroutes the effective Anthropic endpoint, and every + * "official endpoint" behavior — eager tool-input streaming, long cache + * retention, the Cowork TLS profile, the Claude Code session header — switches + * off once it points elsewhere. A contributor running a gateway, or running the + * suite from inside another agent, otherwise sees these tests fail on a clean + * checkout. Tests that exercise gateway routing set the variable explicitly + * with `withEnv` inside the test body, which still wins over this reset. + */ +export function withOfficialAnthropicEndpoint(): void { + let previous: string | undefined; + beforeEach(() => { + previous = Bun.env.ANTHROPIC_BASE_URL; + delete Bun.env.ANTHROPIC_BASE_URL; + }); + afterEach(() => { + if (previous === undefined) { + delete Bun.env.ANTHROPIC_BASE_URL; + } else { + Bun.env.ANTHROPIC_BASE_URL = previous; + } + }); +} diff --git a/packages/ai/test/leaked-thinking-stream.test.ts b/packages/ai/test/leaked-thinking-stream.test.ts index 300381b9f..9072e1525 100644 --- a/packages/ai/test/leaked-thinking-stream.test.ts +++ b/packages/ai/test/leaked-thinking-stream.test.ts @@ -15,6 +15,7 @@ import { getStreamingPartialJson, setStreamingPartialJson } from "@oh-my-pi/pi-a import { AssistantMessageEventStream } from "@oh-my-pi/pi-ai/utils/event-stream"; import { wrapLeakedThinkingStream } from "@oh-my-pi/pi-ai/utils/leaked-thinking-stream"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; +import { withOfficialAnthropicEndpoint } from "./helpers"; /** Minimal assistant message; `content`/`stopReason` overridden per event. */ function msg(overrides: Partial = {}): AssistantMessage { @@ -92,6 +93,8 @@ async function nextToolSnapshot(iterator: AsyncIterator): } } +withOfficialAnthropicEndpoint(); + describe("wrapLeakedThinkingStream", () => { async function runLeakedText(chunks: readonly string[]): Promise<{ events: AssistantMessageEvent[]; diff --git a/packages/coding-agent/test/fast-mode-scope.test.ts b/packages/coding-agent/test/fast-mode-scope.test.ts index 310058a2b..ce60ac0e3 100644 --- a/packages/coding-agent/test/fast-mode-scope.test.ts +++ b/packages/coding-agent/test/fast-mode-scope.test.ts @@ -10,6 +10,9 @@ 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 { TempDir } from "@oh-my-pi/pi-utils"; +import { withOfficialAnthropicEndpoint } from "./helpers/anthropic-endpoint"; + +withOfficialAnthropicEndpoint(); describe("/fast targets the current model's service-tier family", () => { let tempDir: TempDir; diff --git a/packages/coding-agent/test/helpers/anthropic-endpoint.ts b/packages/coding-agent/test/helpers/anthropic-endpoint.ts new file mode 100644 index 000000000..67ae7f629 --- /dev/null +++ b/packages/coding-agent/test/helpers/anthropic-endpoint.ts @@ -0,0 +1,27 @@ +import { afterEach, beforeEach } from "bun:test"; + +/** + * Neutralize `ANTHROPIC_BASE_URL` for the calling test file. + * + * The variable reroutes the effective Anthropic endpoint, and every + * "official endpoint" behavior — eager tool-input streaming, long cache + * retention, the Cowork TLS profile, the Claude Code session header — switches + * off once it points elsewhere. A contributor running a gateway, or running the + * suite from inside another agent, otherwise sees these tests fail on a clean + * checkout. Tests that exercise gateway routing set the variable explicitly + * with `withEnv` inside the test body, which still wins over this reset. + */ +export function withOfficialAnthropicEndpoint(): void { + let previous: string | undefined; + beforeEach(() => { + previous = Bun.env.ANTHROPIC_BASE_URL; + delete Bun.env.ANTHROPIC_BASE_URL; + }); + afterEach(() => { + if (previous === undefined) { + delete Bun.env.ANTHROPIC_BASE_URL; + } else { + Bun.env.ANTHROPIC_BASE_URL = previous; + } + }); +}