package claudecode import ( "testing" "time" "github.com/orchard9/rdev/internal/domain" ) func TestParseStreamMessage_Init(t *testing.T) { line := []byte(`{"type":"init","session_id":"abc123","timestamp":"2024-01-01T00:00:00Z"}`) msg, err := ParseStreamMessage(line) if err != nil { t.Fatalf("unexpected error: %v", err) } if msg.Type != StreamMessageInit { t.Errorf("expected type 'init', got %q", msg.Type) } if msg.SessionID != "abc123" { t.Errorf("expected session_id 'abc123', got %q", msg.SessionID) } } func TestParseStreamMessage_Message(t *testing.T) { line := []byte(`{"type":"message","role":"assistant","content":[{"type":"text","text":"Hello, world!"}]}`) msg, err := ParseStreamMessage(line) if err != nil { t.Fatalf("unexpected error: %v", err) } if msg.Type != StreamMessageMessage { t.Errorf("expected type 'message', got %q", msg.Type) } if msg.Role != "assistant" { t.Errorf("expected role 'assistant', got %q", msg.Role) } if len(msg.Content) != 1 { t.Fatalf("expected 1 content block, got %d", len(msg.Content)) } if msg.Content[0].Text != "Hello, world!" { t.Errorf("expected text 'Hello, world!', got %q", msg.Content[0].Text) } } func TestParseStreamMessage_ToolUse(t *testing.T) { line := []byte(`{"type":"tool_use","name":"Bash","input":{"command":"ls -la"}}`) msg, err := ParseStreamMessage(line) if err != nil { t.Fatalf("unexpected error: %v", err) } if msg.Type != StreamMessageToolUse { t.Errorf("expected type 'tool_use', got %q", msg.Type) } if msg.Name != "Bash" { t.Errorf("expected name 'Bash', got %q", msg.Name) } } func TestParseStreamMessage_ToolResult(t *testing.T) { line := []byte(`{"type":"tool_result","output":"total 64\ndrwxr-xr-x 10 user staff"}`) msg, err := ParseStreamMessage(line) if err != nil { t.Fatalf("unexpected error: %v", err) } if msg.Type != StreamMessageToolResult { t.Errorf("expected type 'tool_result', got %q", msg.Type) } if msg.Output != "total 64\ndrwxr-xr-x 10 user staff" { t.Errorf("unexpected output: %q", msg.Output) } } func TestParseStreamMessage_Result(t *testing.T) { tests := []struct { name string line string wantStatus string wantMs int64 }{ { name: "success", line: `{"type":"result","status":"success","duration_ms":1234}`, wantStatus: "success", wantMs: 1234, }, { name: "error", line: `{"type":"result","status":"error","error":"something went wrong"}`, wantStatus: "error", wantMs: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { msg, err := ParseStreamMessage([]byte(tt.line)) if err != nil { t.Fatalf("unexpected error: %v", err) } if msg.Type != StreamMessageResult { t.Errorf("expected type 'result', got %q", msg.Type) } if msg.Status != tt.wantStatus { t.Errorf("expected status %q, got %q", tt.wantStatus, msg.Status) } if msg.DurationMs != tt.wantMs { t.Errorf("expected duration_ms %d, got %d", tt.wantMs, msg.DurationMs) } }) } } func TestParseStreamMessage_Invalid(t *testing.T) { line := []byte(`not valid json`) _, err := ParseStreamMessage(line) if err == nil { t.Error("expected error for invalid JSON") } } func TestStreamMessage_ToAgentEvent_Init(t *testing.T) { msg := &StreamMessage{ Type: StreamMessageInit, SessionID: "test-session", Timestamp: "2024-01-01T12:00:00Z", } event := msg.ToAgentEvent() if event.Type != domain.AgentEventOutput { t.Errorf("expected type %v, got %v", domain.AgentEventOutput, event.Type) } if event.Content != "Session started" { t.Errorf("expected content 'Session started', got %q", event.Content) } if event.Metadata["session_id"] != "test-session" { t.Errorf("expected session_id in metadata") } } func TestStreamMessage_ToAgentEvent_Message(t *testing.T) { msg := &StreamMessage{ Type: StreamMessageMessage, Role: "assistant", Content: []ContentBlock{ {Type: "text", Text: "Hello from Claude"}, }, } event := msg.ToAgentEvent() if event.Type != domain.AgentEventOutput { t.Errorf("expected type %v, got %v", domain.AgentEventOutput, event.Type) } if event.Content != "Hello from Claude" { t.Errorf("expected content 'Hello from Claude', got %q", event.Content) } } func TestStreamMessage_ToAgentEvent_ToolUse(t *testing.T) { msg := &StreamMessage{ Type: StreamMessageToolUse, Name: "Read", Input: []byte(`{"path":"/workspace/main.go"}`), } event := msg.ToAgentEvent() if event.Type != domain.AgentEventToolUse { t.Errorf("expected type %v, got %v", domain.AgentEventToolUse, event.Type) } if event.ToolName != "Read" { t.Errorf("expected tool name 'Read', got %q", event.ToolName) } if event.ToolInput["path"] != "/workspace/main.go" { t.Errorf("expected path in tool input") } } func TestStreamMessage_ToAgentEvent_ToolResult(t *testing.T) { msg := &StreamMessage{ Type: StreamMessageToolResult, Output: "file contents here", } event := msg.ToAgentEvent() if event.Type != domain.AgentEventToolResult { t.Errorf("expected type %v, got %v", domain.AgentEventToolResult, event.Type) } if event.Content != "file contents here" { t.Errorf("expected content 'file contents here', got %q", event.Content) } } func TestStreamMessage_ToAgentEvent_ResultSuccess(t *testing.T) { msg := &StreamMessage{ Type: StreamMessageResult, Status: "success", DurationMs: 5000, } event := msg.ToAgentEvent() if event.Type != domain.AgentEventComplete { t.Errorf("expected type %v, got %v", domain.AgentEventComplete, event.Type) } if event.Metadata["status"] != "success" { t.Errorf("expected status in metadata") } if event.Metadata["duration_ms"].(int64) != 5000 { t.Errorf("expected duration_ms in metadata") } } func TestStreamMessage_ToAgentEvent_ResultError(t *testing.T) { msg := &StreamMessage{ Type: StreamMessageResult, Status: "error", Error: "execution failed", } event := msg.ToAgentEvent() if event.Type != domain.AgentEventError { t.Errorf("expected type %v, got %v", domain.AgentEventError, event.Type) } if event.Content != "execution failed" { t.Errorf("expected error content, got %q", event.Content) } } func TestStreamMessage_IsTerminal(t *testing.T) { tests := []struct { msgType StreamMessageType terminal bool }{ {StreamMessageInit, false}, {StreamMessageMessage, false}, {StreamMessageToolUse, false}, {StreamMessageToolResult, false}, {StreamMessageResult, true}, } for _, tt := range tests { t.Run(string(tt.msgType), func(t *testing.T) { msg := &StreamMessage{Type: tt.msgType} if msg.IsTerminal() != tt.terminal { t.Errorf("IsTerminal() = %v, want %v", msg.IsTerminal(), tt.terminal) } }) } } func TestStreamMessage_IsSuccess(t *testing.T) { tests := []struct { name string msg StreamMessage success bool }{ {"success result", StreamMessage{Type: StreamMessageResult, Status: "success"}, true}, {"error result", StreamMessage{Type: StreamMessageResult, Status: "error"}, false}, {"non-result", StreamMessage{Type: StreamMessageMessage}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.msg.IsSuccess() != tt.success { t.Errorf("IsSuccess() = %v, want %v", tt.msg.IsSuccess(), tt.success) } }) } } func TestParseTimestamp(t *testing.T) { tests := []struct { input string valid bool }{ {"2024-01-01T12:00:00Z", true}, {"2024-01-01T12:00:00+00:00", true}, {"invalid", false}, {"", false}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { result := parseTimestamp(tt.input) if tt.valid { if result.Year() != 2024 { t.Errorf("expected year 2024, got %d", result.Year()) } } else { // Should return current time for invalid input if time.Since(result) > time.Second { t.Error("expected recent time for invalid input") } } }) } } func TestExtractTextContent(t *testing.T) { tests := []struct { name string blocks []ContentBlock want string }{ { name: "single text block", blocks: []ContentBlock{{Type: "text", Text: "hello"}}, want: "hello", }, { name: "text after tool_use", blocks: []ContentBlock{ {Type: "tool_use", Name: "Bash"}, {Type: "text", Text: "result"}, }, want: "result", }, { name: "empty blocks", blocks: []ContentBlock{}, want: "", }, { name: "no text blocks", blocks: []ContentBlock{{Type: "tool_use", Name: "Read"}}, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := extractTextContent(tt.blocks); got != tt.want { t.Errorf("extractTextContent() = %q, want %q", got, tt.want) } }) } }