1package subagents
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "sort"
9 "strings"
10 "sync"
11 "testing"
12 "time"
13)
14
15func TestTraceContextZeroValueIsNoOp(t *testing.T) {
16 var trace TraceContext
17 if trace.Enabled() {
18 t.Fatal("zero trace context is enabled")
19 }
20 trace.Record(NewTraceEvent("ignored", map[string]any{"secret": "not written"}))
21 if got := TraceContextFrom(context.Background()); got.Enabled() {
22 t.Fatal("context without trace is enabled")
23 }
24 ctx := WithTraceContext(context.Background(), trace)
25 if TraceContextFrom(ctx).Enabled() {
26 t.Fatal("stored no-op context is enabled")
27 }
28}
29
30func TestTraceWriterCreatesPrivateBundleAndDetailedPayload(t *testing.T) {
31 writer, err := NewTraceWriter(filepath.Join(t.TempDir(), "trace"), TraceModeDetailed)
32 if err != nil {
33 t.Fatal(err)
34 }
35 writer.Record(TraceEvent{Type: "tool.result", Data: map[string]any{"name": "cat"}, Payload: map[string]any{"output": "sensitive result"}})
36 if err := writer.Close(); err != nil {
37 t.Fatal(err)
38 }
39
40 for _, name := range []string{"manifest.json", "trace.jsonl", "payloads"} {
41 if _, err := os.Stat(filepath.Join(writer.Dir(), name)); err != nil {
42 t.Fatalf("bundle member %s: %v", name, err)
43 }
44 }
45 assertPrivate(t, writer.Dir(), 0o700)
46 assertPrivate(t, filepath.Join(writer.Dir(), "manifest.json"), 0o600)
47 assertPrivate(t, filepath.Join(writer.Dir(), "trace.jsonl"), 0o600)
48
49 var manifest TraceManifest
50 readJSONFile(t, filepath.Join(writer.Dir(), "manifest.json"), &manifest)
51 if manifest.Mode != TraceModeDetailed || manifest.TraceFile != "trace.jsonl" || manifest.Payloads != "payloads" {
52 t.Fatalf("manifest = %+v", manifest)
53 }
54 lines := readLines(t, filepath.Join(writer.Dir(), "trace.jsonl"))
55 if len(lines) != 1 {
56 t.Fatalf("got %d trace lines, want 1", len(lines))
57 }
58 if strings.Contains(lines[0], "sensitive result") {
59 t.Fatalf("trace event leaked detailed payload: %s", lines[0])
60 }
61 var event TraceEvent
62 if err := json.Unmarshal([]byte(lines[0]), &event); err != nil {
63 t.Fatal(err)
64 }
65 if event.Seq == 0 || event.PayloadFile == "" {
66 t.Fatalf("event = %+v", event)
67 }
68 payloadPath := filepath.Join(writer.Dir(), filepath.FromSlash(event.PayloadFile))
69 assertPrivate(t, payloadPath, 0o600)
70 payload := string(mustRead(t, payloadPath))
71 if !strings.Contains(payload, "sensitive result") {
72 t.Fatalf("payload does not contain detailed value: %s", payload)
73 }
74}
75
76func TestMemoryTraceWriterRetainsOrderedEventsWithoutBundle(t *testing.T) {
77 writer := NewMemoryTraceWriter()
78 for index := 0; index < memoryTraceEventLimit+2; index++ {
79 writer.Record(TraceEvent{Type: "event", Data: map[string]any{"index": index}})
80 // Keep this retention test independent from the separate bounded-queue
81 // contract: a slow Windows race run can otherwise intentionally drop
82 // records before the in-memory limit is exercised.
83 if (index+1)%traceQueueInitialCapacity == 0 {
84 if err := writer.Flush(); err != nil {
85 t.Fatal(err)
86 }
87 }
88 }
89 if err := writer.Flush(); err != nil {
90 t.Fatal(err)
91 }
92 if got := writer.Dir(); got != "" {
93 t.Fatalf("memory trace dir = %q, want empty", got)
94 }
95 events := writer.Events()
96 if len(events) != memoryTraceEventLimit || events[len(events)-1].Seq != memoryTraceEventLimit+2 {
97 t.Fatalf("memory events retained %d with final seq %d", len(events), events[len(events)-1].Seq)
98 }
99 if err := writer.Close(); err != nil {
100 t.Fatal(err)
101 }
102}
103
104func TestSupervisorTraceViewsRetainOpenTurnAcrossEventEviction(t *testing.T) {
105 writer := NewMemoryTraceWriter()
106 t.Cleanup(func() { _ = writer.Close() })
107 supervisor := &Supervisor{trace: writer}
108 started := time.Date(2026, time.August, 13, 1, 52, 0, 0, time.UTC)
109
110 writer.Record(TraceEvent{
111 Timestamp: started,
112 Type: "turn.started",
113 AgentID: "agent-1",
114 TurnID: "turn-1",
115 })
116 for index := 0; index < memoryTraceEventLimit; index++ {
117 writer.Record(TraceEvent{
118 Timestamp: started.Add(time.Duration(index+1) * time.Millisecond),
119 Type: "assistant.stream.observed",
120 AgentID: "agent-1",
121 TurnID: "turn-1",
122 })
123 if (index+1)%traceQueueInitialCapacity == 0 {
124 if err := writer.Flush(); err != nil {
125 t.Fatal(err)
126 }
127 }
128 }
129 requestStarted := started.Add(time.Minute)
130 writer.Record(TraceEvent{
131 Timestamp: requestStarted,
132 Type: "provider.request.started",
133 AgentID: "agent-1",
134 TurnID: "turn-1",
135 })
136 writer.Record(TraceEvent{
137 Timestamp: requestStarted.Add(time.Second),
138 Type: "provider.request.finished",
139 AgentID: "agent-1",
140 TurnID: "turn-1",
141 })
142 if err := writer.Flush(); err != nil {
143 t.Fatal(err)
144 }
145
146 view := supervisor.TraceViews()["agent-1"]
147 if primaryOperation(t, view) == nil || primaryOperation(t, view).Type != "turn.started" {
148 t.Fatalf("primary operation after request = %#v, want open delegated turn", primaryOperation(t, view))
149 }
150 if !primaryOperation(t, view).StartedAt.Equal(started) {
151 t.Fatalf("delegated turn started at %v, want %v", primaryOperation(t, view).StartedAt, started)
152 }
153
154 nextRequestStarted := requestStarted.Add(2 * time.Second)
155 writer.Record(TraceEvent{
156 Timestamp: nextRequestStarted,
157 Type: "provider.request.started",
158 AgentID: "agent-1",
159 TurnID: "turn-1",
160 })
161 if err := writer.Flush(); err != nil {
162 t.Fatal(err)
163 }
164 view = supervisor.TraceViews()["agent-1"]
165 if primaryOperation(t, view) == nil || primaryOperation(t, view).Type != "provider.request.started" {
166 t.Fatalf("primary operation during next request = %#v, want provider request", primaryOperation(t, view))
167 }
168 if got := view.TurnStartedAt(*primaryOperation(t, view)); !got.Equal(started) {
169 t.Fatalf("elapsed timer anchor = %v, want delegated turn start %v", got, started)
170 }
171}
172
173func TestTraceViewsObserveIngestedBoundariesBeforeAsyncWrite(t *testing.T) {
174 writer := &TraceWriter{
175 pending: make([]traceRecord, 0, traceQueueInitialCapacity),
176 }
177 writer.cond = sync.NewCond(&writer.mu)
178 writer.Record(TraceEvent{
179 Timestamp: time.Date(2026, time.August, 13, 2, 0, 0, 0, time.UTC),
180 Type: "turn.started",
181 AgentID: "agent-1",
182 TurnID: "turn-1",
183 })
184
185 view := writer.Views()["agent-1"]
186 if primaryOperation(t, view) == nil || primaryOperation(t, view).Type != "turn.started" {
187 t.Fatalf("primary operation before async write = %#v, want delegated turn", primaryOperation(t, view))
188 }
189}
190
191func TestTraceWriterNormalRedactsSensitiveValues(t *testing.T) {
192 writer, err := NewTraceWriter(filepath.Join(t.TempDir(), "trace"), TraceModeNormal)
193 if err != nil {
194 t.Fatal(err)
195 }
196 secret := "super-secret-value"
197 writer.Record(TraceEvent{
198 Type: "request",
199 Data: map[string]any{
200 "name": "safe", "authorization": secret, "openai_api_key": secret,
201 "nested": map[string]any{"token": secret, "count": 2, "headers": map[string]string{"x_api_key": secret}},
202 },
203 Payload: map[string]any{"content": secret},
204 })
205 if err := writer.Close(); err != nil {
206 t.Fatal(err)
207 }
208 data := string(mustRead(t, filepath.Join(writer.Dir(), "trace.jsonl")))
209 if strings.Contains(data, secret) || strings.Contains(data, "content") {
210 t.Fatalf("normal trace leaked sensitive value: %s", data)
211 }
212 if !strings.Contains(data, "[REDACTED]") || !strings.Contains(data, "safe") {
213 t.Fatalf("normal trace did not retain safe metadata/redaction: %s", data)
214 }
215 entries, err := os.ReadDir(filepath.Join(writer.Dir(), "payloads"))
216 if err != nil {
217 t.Fatal(err)
218 }
219 if len(entries) != 0 {
220 t.Fatalf("normal trace wrote %d payload files", len(entries))
221 }
222}
223
224func TestTraceWriterConcurrentRecordHasOrderedGlobalSequences(t *testing.T) {
225 writer, err := NewTraceWriter(filepath.Join(t.TempDir(), "trace"))
226 if err != nil {
227 t.Fatal(err)
228 }
229 const workers = 8
230 const perWorker = 25
231 var group sync.WaitGroup
232 for worker := 0; worker < workers; worker++ {
233 group.Add(1)
234 go func(worker int) {
235 defer group.Done()
236 for i := 0; i < perWorker; i++ {
237 writer.Record(NewTraceEvent("event", map[string]any{"worker": worker, "index": i}))
238 }
239 }(worker)
240 }
241 group.Wait()
242 if err := writer.Close(); err != nil {
243 t.Fatal(err)
244 }
245 lines := readLines(t, filepath.Join(writer.Dir(), "trace.jsonl"))
246 if len(lines) != workers*perWorker {
247 t.Fatalf("got %d lines, want %d", len(lines), workers*perWorker)
248 }
249 seqs := make([]uint64, 0, len(lines))
250 for _, line := range lines {
251 var event TraceEvent
252 if err := json.Unmarshal([]byte(line), &event); err != nil {
253 t.Fatal(err)
254 }
255 seqs = append(seqs, event.Seq)
256 }
257 if !sort.SliceIsSorted(seqs, func(i, j int) bool { return seqs[i] < seqs[j] }) {
258 t.Fatalf("trace sequences are not ordered: %v", seqs)
259 }
260 for i := 1; i < len(seqs); i++ {
261 if seqs[i] != seqs[i-1]+1 {
262 t.Fatalf("sequence gap at %d: %d then %d", i, seqs[i-1], seqs[i])
263 }
264 }
265}
266
267func assertPrivate(t *testing.T, path string, want os.FileMode) {
268 t.Helper()
269 if os.PathSeparator == '\\' {
270 // Windows does not expose POSIX permission bits through os.FileMode.
271 // The writer still requests restrictive ACL-compatible modes; assert the
272 // portable property that the bundle member exists instead.
273 if _, err := os.Stat(path); err != nil {
274 t.Fatal(err)
275 }
276 return
277 }
278 info, err := os.Stat(path)
279 if err != nil {
280 t.Fatal(err)
281 }
282 if got := info.Mode().Perm(); got != want {
283 t.Fatalf("%s mode = %o, want %o", path, got, want)
284 }
285}
286
287func readJSONFile(t *testing.T, path string, out any) {
288 t.Helper()
289 if err := json.Unmarshal(mustRead(t, path), out); err != nil {
290 t.Fatal(err)
291 }
292}
293
294func readLines(t *testing.T, path string) []string {
295 t.Helper()
296 data := strings.TrimSpace(string(mustRead(t, path)))
297 if data == "" {
298 return nil
299 }
300 return strings.Split(data, "\n")
301}
302
303func mustRead(t *testing.T, path string) []byte {
304 t.Helper()
305 data, err := os.ReadFile(path)
306 if err != nil {
307 t.Fatal(err)
308 }
309 return data
310}