|
| 1 | +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" |
1 | 2 | import { ClaudeCodeHandler } from "../claude-code" |
2 | 3 | import { ApiHandlerOptions } from "../../../shared/api" |
3 | 4 | import { ClaudeCodeMessage } from "../../../integrations/claude-code/types" |
| 5 | +import { runClaudeCode } from "../../../integrations/claude-code/run" |
| 6 | +import { t } from "../../../i18n" |
4 | 7 |
|
5 | 8 | // Mock the runClaudeCode function |
6 | 9 | vi.mock("../../../integrations/claude-code/run", () => ({ |
7 | 10 | runClaudeCode: vi.fn(), |
8 | 11 | })) |
9 | 12 |
|
| 13 | +vi.mock("../../../i18n", () => ({ |
| 14 | + t: vi.fn((key: string, options?: any) => { |
| 15 | + if (key === "common:errors.claudeCode.authenticationError") { |
| 16 | + return `Claude Code authentication failed. Original error: ${options?.originalError || "unknown"}` |
| 17 | + } |
| 18 | + if (key === "common:errors.claudeCode.apiKeyModelPlanMismatch") { |
| 19 | + return "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." |
| 20 | + } |
| 21 | + return key |
| 22 | + }), |
| 23 | +})) |
| 24 | + |
10 | 25 | // Mock the message filter |
11 | 26 | vi.mock("../../../integrations/claude-code/message-filter", () => ({ |
12 | 27 | filterMessagesForClaudeCode: vi.fn((messages) => messages), |
13 | 28 | })) |
14 | 29 |
|
15 | | -const { runClaudeCode } = await import("../../../integrations/claude-code/run") |
16 | 30 | const { filterMessagesForClaudeCode } = await import("../../../integrations/claude-code/message-filter") |
17 | 31 | const mockRunClaudeCode = vi.mocked(runClaudeCode) |
18 | 32 | const mockFilterMessages = vi.mocked(filterMessagesForClaudeCode) |
@@ -563,3 +577,151 @@ describe("ClaudeCodeHandler", () => { |
563 | 577 | consoleSpy.mockRestore() |
564 | 578 | }) |
565 | 579 | }) |
| 580 | + |
| 581 | +describe("ClaudeCodeHandler Authentication Error Handling", () => { |
| 582 | + let handler: ClaudeCodeHandler |
| 583 | + |
| 584 | + beforeEach(() => { |
| 585 | + vi.clearAllMocks() |
| 586 | + handler = new ClaudeCodeHandler({ |
| 587 | + claudeCodePath: "claude", |
| 588 | + apiModelId: "claude-3-5-sonnet-20241022", |
| 589 | + } as ApiHandlerOptions) |
| 590 | + }) |
| 591 | + |
| 592 | + afterEach(() => { |
| 593 | + vi.clearAllMocks() |
| 594 | + }) |
| 595 | + |
| 596 | + it("should detect and handle authentication errors from API response", async () => { |
| 597 | + const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> { |
| 598 | + yield { |
| 599 | + type: "assistant" as const, |
| 600 | + message: { |
| 601 | + id: "msg_123", |
| 602 | + type: "message", |
| 603 | + role: "assistant", |
| 604 | + model: "claude-3-5-sonnet-20241022", |
| 605 | + content: [ |
| 606 | + { |
| 607 | + type: "text", |
| 608 | + text: 'API Error: 401 {"error":{"message":"Authentication failed. Please login with claude login"}}', |
| 609 | + }, |
| 610 | + ], |
| 611 | + stop_reason: "stop", |
| 612 | + stop_sequence: null, |
| 613 | + usage: { |
| 614 | + input_tokens: 100, |
| 615 | + output_tokens: 50, |
| 616 | + cache_read_input_tokens: 0, |
| 617 | + cache_creation_input_tokens: 0, |
| 618 | + }, |
| 619 | + } as any, |
| 620 | + session_id: "session_123", |
| 621 | + } |
| 622 | + } |
| 623 | + |
| 624 | + mockRunClaudeCode.mockReturnValue(mockGenerator()) |
| 625 | + |
| 626 | + const messages: any[] = [{ role: "user", content: "test" }] |
| 627 | + const generator = handler.createMessage("system", messages) |
| 628 | + |
| 629 | + await expect(async () => { |
| 630 | + for await (const _ of generator) { |
| 631 | + // consume generator |
| 632 | + } |
| 633 | + }).rejects.toThrow("Claude Code authentication failed") |
| 634 | + }) |
| 635 | + |
| 636 | + it("should detect various authentication error patterns", async () => { |
| 637 | + const authErrorMessages = [ |
| 638 | + "API Error: 403 Unauthorized", |
| 639 | + "API Error: Invalid API key", |
| 640 | + "API Error: Credential expired", |
| 641 | + "API Error: Login required", |
| 642 | + "API Error: Not authenticated", |
| 643 | + ] |
| 644 | + |
| 645 | + for (const errorMessage of authErrorMessages) { |
| 646 | + const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> { |
| 647 | + yield { |
| 648 | + type: "assistant" as const, |
| 649 | + message: { |
| 650 | + id: "msg_123", |
| 651 | + type: "message", |
| 652 | + role: "assistant", |
| 653 | + model: "claude-3-5-sonnet-20241022", |
| 654 | + content: [ |
| 655 | + { |
| 656 | + type: "text", |
| 657 | + text: errorMessage, |
| 658 | + }, |
| 659 | + ], |
| 660 | + stop_reason: "stop", |
| 661 | + stop_sequence: null, |
| 662 | + usage: { |
| 663 | + input_tokens: 100, |
| 664 | + output_tokens: 50, |
| 665 | + cache_read_input_tokens: 0, |
| 666 | + cache_creation_input_tokens: 0, |
| 667 | + }, |
| 668 | + } as any, |
| 669 | + session_id: "session_123", |
| 670 | + } |
| 671 | + } |
| 672 | + |
| 673 | + mockRunClaudeCode.mockReturnValue(mockGenerator()) |
| 674 | + |
| 675 | + const messages: any[] = [{ role: "user", content: "test" }] |
| 676 | + const generator = handler.createMessage("system", messages) |
| 677 | + |
| 678 | + await expect(async () => { |
| 679 | + for await (const _ of generator) { |
| 680 | + // consume generator |
| 681 | + } |
| 682 | + }).rejects.toThrow("Claude Code authentication failed") |
| 683 | + |
| 684 | + vi.clearAllMocks() |
| 685 | + } |
| 686 | + }) |
| 687 | + |
| 688 | + it("should not treat non-authentication errors as authentication errors", async () => { |
| 689 | + const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> { |
| 690 | + yield { |
| 691 | + type: "assistant" as const, |
| 692 | + message: { |
| 693 | + id: "msg_123", |
| 694 | + type: "message", |
| 695 | + role: "assistant", |
| 696 | + model: "claude-3-5-sonnet-20241022", |
| 697 | + content: [ |
| 698 | + { |
| 699 | + type: "text", |
| 700 | + text: "API Error: 500 Internal Server Error", |
| 701 | + }, |
| 702 | + ], |
| 703 | + stop_reason: "stop", |
| 704 | + stop_sequence: null, |
| 705 | + usage: { |
| 706 | + input_tokens: 100, |
| 707 | + output_tokens: 50, |
| 708 | + cache_read_input_tokens: 0, |
| 709 | + cache_creation_input_tokens: 0, |
| 710 | + }, |
| 711 | + } as any, |
| 712 | + session_id: "session_123", |
| 713 | + } |
| 714 | + } |
| 715 | + |
| 716 | + mockRunClaudeCode.mockReturnValue(mockGenerator()) |
| 717 | + |
| 718 | + const messages: any[] = [{ role: "user", content: "test" }] |
| 719 | + const generator = handler.createMessage("system", messages) |
| 720 | + |
| 721 | + await expect(async () => { |
| 722 | + for await (const _ of generator) { |
| 723 | + // consume generator |
| 724 | + } |
| 725 | + }).rejects.toThrow("API Error: 500 Internal Server Error") |
| 726 | + }) |
| 727 | +}) |
0 commit comments