rdev/internal/adapter/codeagent/claudecode/parser_test.go
jordan c59d348040 chore: prepare for composable monorepo template implementation
This commit captures the current state before implementing the composable
monorepo template system. Key changes included:

Infrastructure:
- Add CockroachDB provisioner adapter for database provisioning
- Add Redis provisioner adapter for cache provisioning
- Add build events system with PostgreSQL storage
- Add WebSocket endpoint for real-time build progress

Code agent improvements:
- Fix Claude Code adapter to use default allowed tools instead of dangerously-skip-permissions
- Add context-aware stream closing for cancellation support
- Improve parser tests for edge cases

Build system:
- Add build event constants and metrics
- Remove deprecated git_operations.go (replaced by pod_git_operations.go)
- Add rollback logic for multi-step provisioning operations

Documentation:
- Add composable-monorepo feature documentation
- Add DNS/Cloudflare service documentation
- Update deployment and troubleshooting guides

Cookbooks:
- Add fullstack-app cookbook
- Refactor landing-test with shared library

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 11:39:28 -07:00

359 lines
8.9 KiB
Go

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
wantSubtype string
wantIsError bool
wantMs int64
}{
{
name: "success",
line: `{"type":"result","subtype":"success","is_error":false,"duration_ms":1234}`,
wantSubtype: "success",
wantIsError: false,
wantMs: 1234,
},
{
name: "error",
line: `{"type":"result","subtype":"error","is_error":true,"error":"something went wrong"}`,
wantSubtype: "error",
wantIsError: true,
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.Subtype != tt.wantSubtype {
t.Errorf("expected subtype %q, got %q", tt.wantSubtype, msg.Subtype)
}
if msg.IsError != tt.wantIsError {
t.Errorf("expected is_error %v, got %v", tt.wantIsError, msg.IsError)
}
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,
Subtype: "success",
IsError: false,
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,
Subtype: "error",
IsError: true,
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, Subtype: "success", IsError: false}, true},
{"error result", StreamMessage{Type: StreamMessageResult, Subtype: "error", IsError: true}, 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)
}
})
}
}