package sdk import ( "context" "testing" ) func TestNewEmitter(t *testing.T) { tests := []struct { name string config Config wantErr bool }{ { name: "valid config", config: Config{ ServerURL: "http://localhost:8080", Framework: "test-framework", ClientID: "test-client", Host: "test-host", }, wantErr: false, }, { name: "missing server URL", config: Config{ Framework: "test-framework", ClientID: "test-client", }, wantErr: true, }, { name: "missing framework", config: Config{ ServerURL: "http://localhost:8080", ClientID: "test-client", }, wantErr: true, }, { name: "missing client ID", config: Config{ ServerURL: "http://localhost:8080", Framework: "test-framework", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { emitter, err := NewEmitter(tt.config) if (err != nil) != tt.wantErr { t.Errorf("NewEmitter() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr { _ = emitter.Close(context.Background()) } }) } } func TestGenerateID(t *testing.T) { id1 := generateID() id2 := generateID() if id1 == id2 { t.Errorf("generateID() should produce unique IDs, got duplicate: %s", id1) } if len(id1) == 0 { t.Error("generateID() should return a non-empty string") } } func TestNewSessionStart(t *testing.T) { sessionID := "test-session-001" event := NewSessionStart(sessionID) if event["event"].(map[string]any)["type"] != "session.start" { t.Error("NewSessionStart() should create session.start event") } if event["correlation"].(map[string]any)["session_id"] != sessionID { t.Error("NewSessionStart() should set session_id in correlation") } if event["schema"].(map[string]any)["name"] != schemaName { t.Error("NewSessionStart() should set correct schema name") } } func TestNewRunStart(t *testing.T) { sessionID := "test-session-001" runID := "test-run-001" event := NewRunStart(sessionID, runID) if event["event"].(map[string]any)["type"] != "run.start" { t.Error("NewRunStart() should create run.start event") } if event["correlation"].(map[string]any)["session_id"] != sessionID { t.Error("NewRunStart() should set session_id in correlation") } if event["correlation"].(map[string]any)["run_id"] != runID { t.Error("NewRunStart() should set run_id in correlation") } } func TestNewSpanStart(t *testing.T) { sessionID := "test-session-001" runID := "test-run-001" traceID := "test-trace-001" spanID := "test-span-001" event := NewSpanStart(sessionID, runID, traceID, spanID) if event["event"].(map[string]any)["type"] != "span.start" { t.Error("NewSpanStart() should create span.start event") } if event["correlation"].(map[string]any)["span_id"] != spanID { t.Error("NewSpanStart() should set span_id in correlation") } if event["correlation"].(map[string]any)["trace_id"] != traceID { t.Error("NewSpanStart() should set trace_id in correlation") } } func TestNewRunEnd(t *testing.T) { sessionID := "test-session-001" runID := "test-run-001" status := "success" durationMs := int64(60000) event := NewRunEnd(sessionID, runID, status, durationMs) if event["event"].(map[string]any)["type"] != "run.end" { t.Error("NewRunEnd() should create run.end event") } payload := event["payload"].(map[string]any) if payload["status"] != status { t.Errorf("NewRunEnd() should set status to %s", status) } if payload["duration_ms"] != durationMs { t.Errorf("NewRunEnd() should set duration_ms to %d", durationMs) } } func TestNewSpanEnd(t *testing.T) { sessionID := "test-session-001" runID := "test-run-001" traceID := "test-trace-001" spanID := "test-span-001" status := "success" durationMs := int64(1000) event := NewSpanEnd(sessionID, runID, traceID, spanID, status, durationMs) if event["event"].(map[string]any)["type"] != "span.end" { t.Error("NewSpanEnd() should create span.end event") } payload := event["payload"].(map[string]any) if payload["status"] != status { t.Errorf("NewSpanEnd() should set status to %s", status) } if payload["duration_ms"] != durationMs { t.Errorf("NewSpanEnd() should set duration_ms to %d", durationMs) } } func TestNewError(t *testing.T) { sessionID := "test-session-001" runID := "test-run-001" traceID := "test-trace-001" spanID := "test-span-001" errType := "validation" message := "invalid input" event := NewError(sessionID, runID, traceID, spanID, errType, message) if event["event"].(map[string]any)["type"] != "error" { t.Error("NewError() should create error event") } payload := event["payload"].(map[string]any) err := payload["error"].(map[string]any) if err["type"] != errType { t.Errorf("NewError() should set error type to %s", errType) } if err["message"] != message { t.Errorf("NewError() should set error message to %s", message) } } func TestNewMetricSnapshot(t *testing.T) { sessionID := "test-session-001" runID := "test-run-001" metrics := map[string]any{ "tokens_in": 1000.0, "tokens_out": 500.0, "cost_usd": 0.002, } event := NewMetricSnapshot(sessionID, runID, metrics) if event["event"].(map[string]any)["type"] != "metric.snapshot" { t.Error("NewMetricSnapshot() should create metric.snapshot event") } payload := event["payload"].(map[string]any) payloadMetrics := payload["metrics"].(map[string]any) if len(payloadMetrics) != len(metrics) { t.Error("NewMetricSnapshot() should include all metrics") } } func TestEventOptions(t *testing.T) { emitter, err := NewEmitter(Config{ ServerURL: "http://localhost:8080", Framework: "test-framework", ClientID: "test-client", Host: "test-host", }) if err != nil { t.Fatalf("NewEmitter() error = %v", err) } defer emitter.Close(context.Background()) sessionID := "test-session-001" event := NewSessionStart(sessionID, WithSource(emitter), WithAttributes(map[string]any{"cwd": "/home/user"}), WithSeq(1), ) if _, ok := event["event"].(map[string]any)["source"]; !ok { t.Error("WithSource() should set source") } if _, ok := event["attributes"]; !ok { t.Error("WithAttributes() should set attributes") } if event["event"].(map[string]any)["seq"] != 1 { t.Error("WithSeq() should set sequence number") } } func TestWithSpanKind(t *testing.T) { sessionID := "test-session-001" runID := "test-run-001" traceID := "test-trace-001" spanID := "test-span-001" event := NewSpanStart(sessionID, runID, traceID, spanID, WithSpanKind("tool")) attrs := event["attributes"].(map[string]any) if attrs["span_kind"] != "tool" { t.Error("WithSpanKind() should set span_kind attribute") } } func TestWithLLMUsage(t *testing.T) { sessionID := "test-session-001" runID := "test-run-001" event := NewRunEnd(sessionID, runID, "success", 60000, WithLLMUsage("claude-3-opus", 1000, 500, 0.015), ) payload := event["payload"].(map[string]any) llm := payload["llm"].(map[string]any) if llm["model"] != "claude-3-opus" { t.Error("WithLLMUsage() should set model") } usage := llm["usage"].(map[string]any) if usage["input_tokens"] != 1000 { t.Error("WithLLMUsage() should set input_tokens") } if usage["output_tokens"] != 500 { t.Error("WithLLMUsage() should set output_tokens") } cost := llm["cost"].(map[string]any) if cost["total_usd"] != 0.015 { t.Error("WithLLMUsage() should set total_usd") } } func TestEmit(t *testing.T) { emitter, err := NewEmitter(Config{ ServerURL: "http://localhost:9999", Framework: "test-framework", ClientID: "test-client", Host: "test-host", BufferSize: 10, }) if err != nil { t.Fatalf("NewEmitter() error = %v", err) } sessionID := "test-session-001" event := NewSessionStart(sessionID, WithSource(emitter)) ctx := context.Background() err = emitter.Emit(ctx, event) if err != nil { t.Errorf("Emit() error = %v", err) } err = emitter.Emit(ctx, NewSessionStart(sessionID+"-2", WithSource(emitter))) if err != nil { t.Errorf("Emit() error = %v", err) } emitter.mu.Lock() buffered := len(emitter.buffer) emitter.mu.Unlock() if buffered != 2 { t.Errorf("Expected 2 buffered events, got %d", buffered) } } func TestWsURLFromHTTP(t *testing.T) { tests := []struct { httpURL string want string }{ {"http://localhost:8080", "ws://localhost:8080/v1/ws"}, {"https://example.com", "wss://example.com/v1/ws"}, {"http://example.com:8080", "ws://example.com:8080/v1/ws"}, } for _, tt := range tests { t.Run(tt.httpURL, func(t *testing.T) { if got := wsURLFromHTTP(tt.httpURL); got != tt.want { t.Errorf("wsURLFromHTTP() = %v, want %v", got, tt.want) } }) } }