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>
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user