import { describe, expect, test } from "vitest"; import { BUILTIN_CLIENT_KEYWORDS, CLAUDE_CODE_KEYWORD_PREFIX, detectClientFull, isBuiltinKeyword, isClientAllowed, isClientAllowedDetailed, matchClientPattern, } from "@/app/v1/_lib/proxy/client-detector"; import type { ProxySession } from "@/app/v1/_lib/proxy/session"; const LEGACY_METADATA_USER_ID = "user_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_account__session_sess_legacy_123"; const JSON_METADATA_USER_ID = JSON.stringify({ device_id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", account_uuid: "", session_id: "sess_json_123", }); type SessionOptions = { userAgent?: string | null; xApp?: string | null; dangerousBrowserAccess?: string | null; anthropicBeta?: string | null; metadataUserId?: string | null; }; function createMockSession(options: SessionOptions = {}): ProxySession { const headers = new Headers(); if (options.xApp !== undefined && options.xApp !== null) { headers.set("x-app", options.xApp); } if (options.dangerousBrowserAccess !== undefined && options.dangerousBrowserAccess !== null) { headers.set("anthropic-dangerous-direct-browser-access", options.dangerousBrowserAccess); } if (options.anthropicBeta !== undefined && options.anthropicBeta !== null) { headers.set("anthropic-beta", options.anthropicBeta); } const message: Record = {}; if (options.metadataUserId !== undefined && options.metadataUserId !== null) { message.metadata = { user_id: options.metadataUserId }; } return { userAgent: options.userAgent ?? null, headers, request: { message, }, } as unknown as ProxySession; } function createConfirmedClaudeCodeSession(userAgent: string): ProxySession { return createMockSession({ userAgent, xApp: "cli", anthropicBeta: "claude-code-test", metadataUserId: LEGACY_METADATA_USER_ID, }); } describe("client-detector", () => { describe("constants", () => { test("CLAUDE_CODE_KEYWORD_PREFIX should be claude-code", () => { expect(CLAUDE_CODE_KEYWORD_PREFIX).toBe("claude-code"); }); test("BUILTIN_CLIENT_KEYWORDS should contain 7 items", () => { expect(BUILTIN_CLIENT_KEYWORDS.size).toBe(7); }); }); describe("isBuiltinKeyword", () => { test.each([ "claude-code", "claude-code-cli", "claude-code-cli-sdk", "claude-code-vscode", "claude-code-sdk-ts", "claude-code-sdk-py", "claude-code-gh-action", ])("should return true for builtin keyword: %s", (pattern) => { expect(isBuiltinKeyword(pattern)).toBe(true); }); test.each([ "gemini-cli", "codex-cli", "custom-pattern", ])("should return false for non-builtin keyword: %s", (pattern) => { expect(isBuiltinKeyword(pattern)).toBe(false); }); }); describe("confirmClaudeCodeSignals via detectClientFull", () => { test("should confirm when all 4 strong signals are present", () => { const session = createMockSession({ userAgent: "claude-cli/1.0.0 (external, cli)", xApp: "cli", anthropicBeta: "interleaved-thinking-2025-05-14", metadataUserId: LEGACY_METADATA_USER_ID, }); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(true); expect(result.signals).toEqual([ "x-app-cli", "ua-prefix", "betas-present", "metadata-user-id", ]); expect(result.supplementary).toEqual([]); }); test("should confirm when anthropic-beta has claude-code- prefix (backwards compat)", () => { const session = createMockSession({ userAgent: "claude-cli/1.0.0 (external, cli)", xApp: "cli", anthropicBeta: "claude-code-cache-control-20260101", metadataUserId: LEGACY_METADATA_USER_ID, }); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(true); expect(result.signals).toEqual([ "x-app-cli", "ua-prefix", "betas-present", "metadata-user-id", ]); }); test("should confirm when metadata.user_id uses JSON string format", () => { const session = createMockSession({ userAgent: "claude-cli/2.1.78 (external, cli)", xApp: "cli", anthropicBeta: "interleaved-thinking-2025-05-14", metadataUserId: JSON_METADATA_USER_ID, }); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(true); expect(result.signals).toEqual([ "x-app-cli", "ua-prefix", "betas-present", "metadata-user-id", ]); }); test.each([ { name: "missing x-app", options: { userAgent: "claude-cli/1.0.0 (external, cli)", anthropicBeta: "some-beta", metadataUserId: LEGACY_METADATA_USER_ID, }, }, { name: "missing ua-prefix", options: { userAgent: "GeminiCLI/1.0", xApp: "cli", anthropicBeta: "some-beta", metadataUserId: LEGACY_METADATA_USER_ID, }, }, { name: "missing betas-present", options: { userAgent: "claude-cli/1.0.0 (external, cli)", xApp: "cli", metadataUserId: LEGACY_METADATA_USER_ID, }, }, { name: "missing metadata-user-id", options: { userAgent: "claude-cli/1.0.0 (external, cli)", xApp: "cli", anthropicBeta: "some-beta", }, }, ])("should not confirm with only 3-of-4 signals: $name", ({ options }) => { const session = createMockSession(options); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(false); expect(result.signals.length).toBe(3); }); test("should not confirm with 0 strong signals", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(false); expect(result.signals).toEqual([]); }); test("should collect supplementary signal without counting it", () => { const session = createMockSession({ userAgent: "claude-cli/1.0.0 (external, cli)", xApp: "cli", dangerousBrowserAccess: "true", }); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(false); expect(result.signals).toEqual(["x-app-cli", "ua-prefix"]); expect(result.supplementary).toEqual(["dangerous-browser-access"]); }); }); describe("extractSubClient via detectClientFull", () => { test.each([ ["cli", "claude-code-cli"], ["sdk-cli", "claude-code-cli-sdk"], ["claude-vscode", "claude-code-vscode"], ["sdk-ts", "claude-code-sdk-ts"], ["sdk-py", "claude-code-sdk-py"], ["claude-code-github-action", "claude-code-gh-action"], ])("should map entrypoint %s to %s", (entrypoint, expectedSubClient) => { const session = createConfirmedClaudeCodeSession( `claude-cli/1.2.3 (external, ${entrypoint})` ); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(true); expect(result.subClient).toBe(expectedSubClient); }); test("should return null for unknown entrypoint", () => { const session = createConfirmedClaudeCodeSession( "claude-cli/1.2.3 (external, unknown-entry)" ); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(true); expect(result.subClient).toBeNull(); }); test("should return null for malformed UA", () => { const session = createConfirmedClaudeCodeSession("claude-cli 1.2.3 (external, cli)"); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(false); expect(result.subClient).toBeNull(); }); test("should return null when UA has no parentheses section", () => { const session = createMockSession({ userAgent: "claude-cli/1.2.3 external, cli", xApp: "cli", anthropicBeta: "claude-code-a", metadataUserId: LEGACY_METADATA_USER_ID, }); const result = detectClientFull(session, "claude-code"); expect(result.hubConfirmed).toBe(true); expect(result.subClient).toBeNull(); }); }); describe("matchClientPattern builtin keyword path", () => { test("should match wildcard claude-code when 3-of-3 is confirmed", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); expect(matchClientPattern(session, "claude-code")).toBe(true); }); test("should match claude-code-cli for cli entrypoint", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); expect(matchClientPattern(session, "claude-code-cli")).toBe(true); }); test("should match claude-code-vscode for claude-vscode entrypoint", () => { const session = createConfirmedClaudeCodeSession( "claude-cli/1.2.3 (external, claude-vscode, agent-sdk/0.1.0)" ); expect(matchClientPattern(session, "claude-code-vscode")).toBe(true); }); test("should return false when sub-client does not match", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-py)"); expect(matchClientPattern(session, "claude-code-sdk-ts")).toBe(false); }); test("should return false when only 3-of-4 signals are present (missing metadata-user-id)", () => { const session = createMockSession({ userAgent: "claude-cli/1.2.3 (external, cli)", xApp: "cli", anthropicBeta: "non-claude-code", }); expect(matchClientPattern(session, "claude-code")).toBe(false); }); }); describe("matchClientPattern custom substring path", () => { test("should match gemini-cli against GeminiCLI", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); expect(matchClientPattern(session, "gemini-cli")).toBe(true); }); test("should match codex-cli against codex_cli", () => { const session = createMockSession({ userAgent: "codex_cli/2.0" }); expect(matchClientPattern(session, "codex-cli")).toBe(true); }); test("should match codex-cli against codex desktop alias", () => { const session = createMockSession({ userAgent: "Codex Desktop/1.0" }); expect(matchClientPattern(session, "codex-cli")).toBe(true); }); test("should NOT match codex_vscode against codex desktop UA (different family)", () => { const session = createMockSession({ userAgent: "Codex Desktop/1.0" }); expect(matchClientPattern(session, "codex_vscode")).toBe(false); }); test("should match codex-tui UA against codex-cli", () => { const session = createMockSession({ userAgent: "codex-tui/0.115.0 (Mac OS 15.7.3; arm64) Apple_Terminal/455.1 (codex-tui; 0.115.0)", }); expect(matchClientPattern(session, "codex-cli")).toBe(true); }); test("should match codex_cli_rs UA against codex-cli", () => { const session = createMockSession({ userAgent: "codex_cli_rs/0.114.0 (Windows 10.0.26200; x86_64) xterm-256color", }); expect(matchClientPattern(session, "codex-cli")).toBe(true); }); test("should match codex_exec UA against codex-cli", () => { const session = createMockSession({ userAgent: "codex_exec/1.0.0" }); expect(matchClientPattern(session, "codex-cli")).toBe(true); }); test("should return false when User-Agent is empty", () => { const session = createMockSession({ userAgent: " " }); expect(matchClientPattern(session, "gemini-cli")).toBe(false); }); test("should return false when custom pattern is not found", () => { const session = createMockSession({ userAgent: "Mozilla/5.0 Compatible" }); expect(matchClientPattern(session, "gemini-cli")).toBe(false); }); test("should return false when pattern normalizes to empty", () => { const session = createMockSession({ userAgent: "AnyClient/1.0" }); expect(matchClientPattern(session, "-_-")).toBe(false); }); }); describe("matchClientPattern glob wildcard path", () => { test("should match codex-* against codex-cli/2.0", () => { const session = createMockSession({ userAgent: "codex-cli/2.0" }); expect(matchClientPattern(session, "codex-*")).toBe(true); }); test("should not match codex-* against GeminiCLI/1.0", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); expect(matchClientPattern(session, "codex-*")).toBe(false); }); test("should match *-cli* against codex-cli/2.0", () => { const session = createMockSession({ userAgent: "codex-cli/2.0" }); expect(matchClientPattern(session, "*-cli*")).toBe(true); }); test("should match bare * against any non-empty UA", () => { const session = createMockSession({ userAgent: "AnyClient/1.0" }); expect(matchClientPattern(session, "*")).toBe(true); }); test("should match My*App against MyCustomApp/1.0", () => { const session = createMockSession({ userAgent: "MyCustomApp/1.0" }); expect(matchClientPattern(session, "My*App*")).toBe(true); }); test("should be case-insensitive for glob", () => { const session = createMockSession({ userAgent: "codex-cli/1.0" }); expect(matchClientPattern(session, "CODEX-*")).toBe(true); }); test("should return false for glob when UA is empty", () => { const session = createMockSession({ userAgent: " " }); expect(matchClientPattern(session, "codex-*")).toBe(false); }); test("should NOT normalize hyphens/underscores in glob mode", () => { const session = createMockSession({ userAgent: "codex_cli/2.0" }); expect(matchClientPattern(session, "codex-*")).toBe(false); }); test("should match glob with underscores literally", () => { const session = createMockSession({ userAgent: "codex_cli/2.0" }); expect(matchClientPattern(session, "codex_*")).toBe(true); }); test("consecutive wildcards ** should behave like single *", () => { const session = createMockSession({ userAgent: "codex-cli/2.0" }); expect(matchClientPattern(session, "codex-**")).toBe(true); expect(matchClientPattern(session, "**codex**")).toBe(true); }); test("glob should handle regex metacharacters literally", () => { const session = createMockSession({ userAgent: "foo.bar/1.0" }); expect(matchClientPattern(session, "foo.bar*")).toBe(true); expect(matchClientPattern(session, "foo*bar*")).toBe(true); const session2 = createMockSession({ userAgent: "fooXbar/1.0" }); expect(matchClientPattern(session2, "foo.bar*")).toBe(false); }); test("glob should handle brackets and parens literally", () => { const session = createMockSession({ userAgent: "tool[v2]/1.0" }); expect(matchClientPattern(session, "tool[v2]*")).toBe(true); }); test("pathological glob pattern completes quickly without ReDoS", () => { const session = createMockSession({ userAgent: `${"a".repeat(32)}b` }); const pattern = "*a*a*a*a*a*a*a*a*c"; const start = performance.now(); const result = matchClientPattern(session, pattern); const elapsed = performance.now() - start; expect(result).toBe(false); expect(elapsed).toBeLessThan(50); }); }); describe("isClientAllowed", () => { test("should reject when blocked matches even if allowed also matches", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); expect(isClientAllowed(session, ["claude-code"], ["claude-code"])).toBe(false); }); test("should allow when allowedClients and blockedClients are both empty", () => { const session = createMockSession({ userAgent: "AnyClient/1.0" }); expect(isClientAllowed(session, [], [])).toBe(true); }); test("should allow when allowedClients match", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); expect(isClientAllowed(session, ["gemini-cli"])).toBe(true); }); test("should reject when allowedClients are set but none match", () => { const session = createMockSession({ userAgent: "UnknownClient/1.0" }); expect(isClientAllowed(session, ["gemini-cli"])).toBe(false); }); test("should reject when only blockedClients are set and blocked matches", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); expect(isClientAllowed(session, [], ["gemini-cli"])).toBe(false); }); test("should allow when only blockedClients are set and blocked does not match", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); expect(isClientAllowed(session, [], ["codex-cli"])).toBe(true); }); test("should allow when blocked does not match and allowed matches", () => { const session = createMockSession({ userAgent: "codex_cli/2.0" }); expect(isClientAllowed(session, ["codex-cli"], ["gemini-cli"])).toBe(true); }); test("should allow codex desktop alias when codex-cli is allowlisted", () => { const session = createMockSession({ userAgent: "Codex Desktop/1.0" }); expect(isClientAllowed(session, ["codex-cli"], [])).toBe(true); }); }); describe("isClientAllowedDetailed", () => { test("should return no_restriction when both lists are empty", () => { const session = createMockSession({ userAgent: "AnyClient/1.0" }); const result = isClientAllowedDetailed(session, [], []); expect(result).toEqual({ allowed: true, matchType: "no_restriction", matchedPattern: undefined, detectedClient: undefined, checkedAllowlist: [], checkedBlocklist: [], }); }); test("should return blocklist_hit with matched pattern", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); const result = isClientAllowedDetailed(session, [], ["gemini-cli"]); expect(result.allowed).toBe(false); expect(result.matchType).toBe("blocklist_hit"); expect(result.matchedPattern).toBe("gemini-cli"); expect(result.detectedClient).toBe("GeminiCLI/1.0"); expect(result.checkedBlocklist).toEqual(["gemini-cli"]); }); test("should return allowlist_miss when no allowlist pattern matches", () => { const session = createMockSession({ userAgent: "UnknownClient/1.0" }); const result = isClientAllowedDetailed(session, ["gemini-cli", "codex-cli"], []); expect(result.allowed).toBe(false); expect(result.matchType).toBe("allowlist_miss"); expect(result.matchedPattern).toBeUndefined(); expect(result.detectedClient).toBe("UnknownClient/1.0"); expect(result.checkedAllowlist).toEqual(["gemini-cli", "codex-cli"]); }); test("should return allowed when allowlist matches", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); const result = isClientAllowedDetailed(session, ["gemini-cli"], []); expect(result.allowed).toBe(true); expect(result.matchType).toBe("allowed"); expect(result.matchedPattern).toBe("gemini-cli"); expect(result.detectedClient).toBe("GeminiCLI/1.0"); }); test("blocklist takes precedence over allowlist", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); const result = isClientAllowedDetailed(session, ["claude-code"], ["claude-code"]); expect(result.allowed).toBe(false); expect(result.matchType).toBe("blocklist_hit"); expect(result.matchedPattern).toBe("claude-code"); }); test("should detect sub-client for builtin keywords", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)"); const result = isClientAllowedDetailed(session, ["claude-code"], []); expect(result.allowed).toBe(true); expect(result.matchType).toBe("allowed"); expect(result.detectedClient).toBe("claude-code-sdk-ts"); expect(result.matchedPattern).toBe("claude-code"); }); test("should return allowed when only blocklist set and no match", () => { const session = createMockSession({ userAgent: "CodexCLI/1.0" }); const result = isClientAllowedDetailed(session, [], ["gemini-cli"]); expect(result.allowed).toBe(true); expect(result.matchType).toBe("allowed"); expect(result.detectedClient).toBe("CodexCLI/1.0"); }); test("should return no_restriction when blockedClients is undefined and allowlist empty", () => { const session = createMockSession({ userAgent: "AnyClient/1.0" }); const result = isClientAllowedDetailed(session, []); expect(result.allowed).toBe(true); expect(result.matchType).toBe("no_restriction"); }); test("should capture first matching blocked pattern", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); const result = isClientAllowedDetailed( session, [], ["codex-cli", "gemini-cli", "factory-cli"] ); expect(result.allowed).toBe(false); expect(result.matchType).toBe("blocklist_hit"); expect(result.matchedPattern).toBe("gemini-cli"); }); test("should include signals and hubConfirmed when builtin keyword is in allowlist", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); const result = isClientAllowedDetailed(session, ["claude-code"], []); expect(result.signals).toEqual([ "x-app-cli", "ua-prefix", "betas-present", "metadata-user-id", ]); expect(result.hubConfirmed).toBe(true); }); test("should include signals and hubConfirmed when builtin keyword is in blocklist", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, cli)"); const result = isClientAllowedDetailed(session, [], ["claude-code"]); expect(result.signals).toEqual([ "x-app-cli", "ua-prefix", "betas-present", "metadata-user-id", ]); expect(result.hubConfirmed).toBe(true); }); test("should not include signals when no builtin keyword is in lists", () => { const session = createMockSession({ userAgent: "GeminiCLI/1.0" }); const result = isClientAllowedDetailed(session, ["gemini-cli"], []); expect(result.signals).toBeUndefined(); expect(result.hubConfirmed).toBeUndefined(); }); test("should allow when glob pattern in allowlist matches", () => { const session = createMockSession({ userAgent: "codex-cli/2.0" }); const result = isClientAllowedDetailed(session, ["codex-*"], []); expect(result.allowed).toBe(true); expect(result.matchType).toBe("allowed"); expect(result.matchedPattern).toBe("codex-*"); }); test("should reject when glob pattern in blocklist matches", () => { const session = createMockSession({ userAgent: "codex-cli/2.0" }); const result = isClientAllowedDetailed(session, [], ["codex-*"]); expect(result.allowed).toBe(false); expect(result.matchType).toBe("blocklist_hit"); expect(result.matchedPattern).toBe("codex-*"); }); test("should work with mix of glob and substring patterns", () => { const session = createMockSession({ userAgent: "my-custom-tool/3.0" }); const result = isClientAllowedDetailed(session, ["codex-*", "custom"], []); expect(result.allowed).toBe(true); expect(result.matchType).toBe("allowed"); expect(result.matchedPattern).toBe("custom"); }); }); describe("codex family UA matching via matchesCodexFamilyAlias", () => { test("codex-tui UA matches codex-cli allowlist", () => { const session = createMockSession({ userAgent: "codex-tui/0.115.0 (Mac OS 15.7.3; arm64) Apple_Terminal/455.1 (codex-tui; 0.115.0)", }); const result = isClientAllowedDetailed(session, ["codex-cli"], []); expect(result.allowed).toBe(true); expect(result.matchedPattern).toBe("codex-cli"); }); test("codex_cli_rs UA matches codex-cli allowlist", () => { const session = createMockSession({ userAgent: "codex_cli_rs/0.114.0 (Windows 10.0.26200; x86_64) xterm-256color", }); const result = isClientAllowedDetailed(session, ["codex-cli"], []); expect(result.allowed).toBe(true); expect(result.matchedPattern).toBe("codex-cli"); }); test("codex_exec UA matches codex-cli allowlist", () => { const session = createMockSession({ userAgent: "codex_exec/1.0.0" }); const result = isClientAllowedDetailed(session, ["codex-cli"], []); expect(result.allowed).toBe(true); expect(result.matchedPattern).toBe("codex-cli"); }); test("codex_vscode UA matches codex-cli allowlist", () => { const session = createMockSession({ userAgent: "codex_vscode/1.0.0" }); const result = isClientAllowedDetailed(session, ["codex-cli"], []); expect(result.allowed).toBe(true); expect(result.matchedPattern).toBe("codex-cli"); }); test("codex-tui UA matches codex_cli_core child pattern", () => { const session = createMockSession({ userAgent: "codex-tui/0.115.0 (Mac OS 15.7.3; arm64)", }); const result = isClientAllowedDetailed(session, ["codex_cli_core"], []); expect(result.allowed).toBe(true); expect(result.matchedPattern).toBe("codex_cli_core"); }); test("codex_cli_rs UA matches codex_cli_core child pattern", () => { const session = createMockSession({ userAgent: "codex_cli_rs/0.114.0 (Windows 10.0.26200; x86_64)", }); const result = isClientAllowedDetailed(session, ["codex_cli_core"], []); expect(result.allowed).toBe(true); expect(result.matchedPattern).toBe("codex_cli_core"); }); test("codex_exec UA matches codex_exec child pattern", () => { const session = createMockSession({ userAgent: "codex_exec/1.0.0" }); const result = isClientAllowedDetailed(session, ["codex_exec"], []); expect(result.allowed).toBe(true); expect(result.matchedPattern).toBe("codex_exec"); }); test("codex_vscode UA matches codex_vscode child pattern", () => { const session = createMockSession({ userAgent: "codex_vscode/1.0.0" }); const result = isClientAllowedDetailed(session, ["codex_vscode"], []); expect(result.allowed).toBe(true); expect(result.matchedPattern).toBe("codex_vscode"); }); test("codex-tui UA does NOT match codex_vscode pattern (isolation)", () => { const session = createMockSession({ userAgent: "codex-tui/0.115.0 (Mac OS 15.7.3; arm64)", }); const result = isClientAllowedDetailed(session, ["codex_vscode"], []); expect(result.allowed).toBe(false); }); test("codex-tui UA does NOT match codex_exec pattern (isolation)", () => { const session = createMockSession({ userAgent: "codex-tui/0.115.0" }); const result = isClientAllowedDetailed(session, ["codex_exec"], []); expect(result.allowed).toBe(false); }); test("Codex Desktop UA still matches codex-cli (regression)", () => { const session = createMockSession({ userAgent: "Codex Desktop/1.0" }); const result = isClientAllowedDetailed(session, ["codex-cli"], []); expect(result.allowed).toBe(true); }); test("Codex Desktop UA still matches Codex Desktop pattern (regression)", () => { const session = createMockSession({ userAgent: "Codex Desktop/1.0" }); const result = isClientAllowedDetailed(session, ["Codex Desktop"], []); expect(result.allowed).toBe(true); }); }); describe("detectClientFull", () => { test("should return matched=true for confirmed claude-code wildcard", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)"); const result = detectClientFull(session, "claude-code"); expect(result).toEqual({ matched: true, hubConfirmed: true, subClient: "claude-code-sdk-ts", signals: ["x-app-cli", "ua-prefix", "betas-present", "metadata-user-id"], supplementary: [], }); }); test("should return matched=false for confirmed but different builtin sub-client", () => { const session = createConfirmedClaudeCodeSession("claude-cli/1.2.3 (external, sdk-ts)"); const result = detectClientFull(session, "claude-code-cli"); expect(result.hubConfirmed).toBe(true); expect(result.subClient).toBe("claude-code-sdk-ts"); expect(result.matched).toBe(false); }); test("should use custom normalization path for non-builtin patterns", () => { const session = createMockSession({ userAgent: "GeminiCLI/0.22.5" }); const result = detectClientFull(session, "gemini-cli"); expect(result.matched).toBe(true); expect(result.hubConfirmed).toBe(false); expect(result.subClient).toBeNull(); }); test("should return matched=false for custom pattern when User-Agent is missing", () => { const session = createMockSession({ userAgent: null }); const result = detectClientFull(session, "gemini-cli"); expect(result.matched).toBe(false); expect(result.hubConfirmed).toBe(false); expect(result.signals).toEqual([]); expect(result.supplementary).toEqual([]); }); test("should match codex desktop alias in detectClientFull", () => { const session = createMockSession({ userAgent: "Codex Desktop/1.0" }); const result = detectClientFull(session, "codex-cli"); expect(result.matched).toBe(true); expect(result.hubConfirmed).toBe(false); expect(result.subClient).toBeNull(); }); test("should NOT match codex_vscode via codex desktop UA (different family)", () => { const session = createMockSession({ userAgent: "Codex Desktop/1.0" }); const result = detectClientFull(session, "codex_vscode"); expect(result.matched).toBe(false); expect(result.hubConfirmed).toBe(false); }); }); });