소스 검색

test: 添加模型路由单元测试 (测试计划 05)

覆盖 isModelAlias、isModelFamilyAlias、getAPIProvider、
isFirstPartyAnthropicBaseUrl、firstPartyNameToCanonical,共 40 个测试用例。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
claude-code-best 3 주 전
부모
커밋
25839ab454
3개의 변경된 파일246개의 추가작업 그리고 0개의 파일을 삭제
  1. 70 0
      src/utils/model/__tests__/aliases.test.ts
  2. 92 0
      src/utils/model/__tests__/model.test.ts
  3. 84 0
      src/utils/model/__tests__/providers.test.ts

+ 70 - 0
src/utils/model/__tests__/aliases.test.ts

@@ -0,0 +1,70 @@
+import { describe, expect, test } from "bun:test";
+import { isModelAlias, isModelFamilyAlias } from "../aliases";
+
+describe("isModelAlias", () => {
+  test('returns true for "sonnet"', () => {
+    expect(isModelAlias("sonnet")).toBe(true);
+  });
+
+  test('returns true for "opus"', () => {
+    expect(isModelAlias("opus")).toBe(true);
+  });
+
+  test('returns true for "haiku"', () => {
+    expect(isModelAlias("haiku")).toBe(true);
+  });
+
+  test('returns true for "best"', () => {
+    expect(isModelAlias("best")).toBe(true);
+  });
+
+  test('returns true for "sonnet[1m]"', () => {
+    expect(isModelAlias("sonnet[1m]")).toBe(true);
+  });
+
+  test('returns true for "opus[1m]"', () => {
+    expect(isModelAlias("opus[1m]")).toBe(true);
+  });
+
+  test('returns true for "opusplan"', () => {
+    expect(isModelAlias("opusplan")).toBe(true);
+  });
+
+  test("returns false for full model ID", () => {
+    expect(isModelAlias("claude-sonnet-4-6-20250514")).toBe(false);
+  });
+
+  test("returns false for unknown string", () => {
+    expect(isModelAlias("gpt-4")).toBe(false);
+  });
+
+  test("is case-sensitive", () => {
+    expect(isModelAlias("Sonnet")).toBe(false);
+  });
+});
+
+describe("isModelFamilyAlias", () => {
+  test('returns true for "sonnet"', () => {
+    expect(isModelFamilyAlias("sonnet")).toBe(true);
+  });
+
+  test('returns true for "opus"', () => {
+    expect(isModelFamilyAlias("opus")).toBe(true);
+  });
+
+  test('returns true for "haiku"', () => {
+    expect(isModelFamilyAlias("haiku")).toBe(true);
+  });
+
+  test('returns false for "best"', () => {
+    expect(isModelFamilyAlias("best")).toBe(false);
+  });
+
+  test('returns false for "opusplan"', () => {
+    expect(isModelFamilyAlias("opusplan")).toBe(false);
+  });
+
+  test('returns false for "sonnet[1m]"', () => {
+    expect(isModelFamilyAlias("sonnet[1m]")).toBe(false);
+  });
+});

+ 92 - 0
src/utils/model/__tests__/model.test.ts

@@ -0,0 +1,92 @@
+import { describe, expect, test } from "bun:test";
+import { firstPartyNameToCanonical } from "../model";
+
+describe("firstPartyNameToCanonical", () => {
+  test("maps opus-4-6 full name to canonical", () => {
+    expect(firstPartyNameToCanonical("claude-opus-4-6-20250514")).toBe(
+      "claude-opus-4-6"
+    );
+  });
+
+  test("maps sonnet-4-6 full name", () => {
+    expect(firstPartyNameToCanonical("claude-sonnet-4-6-20250514")).toBe(
+      "claude-sonnet-4-6"
+    );
+  });
+
+  test("maps haiku-4-5", () => {
+    expect(firstPartyNameToCanonical("claude-haiku-4-5-20251001")).toBe(
+      "claude-haiku-4-5"
+    );
+  });
+
+  test("maps 3P provider format", () => {
+    expect(
+      firstPartyNameToCanonical("us.anthropic.claude-opus-4-6-v1:0")
+    ).toBe("claude-opus-4-6");
+  });
+
+  test("maps claude-3-7-sonnet", () => {
+    expect(firstPartyNameToCanonical("claude-3-7-sonnet-20250219")).toBe(
+      "claude-3-7-sonnet"
+    );
+  });
+
+  test("maps claude-3-5-sonnet", () => {
+    expect(firstPartyNameToCanonical("claude-3-5-sonnet-20241022")).toBe(
+      "claude-3-5-sonnet"
+    );
+  });
+
+  test("maps claude-3-5-haiku", () => {
+    expect(firstPartyNameToCanonical("claude-3-5-haiku-20241022")).toBe(
+      "claude-3-5-haiku"
+    );
+  });
+
+  test("maps claude-3-opus", () => {
+    expect(firstPartyNameToCanonical("claude-3-opus-20240229")).toBe(
+      "claude-3-opus"
+    );
+  });
+
+  test("is case insensitive", () => {
+    expect(firstPartyNameToCanonical("Claude-Opus-4-6-20250514")).toBe(
+      "claude-opus-4-6"
+    );
+  });
+
+  test("falls back to input for unknown model", () => {
+    expect(firstPartyNameToCanonical("unknown-model")).toBe("unknown-model");
+  });
+
+  test("differentiates opus-4 vs opus-4-5 vs opus-4-6", () => {
+    expect(firstPartyNameToCanonical("claude-opus-4-20240101")).toBe(
+      "claude-opus-4"
+    );
+    expect(firstPartyNameToCanonical("claude-opus-4-5-20240101")).toBe(
+      "claude-opus-4-5"
+    );
+    expect(firstPartyNameToCanonical("claude-opus-4-6-20240101")).toBe(
+      "claude-opus-4-6"
+    );
+  });
+
+  test("maps opus-4-1", () => {
+    expect(firstPartyNameToCanonical("claude-opus-4-1-20240101")).toBe(
+      "claude-opus-4-1"
+    );
+  });
+
+  test("maps sonnet-4-5", () => {
+    expect(firstPartyNameToCanonical("claude-sonnet-4-5-20240101")).toBe(
+      "claude-sonnet-4-5"
+    );
+  });
+
+  test("maps sonnet-4", () => {
+    expect(firstPartyNameToCanonical("claude-sonnet-4-20240101")).toBe(
+      "claude-sonnet-4"
+    );
+  });
+});

