|
|
@@ -1,230 +1,503 @@
|
|
|
import { describe, test, expect, vi, beforeEach } from "vitest"
|
|
|
import { ClaudeCodeHandler } from "../claude-code"
|
|
|
import { ApiHandlerOptions } from "../../../shared/api"
|
|
|
+import { ClaudeCodeMessage } from "../../../integrations/claude-code/types"
|
|
|
|
|
|
// Mock the runClaudeCode function
|
|
|
vi.mock("../../../integrations/claude-code/run", () => ({
|
|
|
runClaudeCode: vi.fn(),
|
|
|
}))
|
|
|
|
|
|
+// Mock the message filter
|
|
|
+vi.mock("../../../integrations/claude-code/message-filter", () => ({
|
|
|
+ filterMessagesForClaudeCode: vi.fn((messages) => messages),
|
|
|
+}))
|
|
|
+
|
|
|
const { runClaudeCode } = await import("../../../integrations/claude-code/run")
|
|
|
+const { filterMessagesForClaudeCode } = await import("../../../integrations/claude-code/message-filter")
|
|
|
const mockRunClaudeCode = vi.mocked(runClaudeCode)
|
|
|
-
|
|
|
-// Mock the EventEmitter for the process
|
|
|
-class MockEventEmitter {
|
|
|
- private handlers: { [event: string]: ((...args: any[]) => void)[] } = {}
|
|
|
-
|
|
|
- on(event: string, handler: (...args: any[]) => void) {
|
|
|
- if (!this.handlers[event]) {
|
|
|
- this.handlers[event] = []
|
|
|
- }
|
|
|
- this.handlers[event].push(handler)
|
|
|
- }
|
|
|
-
|
|
|
- emit(event: string, ...args: any[]) {
|
|
|
- if (this.handlers[event]) {
|
|
|
- this.handlers[event].forEach((handler) => handler(...args))
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+const mockFilterMessages = vi.mocked(filterMessagesForClaudeCode)
|
|
|
|
|
|
describe("ClaudeCodeHandler", () => {
|
|
|
let handler: ClaudeCodeHandler
|
|
|
- let mockProcess: any
|
|
|
|
|
|
beforeEach(() => {
|
|
|
+ vi.clearAllMocks()
|
|
|
const options: ApiHandlerOptions = {
|
|
|
claudeCodePath: "claude",
|
|
|
apiModelId: "claude-3-5-sonnet-20241022",
|
|
|
}
|
|
|
handler = new ClaudeCodeHandler(options)
|
|
|
+ })
|
|
|
|
|
|
- const mainEmitter = new MockEventEmitter()
|
|
|
- mockProcess = {
|
|
|
- stdout: new MockEventEmitter(),
|
|
|
- stderr: new MockEventEmitter(),
|
|
|
- on: mainEmitter.on.bind(mainEmitter),
|
|
|
- emit: mainEmitter.emit.bind(mainEmitter),
|
|
|
+ test("should create handler with correct model configuration", () => {
|
|
|
+ const model = handler.getModel()
|
|
|
+ expect(model.id).toBe("claude-3-5-sonnet-20241022")
|
|
|
+ expect(model.info.supportsImages).toBe(false)
|
|
|
+ expect(model.info.supportsPromptCache).toBe(false)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("should use default model when invalid model provided", () => {
|
|
|
+ const options: ApiHandlerOptions = {
|
|
|
+ claudeCodePath: "claude",
|
|
|
+ apiModelId: "invalid-model",
|
|
|
}
|
|
|
+ const handlerWithInvalidModel = new ClaudeCodeHandler(options)
|
|
|
+ const model = handlerWithInvalidModel.getModel()
|
|
|
|
|
|
- mockRunClaudeCode.mockReturnValue(mockProcess)
|
|
|
+ expect(model.id).toBe("claude-sonnet-4-20250514") // default model
|
|
|
})
|
|
|
|
|
|
- test("should handle thinking content properly", async () => {
|
|
|
+ test("should filter messages and call runClaudeCode", async () => {
|
|
|
const systemPrompt = "You are a helpful assistant"
|
|
|
const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
+ const filteredMessages = [{ role: "user" as const, content: "Hello (filtered)" }]
|
|
|
+
|
|
|
+ mockFilterMessages.mockReturnValue(filteredMessages)
|
|
|
+
|
|
|
+ // Mock empty async generator
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ // Empty generator for basic test
|
|
|
+ }
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
|
|
|
- // Start the stream
|
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
|
- const streamGenerator = stream[Symbol.asyncIterator]()
|
|
|
-
|
|
|
- // Simulate thinking content response
|
|
|
- const thinkingResponse = {
|
|
|
- type: "assistant",
|
|
|
- message: {
|
|
|
- id: "msg_123",
|
|
|
- type: "message",
|
|
|
- role: "assistant",
|
|
|
- model: "claude-3-5-sonnet-20241022",
|
|
|
- content: [
|
|
|
- {
|
|
|
- type: "thinking",
|
|
|
- thinking: "I need to think about this carefully...",
|
|
|
- signature: "abc123",
|
|
|
- },
|
|
|
- ],
|
|
|
- stop_reason: null,
|
|
|
- stop_sequence: null,
|
|
|
- usage: {
|
|
|
- input_tokens: 10,
|
|
|
- output_tokens: 20,
|
|
|
- service_tier: "standard" as const,
|
|
|
- },
|
|
|
- },
|
|
|
- session_id: "session_123",
|
|
|
- }
|
|
|
-
|
|
|
- // Emit the thinking response and wait for processing
|
|
|
- setImmediate(() => {
|
|
|
- mockProcess.stdout.emit("data", JSON.stringify(thinkingResponse) + "\n")
|
|
|
- setImmediate(() => {
|
|
|
- mockProcess.emit("close", 0)
|
|
|
- })
|
|
|
+
|
|
|
+ // Need to start iterating to trigger the call
|
|
|
+ const iterator = stream[Symbol.asyncIterator]()
|
|
|
+ await iterator.next()
|
|
|
+
|
|
|
+ // Verify message filtering was called
|
|
|
+ expect(mockFilterMessages).toHaveBeenCalledWith(messages)
|
|
|
+
|
|
|
+ // Verify runClaudeCode was called with filtered messages
|
|
|
+ expect(mockRunClaudeCode).toHaveBeenCalledWith({
|
|
|
+ systemPrompt,
|
|
|
+ messages: filteredMessages,
|
|
|
+ path: "claude",
|
|
|
+ modelId: "claude-3-5-sonnet-20241022",
|
|
|
})
|
|
|
+ })
|
|
|
+
|
|
|
+ test("should handle thinking content properly", async () => {
|
|
|
+ const systemPrompt = "You are a helpful assistant"
|
|
|
+ const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
+
|
|
|
+ // Mock async generator that yields thinking content
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ yield {
|
|
|
+ type: "assistant" as const,
|
|
|
+ message: {
|
|
|
+ id: "msg_123",
|
|
|
+ type: "message",
|
|
|
+ role: "assistant",
|
|
|
+ model: "claude-3-5-sonnet-20241022",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "thinking",
|
|
|
+ thinking: "I need to think about this carefully...",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ stop_reason: null,
|
|
|
+ stop_sequence: null,
|
|
|
+ usage: {
|
|
|
+ input_tokens: 10,
|
|
|
+ output_tokens: 20,
|
|
|
+ },
|
|
|
+ } as any,
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
+
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages)
|
|
|
+ const results = []
|
|
|
|
|
|
- // Get the result
|
|
|
- const result = await streamGenerator.next()
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ results.push(chunk)
|
|
|
+ }
|
|
|
|
|
|
- expect(result.done).toBe(false)
|
|
|
- expect(result.value).toEqual({
|
|
|
+ expect(results).toHaveLength(1)
|
|
|
+ expect(results[0]).toEqual({
|
|
|
type: "reasoning",
|
|
|
text: "I need to think about this carefully...",
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- test("should handle mixed content types", async () => {
|
|
|
+ test("should handle redacted thinking content", async () => {
|
|
|
const systemPrompt = "You are a helpful assistant"
|
|
|
const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
|
|
|
- const stream = handler.createMessage(systemPrompt, messages)
|
|
|
- const streamGenerator = stream[Symbol.asyncIterator]()
|
|
|
-
|
|
|
- // Simulate mixed content response
|
|
|
- const mixedResponse = {
|
|
|
- type: "assistant",
|
|
|
- message: {
|
|
|
- id: "msg_123",
|
|
|
- type: "message",
|
|
|
- role: "assistant",
|
|
|
- model: "claude-3-5-sonnet-20241022",
|
|
|
- content: [
|
|
|
- {
|
|
|
- type: "thinking",
|
|
|
- thinking: "Let me think about this...",
|
|
|
+ // Mock async generator that yields redacted thinking content
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ yield {
|
|
|
+ type: "assistant" as const,
|
|
|
+ message: {
|
|
|
+ id: "msg_123",
|
|
|
+ type: "message",
|
|
|
+ role: "assistant",
|
|
|
+ model: "claude-3-5-sonnet-20241022",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "redacted_thinking",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ stop_reason: null,
|
|
|
+ stop_sequence: null,
|
|
|
+ usage: {
|
|
|
+ input_tokens: 10,
|
|
|
+ output_tokens: 20,
|
|
|
},
|
|
|
- {
|
|
|
- type: "text",
|
|
|
- text: "Here's my response!",
|
|
|
- },
|
|
|
- ],
|
|
|
- stop_reason: null,
|
|
|
- stop_sequence: null,
|
|
|
- usage: {
|
|
|
- input_tokens: 10,
|
|
|
- output_tokens: 20,
|
|
|
- service_tier: "standard" as const,
|
|
|
- },
|
|
|
- },
|
|
|
- session_id: "session_123",
|
|
|
- }
|
|
|
-
|
|
|
- // Emit the mixed response and wait for processing
|
|
|
- setImmediate(() => {
|
|
|
- mockProcess.stdout.emit("data", JSON.stringify(mixedResponse) + "\n")
|
|
|
- setImmediate(() => {
|
|
|
- mockProcess.emit("close", 0)
|
|
|
- })
|
|
|
+ } as any,
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
+
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages)
|
|
|
+ const results = []
|
|
|
+
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ results.push(chunk)
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(results).toHaveLength(1)
|
|
|
+ expect(results[0]).toEqual({
|
|
|
+ type: "reasoning",
|
|
|
+ text: "[Redacted thinking block]",
|
|
|
})
|
|
|
+ })
|
|
|
+
|
|
|
+ test("should handle mixed content types", async () => {
|
|
|
+ const systemPrompt = "You are a helpful assistant"
|
|
|
+ const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
+
|
|
|
+ // Mock async generator that yields mixed content
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ yield {
|
|
|
+ type: "assistant" as const,
|
|
|
+ message: {
|
|
|
+ id: "msg_123",
|
|
|
+ type: "message",
|
|
|
+ role: "assistant",
|
|
|
+ model: "claude-3-5-sonnet-20241022",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "thinking",
|
|
|
+ thinking: "Let me think about this...",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: "text",
|
|
|
+ text: "Here's my response!",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ stop_reason: null,
|
|
|
+ stop_sequence: null,
|
|
|
+ usage: {
|
|
|
+ input_tokens: 10,
|
|
|
+ output_tokens: 20,
|
|
|
+ },
|
|
|
+ } as any,
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // Get the first result (thinking)
|
|
|
- const thinkingResult = await streamGenerator.next()
|
|
|
- expect(thinkingResult.done).toBe(false)
|
|
|
- expect(thinkingResult.value).toEqual({
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
+
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages)
|
|
|
+ const results = []
|
|
|
+
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ results.push(chunk)
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(results).toHaveLength(2)
|
|
|
+ expect(results[0]).toEqual({
|
|
|
type: "reasoning",
|
|
|
text: "Let me think about this...",
|
|
|
})
|
|
|
-
|
|
|
- // Get the second result (text)
|
|
|
- const textResult = await streamGenerator.next()
|
|
|
- expect(textResult.done).toBe(false)
|
|
|
- expect(textResult.value).toEqual({
|
|
|
+ expect(results[1]).toEqual({
|
|
|
type: "text",
|
|
|
text: "Here's my response!",
|
|
|
})
|
|
|
})
|
|
|
|
|
|
- test("should handle stop_reason with thinking content in error messages", async () => {
|
|
|
+ test("should handle string chunks from generator", async () => {
|
|
|
const systemPrompt = "You are a helpful assistant"
|
|
|
const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
|
|
|
+ // Mock async generator that yields string chunks
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ yield "This is a string chunk"
|
|
|
+ yield "Another string chunk"
|
|
|
+ }
|
|
|
+
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
+
|
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
|
- const streamGenerator = stream[Symbol.asyncIterator]()
|
|
|
-
|
|
|
- // Simulate error response with thinking content
|
|
|
- const errorResponse = {
|
|
|
- type: "assistant",
|
|
|
- message: {
|
|
|
- id: "msg_123",
|
|
|
- type: "message",
|
|
|
- role: "assistant",
|
|
|
- model: "claude-3-5-sonnet-20241022",
|
|
|
- content: [
|
|
|
- {
|
|
|
- type: "thinking",
|
|
|
- thinking: "This is an error scenario",
|
|
|
- },
|
|
|
- ],
|
|
|
- stop_reason: "max_tokens",
|
|
|
- stop_sequence: null,
|
|
|
- usage: {
|
|
|
- input_tokens: 10,
|
|
|
- output_tokens: 20,
|
|
|
- service_tier: "standard" as const,
|
|
|
- },
|
|
|
- },
|
|
|
- session_id: "session_123",
|
|
|
- }
|
|
|
-
|
|
|
- // Emit the error response and wait for processing
|
|
|
- setImmediate(() => {
|
|
|
- mockProcess.stdout.emit("data", JSON.stringify(errorResponse) + "\n")
|
|
|
- setImmediate(() => {
|
|
|
- mockProcess.emit("close", 0)
|
|
|
- })
|
|
|
+ const results = []
|
|
|
+
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ results.push(chunk)
|
|
|
+ }
|
|
|
+
|
|
|
+ expect(results).toHaveLength(2)
|
|
|
+ expect(results[0]).toEqual({
|
|
|
+ type: "text",
|
|
|
+ text: "This is a string chunk",
|
|
|
+ })
|
|
|
+ expect(results[1]).toEqual({
|
|
|
+ type: "text",
|
|
|
+ text: "Another string chunk",
|
|
|
})
|
|
|
+ })
|
|
|
+
|
|
|
+ test("should handle usage and cost tracking with paid usage", async () => {
|
|
|
+ const systemPrompt = "You are a helpful assistant"
|
|
|
+ const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
+
|
|
|
+ // Mock async generator with init, assistant, and result messages
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ // Init message indicating paid usage
|
|
|
+ yield {
|
|
|
+ type: "system" as const,
|
|
|
+ subtype: "init" as const,
|
|
|
+ session_id: "session_123",
|
|
|
+ tools: [],
|
|
|
+ mcp_servers: [],
|
|
|
+ apiKeySource: "/login managed key",
|
|
|
+ }
|
|
|
+
|
|
|
+ // Assistant message
|
|
|
+ yield {
|
|
|
+ type: "assistant" as const,
|
|
|
+ message: {
|
|
|
+ id: "msg_123",
|
|
|
+ type: "message",
|
|
|
+ role: "assistant",
|
|
|
+ model: "claude-3-5-sonnet-20241022",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "text",
|
|
|
+ text: "Hello there!",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ stop_reason: null,
|
|
|
+ stop_sequence: null,
|
|
|
+ usage: {
|
|
|
+ input_tokens: 10,
|
|
|
+ output_tokens: 20,
|
|
|
+ cache_read_input_tokens: 5,
|
|
|
+ cache_creation_input_tokens: 3,
|
|
|
+ },
|
|
|
+ } as any,
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+
|
|
|
+ // Result message
|
|
|
+ yield {
|
|
|
+ type: "result" as const,
|
|
|
+ subtype: "success" as const,
|
|
|
+ total_cost_usd: 0.05,
|
|
|
+ is_error: false,
|
|
|
+ duration_ms: 1000,
|
|
|
+ duration_api_ms: 800,
|
|
|
+ num_turns: 1,
|
|
|
+ result: "success",
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
+
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages)
|
|
|
+ const results = []
|
|
|
|
|
|
- // Should throw error with thinking content
|
|
|
- await expect(streamGenerator.next()).rejects.toThrow("This is an error scenario")
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ results.push(chunk)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Should have text chunk and usage chunk
|
|
|
+ expect(results).toHaveLength(2)
|
|
|
+ expect(results[0]).toEqual({
|
|
|
+ type: "text",
|
|
|
+ text: "Hello there!",
|
|
|
+ })
|
|
|
+ expect(results[1]).toEqual({
|
|
|
+ type: "usage",
|
|
|
+ inputTokens: 10,
|
|
|
+ outputTokens: 20,
|
|
|
+ cacheReadTokens: 5,
|
|
|
+ cacheWriteTokens: 3,
|
|
|
+ totalCost: 0.05, // Paid usage, so cost is included
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
- test("should handle incomplete JSON in buffer on process close", async () => {
|
|
|
+ test("should handle usage tracking with subscription (free) usage", async () => {
|
|
|
const systemPrompt = "You are a helpful assistant"
|
|
|
const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
|
|
|
+ // Mock async generator with subscription usage
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ // Init message indicating subscription usage
|
|
|
+ yield {
|
|
|
+ type: "system" as const,
|
|
|
+ subtype: "init" as const,
|
|
|
+ session_id: "session_123",
|
|
|
+ tools: [],
|
|
|
+ mcp_servers: [],
|
|
|
+ apiKeySource: "none", // Subscription usage
|
|
|
+ }
|
|
|
+
|
|
|
+ // Assistant message
|
|
|
+ yield {
|
|
|
+ type: "assistant" as const,
|
|
|
+ message: {
|
|
|
+ id: "msg_123",
|
|
|
+ type: "message",
|
|
|
+ role: "assistant",
|
|
|
+ model: "claude-3-5-sonnet-20241022",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "text",
|
|
|
+ text: "Hello there!",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ stop_reason: null,
|
|
|
+ stop_sequence: null,
|
|
|
+ usage: {
|
|
|
+ input_tokens: 10,
|
|
|
+ output_tokens: 20,
|
|
|
+ },
|
|
|
+ } as any,
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+
|
|
|
+ // Result message
|
|
|
+ yield {
|
|
|
+ type: "result" as const,
|
|
|
+ subtype: "success" as const,
|
|
|
+ total_cost_usd: 0.05,
|
|
|
+ is_error: false,
|
|
|
+ duration_ms: 1000,
|
|
|
+ duration_api_ms: 800,
|
|
|
+ num_turns: 1,
|
|
|
+ result: "success",
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
+
|
|
|
const stream = handler.createMessage(systemPrompt, messages)
|
|
|
- const streamGenerator = stream[Symbol.asyncIterator]()
|
|
|
-
|
|
|
- // Simulate incomplete JSON data followed by process close
|
|
|
- setImmediate(() => {
|
|
|
- // Send incomplete JSON (missing closing brace)
|
|
|
- mockProcess.stdout.emit("data", '{"type":"assistant","message":{"id":"msg_123"')
|
|
|
- setImmediate(() => {
|
|
|
- mockProcess.emit("close", 0)
|
|
|
- })
|
|
|
+ const results = []
|
|
|
+
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ results.push(chunk)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Should have text chunk and usage chunk
|
|
|
+ expect(results).toHaveLength(2)
|
|
|
+ expect(results[0]).toEqual({
|
|
|
+ type: "text",
|
|
|
+ text: "Hello there!",
|
|
|
})
|
|
|
+ expect(results[1]).toEqual({
|
|
|
+ type: "usage",
|
|
|
+ inputTokens: 10,
|
|
|
+ outputTokens: 20,
|
|
|
+ cacheReadTokens: 0,
|
|
|
+ cacheWriteTokens: 0,
|
|
|
+ totalCost: 0, // Subscription usage, so cost is 0
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ test("should handle API errors properly", async () => {
|
|
|
+ const systemPrompt = "You are a helpful assistant"
|
|
|
+ const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
+
|
|
|
+ // Mock async generator that yields an API error
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ yield {
|
|
|
+ type: "assistant" as const,
|
|
|
+ message: {
|
|
|
+ id: "msg_123",
|
|
|
+ type: "message",
|
|
|
+ role: "assistant",
|
|
|
+ model: "claude-3-5-sonnet-20241022",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "text",
|
|
|
+ text: 'API Error: 400 {"error":{"message":"Invalid model name"}}',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ stop_reason: "stop_sequence",
|
|
|
+ stop_sequence: null,
|
|
|
+ usage: {
|
|
|
+ input_tokens: 10,
|
|
|
+ output_tokens: 20,
|
|
|
+ },
|
|
|
+ } as any,
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
+
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages)
|
|
|
+ const iterator = stream[Symbol.asyncIterator]()
|
|
|
+
|
|
|
+ // Should throw an error
|
|
|
+ await expect(iterator.next()).rejects.toThrow()
|
|
|
+ })
|
|
|
+
|
|
|
+ test("should log warning for unsupported tool_use content", async () => {
|
|
|
+ const systemPrompt = "You are a helpful assistant"
|
|
|
+ const messages = [{ role: "user" as const, content: "Hello" }]
|
|
|
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
|
+
|
|
|
+ // Mock async generator that yields tool_use content
|
|
|
+ const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
|
|
|
+ yield {
|
|
|
+ type: "assistant" as const,
|
|
|
+ message: {
|
|
|
+ id: "msg_123",
|
|
|
+ type: "message",
|
|
|
+ role: "assistant",
|
|
|
+ model: "claude-3-5-sonnet-20241022",
|
|
|
+ content: [
|
|
|
+ {
|
|
|
+ type: "tool_use",
|
|
|
+ id: "tool_123",
|
|
|
+ name: "test_tool",
|
|
|
+ input: { test: "data" },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ stop_reason: null,
|
|
|
+ stop_sequence: null,
|
|
|
+ usage: {
|
|
|
+ input_tokens: 10,
|
|
|
+ output_tokens: 20,
|
|
|
+ },
|
|
|
+ } as any,
|
|
|
+ session_id: "session_123",
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mockRunClaudeCode.mockReturnValue(mockGenerator())
|
|
|
+
|
|
|
+ const stream = handler.createMessage(systemPrompt, messages)
|
|
|
+ const results = []
|
|
|
+
|
|
|
+ for await (const chunk of stream) {
|
|
|
+ results.push(chunk)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Should log error for unsupported tool_use
|
|
|
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("tool_use is not supported yet"))
|
|
|
|
|
|
- // Should complete without throwing, incomplete JSON should be discarded
|
|
|
- const result = await streamGenerator.next()
|
|
|
- expect(result.done).toBe(true)
|
|
|
+ consoleSpy.mockRestore()
|
|
|
})
|
|
|
})
|