Files
agentmon/internal/sdk/emitter_test.go
T
William Valentin 3434db3c59 feat: complete agent monitoring - hook, UI, and backend filter
- Add event_type and framework filters to events query endpoint
- Add /agents SPA route to web-ui server
- Add Agents nav link and route in frontend
- Add agents page CSS (timeline, VM pills, stats panel)
- Build VM status strip, activity timeline, and real-time stats
- Add agentmon hook for OpenClaw (HOOK.md + handler.ts)
- Add docker-compose, Dockerfile, and supporting infra files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 00:26:42 -07:00

349 lines
8.5 KiB
Go

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)
}
})
}
}