+ 84 - 0
src/utils/model/__tests__/providers.test.ts

@@ -0,0 +1,84 @@
+import { describe, expect, test, beforeEach, afterEach } from "bun:test";
+import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from "../providers";
+
+describe("getAPIProvider", () => {
+  const originalEnv = { ...process.env };
+
+  afterEach(() => {
+    delete process.env.CLAUDE_CODE_USE_BEDROCK;
+    delete process.env.CLAUDE_CODE_USE_VERTEX;
+    delete process.env.CLAUDE_CODE_USE_FOUNDRY;
+  });
+
+  test('returns "firstParty" by default', () => {
+    delete process.env.CLAUDE_CODE_USE_BEDROCK;
+    delete process.env.CLAUDE_CODE_USE_VERTEX;
+    delete process.env.CLAUDE_CODE_USE_FOUNDRY;
+    expect(getAPIProvider()).toBe("firstParty");
+  });
+
+  test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set', () => {
+    process.env.CLAUDE_CODE_USE_BEDROCK = "1";
+    expect(getAPIProvider()).toBe("bedrock");
+  });
+
+  test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set', () => {
+    process.env.CLAUDE_CODE_USE_VERTEX = "1";
+    expect(getAPIProvider()).toBe("vertex");
+  });
+
+  test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set', () => {
+    process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
+    expect(getAPIProvider()).toBe("foundry");
+  });
+
+  test("bedrock takes precedence over vertex", () => {
+    process.env.CLAUDE_CODE_USE_BEDROCK = "1";
+    process.env.CLAUDE_CODE_USE_VERTEX = "1";
+    expect(getAPIProvider()).toBe("bedrock");
+  });
+});
+
+describe("isFirstPartyAnthropicBaseUrl", () => {
+  const originalBaseUrl = process.env.ANTHROPIC_BASE_URL;
+  const originalUserType = process.env.USER_TYPE;
+
+  afterEach(() => {
+    if (originalBaseUrl !== undefined) {
+      process.env.ANTHROPIC_BASE_URL = originalBaseUrl;
+    } else {
+      delete process.env.ANTHROPIC_BASE_URL;
+    }
+    if (originalUserType !== undefined) {
+      process.env.USER_TYPE = originalUserType;
+    } else {
+      delete process.env.USER_TYPE;
+    }
+  });
+
+  test("returns true when ANTHROPIC_BASE_URL is not set", () => {
+    delete process.env.ANTHROPIC_BASE_URL;
+    expect(isFirstPartyAnthropicBaseUrl()).toBe(true);
+  });
+
+  test("returns true for api.anthropic.com", () => {
+    process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com";
+    expect(isFirstPartyAnthropicBaseUrl()).toBe(true);
+  });
+
+  test("returns false for custom URL", () => {
+    process.env.ANTHROPIC_BASE_URL = "https://my-proxy.com";
+    expect(isFirstPartyAnthropicBaseUrl()).toBe(false);
+  });
+
+  test("returns false for invalid URL", () => {
+    process.env.ANTHROPIC_BASE_URL = "not-a-url";
+    expect(isFirstPartyAnthropicBaseUrl()).toBe(false);
+  });
+
+  test("returns true for staging URL when USER_TYPE is ant", () => {
+    process.env.ANTHROPIC_BASE_URL = "https://api-staging.anthropic.com";
+    process.env.USER_TYPE = "ant";
+    expect(isFirstPartyAnthropicBaseUrl()).toBe(true);
+  });
+});