Files
tutti/packages/agent/daemon/runtime/codex_appserver_turn_diagnostics_test.go
T
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

249 lines
7.5 KiB
Go

package agentruntime
import (
"encoding/json"
"strings"
"testing"
"time"
)
type codexAppServerDiagnosticRecord struct {
event string
fields map[string]any
}
type codexAppServerDiagnosticSink struct {
records []codexAppServerDiagnosticRecord
}
func (s *codexAppServerDiagnosticSink) Log(event string, fields map[string]any) {
s.records = append(s.records, codexAppServerDiagnosticRecord{event: event, fields: fields})
}
func TestCodexAppServerTurnDiagnosticsRecordsBoundedToolLifecycle(t *testing.T) {
startedAt := time.Date(2026, 7, 24, 7, 0, 0, 0, time.UTC)
now := startedAt
sink := &codexAppServerDiagnosticSink{}
diagnostics := newCodexAppServerTurnDiagnostics(sink, "turn-1")
diagnostics.startedAt = startedAt
diagnostics.now = func() time.Time { return now }
now = startedAt.Add(time.Second)
command := map[string]any{
"id": "command-1",
"type": "commandExecution",
"command": "curl https://secret.example/?token=do-not-log",
"cwd": "/secret/workspace",
"status": "inProgress",
}
diagnostics.ObserveNotification(appServerNotifyItemStarted, map[string]any{"item": command})
diagnostics.ObserveNotification(appServerNotifyItemStarted, map[string]any{"item": command})
now = startedAt.Add(3250 * time.Millisecond)
command["status"] = "completed"
command["exitCode"] = 7
command["aggregatedOutput"] = "secret command output"
diagnostics.ObserveNotification(appServerNotifyItemCompleted, map[string]any{"item": command})
diagnostics.ObserveNotification(appServerNotifyItemCompleted, map[string]any{"item": command})
now = startedAt.Add(4 * time.Second)
diagnostics.ObserveNotification(appServerNotifyItemStarted, map[string]any{
"item": map[string]any{
"id": "mcp-1",
"type": "mcpToolCall",
"server": "documents",
"tool": "search",
"arguments": map[string]any{"query": "secret query"},
},
})
diagnostics.ObserveNotification(appServerNotifyAgentMessageDelta, map[string]any{
"delta": "secret assistant content",
})
now = startedAt.Add(10 * time.Second)
diagnostics.Finish("completed", "provider-turn-1")
diagnostics.Finish("failed", "provider-turn-1")
if got, want := len(sink.records), 4; got != want {
t.Fatalf("record count = %d, want %d: %#v", got, want, sink.records)
}
wantEvents := []string{
"turn.tool.started",
"turn.tool.completed",
"turn.tool.started",
"turn.completed",
}
for index, want := range wantEvents {
if got := sink.records[index].event; got != want {
t.Fatalf("record[%d].event = %q, want %q", index, got, want)
}
}
completed := sink.records[1].fields
if got := completed["duration_ms"]; got != int64(2250) {
t.Fatalf("command duration_ms = %#v, want 2250", got)
}
if got := completed["outcome"]; got != "failed" {
t.Fatalf("command outcome = %#v, want failed", got)
}
if got := completed["exit_code"]; got != 7 {
t.Fatalf("command exit_code = %#v, want 7", got)
}
summary := sink.records[3].fields
for key, want := range map[string]any{
"duration_ms": int64(10000),
"tool_started_count": 2,
"tool_completed_count": 1,
"tool_failed_count": 1,
"open_tool_count": 1,
"max_tool_duration_ms": int64(2250),
"max_tool_item_type": "commandExecution",
"longest_open_tool_ms": int64(6000),
"longest_open_item_type": "mcpToolCall",
"provider_turn_id": "provider-turn-1",
"turn_id": "turn-1",
"outcome": "completed",
} {
if got := summary[key]; got != want {
t.Fatalf("summary[%q] = %#v, want %#v", key, got, want)
}
}
serializedRecords := make([]map[string]any, 0, len(sink.records))
for _, record := range sink.records {
serializedRecords = append(serializedRecords, map[string]any{
"event": record.event,
"fields": record.fields,
})
}
encoded, err := json.Marshal(serializedRecords)
if err != nil {
t.Fatal(err)
}
for _, secret := range []string{
"do-not-log",
"/secret/workspace",
"secret command output",
"secret query",
"secret assistant content",
} {
if strings.Contains(string(encoded), secret) {
t.Fatalf("diagnostic records leaked %q: %s", secret, encoded)
}
}
}
func TestCodexAppServerTurnDiagnosticsCompletesToolWithoutStart(t *testing.T) {
sink := &codexAppServerDiagnosticSink{}
diagnostics := newCodexAppServerTurnDiagnostics(sink, "turn-2")
diagnostics.ObserveNotification(appServerNotifyItemCompleted, map[string]any{
"item": map[string]any{
"id": "tool-1",
"type": "dynamicToolCall",
"tool": "render",
"status": "completed",
"success": true,
},
})
if got, want := len(sink.records), 1; got != want {
t.Fatalf("record count = %d, want %d", got, want)
}
fields := sink.records[0].fields
if got := fields["start_observed"]; got != false {
t.Fatalf("start_observed = %#v, want false", got)
}
if _, ok := fields["duration_ms"]; ok {
t.Fatalf("duration_ms should be absent when item/started was not observed: %#v", fields)
}
}
func TestCodexAppServerReducerRecordsOnlyAcceptedToolNotifications(t *testing.T) {
adapter := NewCodexAppServerAdapter(nil)
session := Session{
AgentSessionID: "agent-1",
Provider: ProviderCodex,
ProviderSessionID: "thread-1",
}
normalizer := newACPTurnNormalizer()
sink := &codexAppServerDiagnosticSink{}
activeTurn := &codexAppServerActiveTurn{
turnID: "turn-1",
normalizer: normalizer,
diagnostics: newCodexAppServerTurnDiagnostics(sink, "turn-1"),
phase: codexAppServerTurnPhaseRunning,
}
adapter.storeSession(session.AgentSessionID, &codexAppServerSession{
threadID: session.ProviderSessionID,
activeTurn: activeTurn,
})
reducer := newCodexAppServerReducer(adapter)
reducer.ReduceNotification(nil, session, activeTurn.turnID, acpMessage{
Method: appServerNotifyItemStarted,
Params: mustJSONRawMessage(t, map[string]any{
"threadId": session.ProviderSessionID,
"turnId": "provider-turn-1",
"item": map[string]any{
"id": "command-1",
"type": "commandExecution",
"command": "do-not-log this command",
},
}),
}, normalizer, nil)
reducer.ReduceNotification(nil, session, activeTurn.turnID, acpMessage{
Method: appServerNotifyItemStarted,
Params: mustJSONRawMessage(t, map[string]any{
"threadId": "foreign-thread",
"turnId": "foreign-turn",
"item": map[string]any{
"id": "foreign-command",
"type": "commandExecution",
"command": "foreign command",
},
}),
}, normalizer, nil)
if got, want := len(sink.records), 1; got != want {
t.Fatalf("record count = %d, want %d: %#v", got, want, sink.records)
}
if got := sink.records[0].fields["item_id"]; got != "command-1" {
t.Fatalf("item_id = %#v, want command-1", got)
}
}
func TestCodexAppServerToolDiagnosticFieldsRecognizeOnlyTools(t *testing.T) {
toolTypes := []string{
"commandExecution",
"fileChange",
"mcpToolCall",
"webSearch",
"dynamicToolCall",
"collabAgentToolCall",
"imageGeneration",
"imageView",
}
for _, itemType := range toolTypes {
t.Run(itemType, func(t *testing.T) {
_, gotType, _, ok := codexAppServerToolDiagnosticFields(map[string]any{
"id": "item-1",
"type": itemType,
})
if !ok || gotType != itemType {
t.Fatalf("recognized = %v, type = %q", ok, gotType)
}
})
}
for _, itemType := range []string{"agentMessage", "reasoning", "plan", "userMessage"} {
t.Run(itemType, func(t *testing.T) {
if _, _, _, ok := codexAppServerToolDiagnosticFields(map[string]any{
"id": "item-1",
"type": itemType,
}); ok {
t.Fatalf("non-tool item %q was recognized as a tool", itemType)
}
})
}
}