v0.27.0 01c3d31

resident_journal_test.go RawHistoryFolder

1package subagents
2
3import (
4	"encoding/json"
5	"os"
6	"path/filepath"
7	"runtime"
8	"strings"
9	"testing"
10	"time"
11	"unicode/utf8"
12
13	"github.com/bnema/zut/packages/core"
14	"github.com/bnema/zut/packages/provider"
15)
16
17func TestResidentJournalAcceptCommitsAuthorityBeforeMetadataProjection(t *testing.T) {
18	root := t.TempDir()
19	journal, err := OpenResidentJournal(root, "child-1")
20	if err != nil {
21		t.Fatalf("OpenResidentJournal: %v", err)
22	}
23	defer journal.Close()
24
25	spec := ResidentChildSpec{
26		ID:              "child-1",
27		SessionID:       "child-session-1",
28		RootCacheID:     "root-cache-1",
29		ParentSessionID: "parent-session-1",
30		Provider:        "openai-codex",
31		Model:           "gpt-5.6-terra",
32		Profile:         "reviewer",
33		Tools:           []string{"read", "bash"},
34	}
35	if err := journal.Accept(spec, "review this change"); err != nil {
36		t.Fatalf("Accept: %v", err)
37	}
38
39	records, err := ReadResidentJournal(filepath.Join(root, "child-1", residentTranscriptName))
40	if err != nil {
41		t.Fatalf("ReadResidentJournal: %v", err)
42	}
43	if len(records) != 1 || records[0].Type != residentRecordAccepted {
44		t.Fatalf("records = %#v, want one accepted record", records)
45	}
46	if records[0].Spec == nil || records[0].Spec.SessionID != spec.SessionID || records[0].Spec.RootCacheID != spec.RootCacheID || records[0].Prompt != "review this change" {
47		t.Fatalf("accepted record = %#v", records[0])
48	}
49
50	meta, err := ReadResidentMetadata(filepath.Join(root, "child-1", residentMetadataName))
51	if err != nil {
52		t.Fatalf("ReadResidentMetadata: %v", err)
53	}
54	if meta.State != ResidentQueued || meta.SessionID != spec.SessionID || meta.RootCacheID != spec.RootCacheID {
55		t.Fatalf("metadata = %#v", meta)
56	}
57	info, err := os.Stat(filepath.Join(root, "child-1", residentTranscriptName))
58	if err != nil {
59		t.Fatal(err)
60	}
61	if runtime.GOOS != "windows" && info.Mode().Perm()&0o077 != 0 {
62		t.Fatalf("transcript permissions = %o, want no group/other access", info.Mode().Perm())
63	}
64}
65
66func TestResidentJournalReconcilesUsageMetadata(t *testing.T) {
67	root := t.TempDir()
68	journal, err := OpenResidentJournal(root, "usage-child")
69	if err != nil {
70		t.Fatal(err)
71	}
72	spec := ResidentChildSpec{ID: "usage-child", SessionID: "usage-session", Provider: "openai-codex", Model: "gpt-test"}
73	if err := journal.Accept(spec, "task"); err != nil {
74		t.Fatal(err)
75	}
76	journal.ConfigureUsage(272_000, true)
77	turn := provider.Usage{InputTokens: 84_000, OutputTokens: 1_500, CacheReadTokens: 123_000, CostUSD: 0.525}
78	if err := journal.RecordAgentEvent(core.EvUsage{Usage: turn, Cumulative: turn}); err != nil {
79		t.Fatal(err)
80	}
81	if err := journal.RecordAgentEvent(core.EvUsage{Usage: provider.Usage{}, Cumulative: turn}); err != nil {
82		t.Fatal(err)
83	}
84	if err := journal.Close(); err != nil {
85		t.Fatal(err)
86	}
87	metadata, err := ReconcileResidentJournal(filepath.Join(root, spec.ID))
88	if err != nil {
89		t.Fatal(err)
90	}
91	if metadata.Usage != turn || metadata.ContextUsed != 207_000 || metadata.ContextMax != 272_000 || !metadata.Subscription {
92		t.Fatalf("metadata = %#v, want durable usage projection", metadata)
93	}
94}
95
96func TestResidentJournalSkipsDuplicateUsageRecord(t *testing.T) {
97	root := t.TempDir()
98	journal, err := OpenResidentJournal(root, "duplicate-usage")
99	if err != nil {
100		t.Fatal(err)
101	}
102	t.Cleanup(func() { _ = journal.Close() })
103	spec := ResidentChildSpec{ID: "duplicate-usage", SessionID: "usage-session", Provider: "openai", Model: "gpt-test"}
104	if err := journal.Accept(spec, "task"); err != nil {
105		t.Fatal(err)
106	}
107	journal.ConfigureUsage(128_000, false)
108	usage := provider.Usage{InputTokens: 10_000, OutputTokens: 500}
109	event := core.EvUsage{Usage: usage, Cumulative: usage}
110	if err := journal.RecordAgentEvent(event); err != nil {
111		t.Fatal(err)
112	}
113	if err := journal.RecordAgentEvent(event); err != nil {
114		t.Fatal(err)
115	}
116
117	records, err := ReadResidentJournal(filepath.Join(journal.Dir(), residentTranscriptName))
118	if err != nil {
119		t.Fatal(err)
120	}
121	if got := len(records); got != 2 {
122		t.Fatalf("journal records = %d, want accepted record plus one usage record", got)
123	}
124}
125
126func TestResidentJournalDuplicateUsageRetriesFailedMetadataProjection(t *testing.T) {
127	root := t.TempDir()
128	journal, err := OpenResidentJournal(root, "retry-usage-projection")
129	if err != nil {
130		t.Fatal(err)
131	}
132	t.Cleanup(func() { _ = journal.Close() })
133	spec := ResidentChildSpec{ID: "retry-usage-projection", SessionID: "usage-session", Provider: "openai", Model: "gpt-test"}
134	if err := journal.Accept(spec, "task"); err != nil {
135		t.Fatal(err)
136	}
137	journal.ConfigureUsage(128_000, false)
138	metadataPath := filepath.Join(journal.Dir(), residentMetadataName)
139	originalMetadata, err := os.ReadFile(metadataPath)
140	if err != nil {
141		t.Fatal(err)
142	}
143	if err := os.WriteFile(metadataPath, []byte("not-json"), 0o600); err != nil {
144		t.Fatal(err)
145	}
146
147	usage := provider.Usage{InputTokens: 10_000, OutputTokens: 500}
148	event := core.EvUsage{Usage: usage, Cumulative: usage}
149	if err := journal.RecordAgentEvent(event); err == nil {
150		t.Fatal("first usage projection succeeded with malformed metadata")
151	}
152	if err := os.WriteFile(metadataPath, originalMetadata, 0o600); err != nil {
153		t.Fatal(err)
154	}
155	if err := journal.RecordAgentEvent(event); err != nil {
156		t.Fatalf("retry identical usage: %v", err)
157	}
158
159	records, err := ReadResidentJournal(filepath.Join(journal.Dir(), residentTranscriptName))
160	if err != nil {
161		t.Fatal(err)
162	}
163	if got := len(records); got != 2 {
164		t.Fatalf("journal records = %d, want accepted record plus one usage record", got)
165	}
166	metadata, err := ReadResidentMetadata(metadataPath)
167	if err != nil {
168		t.Fatal(err)
169	}
170	if metadata.Usage != usage || metadata.ContextUsed != usage.PromptTokens() {
171		t.Fatalf("metadata usage = %+v, context = %d; want %+v, %d", metadata.Usage, metadata.ContextUsed, usage, usage.PromptTokens())
172	}
173}
174
175func TestReconcileResidentJournalRetainsRootCacheID(t *testing.T) {
176	root := t.TempDir()
177	journal, err := OpenResidentJournal(root, "cache-child")
178	if err != nil {
179		t.Fatal(err)
180	}
181	spec := ResidentChildSpec{ID: "cache-child", SessionID: "child-session", RootCacheID: "root-cache", Provider: "openai", Model: "gpt-test"}
182	if err := journal.Accept(spec, "task"); err != nil {
183		t.Fatal(err)
184	}
185	if err := journal.Close(); err != nil {
186		t.Fatal(err)
187	}
188	metadata, err := ReconcileResidentJournal(filepath.Join(root, spec.ID))
189	if err != nil {
190		t.Fatal(err)
191	}
192	if metadata.RootCacheID != spec.RootCacheID {
193		t.Fatalf("RootCacheID = %q, want %q", metadata.RootCacheID, spec.RootCacheID)
194	}
195}
196
197func TestResidentJournalProjectsUsageEmittedAfterInterruption(t *testing.T) {
198	root := t.TempDir()
199	journal, err := OpenResidentJournal(root, "interrupted-usage")
200	if err != nil {
201		t.Fatal(err)
202	}
203	t.Cleanup(func() { _ = journal.Close() })
204	spec := ResidentChildSpec{ID: "interrupted-usage", SessionID: "usage-session", Provider: "openai", Model: "gpt-test"}
205	if err := journal.Accept(spec, "task"); err != nil {
206		t.Fatal(err)
207	}
208	journal.ConfigureUsage(272_000, false)
209	if err := journal.RecordTurnStarted(spec, "turn-1"); err != nil {
210		t.Fatal(err)
211	}
212	if err := journal.RecordTurnInterrupted(spec, "turn-1"); err != nil {
213		t.Fatal(err)
214	}
215	usage := provider.Usage{InputTokens: 84_000, CacheReadTokens: 123_000}
216	if err := journal.RecordAgentEvent(core.EvUsage{Usage: usage, Cumulative: usage}); err != nil {
217		t.Fatal(err)
218	}
219	metadata, err := ReadResidentMetadata(filepath.Join(journal.Dir(), residentMetadataName))
220	if err != nil {
221		t.Fatal(err)
222	}
223	if metadata.State != ResidentInterrupted || metadata.Usage != usage || metadata.ContextUsed != 207_000 {
224		t.Fatalf("metadata = %#v, want interrupted state with latest usage", metadata)
225	}
226}
227
228func TestResidentProjectionSkipsIdenticalRewrite(t *testing.T) {
229	if runtime.GOOS == "windows" {
230		t.Skip("no-follow projection reads are unavailable on Windows")
231	}
232	dir := t.TempDir()
233	metadata := ResidentMetadata{
234		Version:   residentJournalVersion,
235		ID:        "projection-child",
236		SessionID: "projection-session",
237		State:     ResidentIdle,
238		UpdatedAt: time.Unix(1_700_000_000, 0).UTC(),
239	}
240	if err := writeResidentMetadata(dir, metadata); err != nil {
241		t.Fatal(err)
242	}
243	path := filepath.Join(dir, residentMetadataName)
244	oldTime := time.Unix(1_600_000_000, 0)
245	if err := os.Chtimes(path, oldTime, oldTime); err != nil {
246		t.Fatal(err)
247	}
248
249	if err := writeResidentMetadata(dir, metadata); err != nil {
250		t.Fatal(err)
251	}
252	info, err := os.Stat(path)
253	if err != nil {
254		t.Fatal(err)
255	}
256	if !info.ModTime().Equal(oldTime) {
257		t.Fatalf("identical projection rewrite changed mtime to %v", info.ModTime())
258	}
259
260	metadata.State = ResidentStopped
261	if err := writeResidentMetadata(dir, metadata); err != nil {
262		t.Fatal(err)
263	}
264	info, err = os.Stat(path)
265	if err != nil {
266		t.Fatal(err)
267	}
268	if info.ModTime().Equal(oldTime) {
269		t.Fatal("changed projection was not rewritten")
270	}
271}
272
273func TestResidentProjectionReplacesSymlinkInsteadOfFollowingIt(t *testing.T) {
274	if runtime.GOOS == "windows" {
275		t.Skip("symlink creation may require elevated privileges on Windows")
276	}
277	dir := t.TempDir()
278	metadata := ResidentMetadata{Version: residentJournalVersion, ID: "projection-child", SessionID: "projection-session", State: ResidentIdle}
279	data, err := json.Marshal(metadata)
280	if err != nil {
281		t.Fatal(err)
282	}
283	target := filepath.Join(dir, "target.json")
284	if err := os.WriteFile(target, data, 0o600); err != nil {
285		t.Fatal(err)
286	}
287	path := filepath.Join(dir, residentMetadataName)
288	if err := os.Symlink(target, path); err != nil {
289		t.Fatal(err)
290	}
291
292	if err := writeResidentMetadata(dir, metadata); err != nil {
293		t.Fatal(err)
294	}
295	info, err := os.Lstat(path)
296	if err != nil {
297		t.Fatal(err)
298	}
299	if !info.Mode().IsRegular() {
300		t.Fatalf("projection mode = %v, want regular file", info.Mode())
301	}
302}
303
304func TestResidentProjectionRepairsPermissionsForIdenticalData(t *testing.T) {
305	if runtime.GOOS == "windows" {
306		t.Skip("POSIX permission bits are not enforced on Windows")
307	}
308	dir := t.TempDir()
309	metadata := ResidentMetadata{Version: residentJournalVersion, ID: "projection-child", SessionID: "projection-session", State: ResidentIdle}
310	data, err := json.Marshal(metadata)
311	if err != nil {
312		t.Fatal(err)
313	}
314	path := filepath.Join(dir, residentMetadataName)
315	if err := os.WriteFile(path, data, 0o644); err != nil {
316		t.Fatal(err)
317	}
318	if err := os.Chmod(path, 0o644); err != nil {
319		t.Fatal(err)
320	}
321
322	if err := writeResidentMetadata(dir, metadata); err != nil {
323		t.Fatal(err)
324	}
325	info, err := os.Stat(path)
326	if err != nil {
327		t.Fatal(err)
328	}
329	if got := info.Mode().Perm(); got != 0o600 {
330		t.Fatalf("projection permissions = %o, want 600", got)
331	}
332}
333
334func TestOpenResidentJournalRejectsDotChildIDs(t *testing.T) {
335	for _, childID := range []string{".", "..", "nested/child"} {
336		t.Run(childID, func(t *testing.T) {
337			if _, err := OpenResidentJournal(t.TempDir(), childID); err == nil {
338				t.Fatalf("OpenResidentJournal(%q) succeeded", childID)
339			}
340		})
341	}
342}
343
344func TestResidentJournalPersistsFinalizedAgentEvents(t *testing.T) {
345	root := t.TempDir()
346	journal, err := OpenResidentJournal(root, "child-events")
347	if err != nil {
348		t.Fatal(err)
349	}
350	spec := ResidentChildSpec{ID: "child-events", SessionID: "child-session", Provider: "openai", Model: "gpt-5"}
351	if err := journal.Accept(spec, "task"); err != nil {
352		t.Fatal(err)
353	}
354	message := provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "done"}}}
355	if err := journal.RecordAgentEvent(core.EvAssistantMessage{Message: message}); err != nil {
356		t.Fatal(err)
357	}
358	if err := journal.RecordAgentEvent(core.EvToolCall{ID: "call-1", Name: "bash", Args: json.RawMessage(`{"command":"pwd"}`)}); err != nil {
359		t.Fatal(err)
360	}
361	if err := journal.Close(); err != nil {
362		t.Fatal(err)
363	}
364	records, err := ReadResidentJournal(filepath.Join(root, spec.ID, residentTranscriptName))
365	if err != nil {
366		t.Fatal(err)
367	}
368	if len(records) != 3 || records[1].Type != residentRecordAssistant || len(records[1].Message) == 0 || records[2].Type != residentRecordToolCall || records[2].ToolName != "bash" {
369		t.Fatalf("records = %#v", records)
370	}
371}
372
373func TestReconcileResidentJournalInterruptsStartedTurnWithoutReplay(t *testing.T) {
374	root := t.TempDir()
375	journal, err := OpenResidentJournal(root, "child-2")
376	if err != nil {
377		t.Fatal(err)
378	}
379	spec := ResidentChildSpec{ID: "child-2", SessionID: "child-session-2", InitialTurnID: "initial-turn", Provider: "openai", Model: "gpt-5"}
380	if err := journal.Accept(spec, "do work"); err != nil {
381		t.Fatal(err)
382	}
383	if err := journal.RecordTurnStarted(spec, spec.InitialTurnID); err != nil {
384		t.Fatal(err)
385	}
386	if err := journal.Close(); err != nil {
387		t.Fatal(err)
388	}
389
390	metadata, err := ReconcileResidentJournal(filepath.Join(root, "child-2"))
391	if err != nil {
392		t.Fatalf("ReconcileResidentJournal: %v", err)
393	}
394	if metadata.State != ResidentInterrupted {
395		t.Fatalf("state = %q, want interrupted", metadata.State)
396	}
397	records, err := ReadResidentJournal(filepath.Join(root, "child-2", residentTranscriptName))
398	if err != nil {
399		t.Fatal(err)
400	}
401	if got := records[len(records)-1].Type; got != residentRecordInterrupted {
402		t.Fatalf("last record = %q, want interruption", got)
403	}
404	if _, err := ReconcileResidentJournal(filepath.Join(root, "child-2")); err != nil {
405		t.Fatalf("repeat reconciliation: %v", err)
406	}
407	repeated, err := ReadResidentJournal(filepath.Join(root, "child-2", residentTranscriptName))
408	if err != nil {
409		t.Fatal(err)
410	}
411	if len(repeated) != len(records) {
412		t.Fatalf("repeat reconciliation appended records: %d -> %d", len(records), len(repeated))
413	}
414}
415
416func TestReconcileResidentJournalInterruptsQueuedInitialTurnOnce(t *testing.T) {
417	root := t.TempDir()
418	journal, err := OpenResidentJournal(root, "queued-child")
419	if err != nil {
420		t.Fatal(err)
421	}
422	spec := ResidentChildSpec{ID: "queued-child", SessionID: "queued-session", InitialTurnID: "initial-turn", Provider: "openai", Model: "gpt-5"}
423	if err := journal.Accept(spec, "do work"); err != nil {
424		t.Fatal(err)
425	}
426	if err := journal.Close(); err != nil {
427		t.Fatal(err)
428	}
429	for attempt := 0; attempt < 2; attempt++ {
430		metadata, err := ReconcileResidentJournal(filepath.Join(root, spec.ID))
431		if err != nil || metadata.State != ResidentInterrupted {
432			t.Fatalf("reconcile %d = (%#v, %v)", attempt, metadata, err)
433		}
434	}
435	records, err := ReadResidentJournal(filepath.Join(root, spec.ID, residentTranscriptName))
436	if err != nil {
437		t.Fatal(err)
438	}
439	if len(records) != 2 || records[1].Type != residentRecordInterrupted || records[1].TurnID != spec.InitialTurnID {
440		t.Fatalf("queued initial records = %#v", records)
441	}
442}
443
444func TestReconcileResidentJournalRepairsOnlyDanglingToolCalls(t *testing.T) {
445	root := t.TempDir()
446	journal, err := OpenResidentJournal(root, "tool-pair")
447	if err != nil {
448		t.Fatal(err)
449	}
450	spec := ResidentChildSpec{ID: "tool-pair", SessionID: "child-session", InitialTurnID: "turn-1", Provider: "openai", Model: "gpt-5"}
451	if err := journal.Accept(spec, "task"); err != nil {
452		t.Fatal(err)
453	}
454	if err := journal.RecordTurnStarted(spec, "turn-1"); err != nil {
455		t.Fatal(err)
456	}
457	if err := journal.RecordAgentEvent(core.EvToolCall{ID: "call-1", Name: "bash", Args: json.RawMessage(`{"command":"pwd"}`)}); err != nil {
458		t.Fatal(err)
459	}
460	if err := journal.Close(); err != nil {
461		t.Fatal(err)
462	}
463	if _, err := ReconcileResidentJournal(filepath.Join(root, spec.ID)); err != nil {
464		t.Fatal(err)
465	}
466	records, err := ReadResidentJournal(filepath.Join(root, spec.ID, residentTranscriptName))
467	if err != nil {
468		t.Fatal(err)
469	}
470	for _, record := range records {
471		if record.Type == residentRecordToolResult && record.ToolID == "call-1" {
472			return
473		}
474	}
475	t.Fatalf("no repaired tool result in %#v", records)
476}
477
478func TestReconcileResidentJournalRepairsTerminalDanglingCallWithoutInterruptingChild(t *testing.T) {
479	root := t.TempDir()
480	journal, err := OpenResidentJournal(root, "terminal-tool-pair")
481	if err != nil {
482		t.Fatal(err)
483	}
484	spec := ResidentChildSpec{ID: "terminal-tool-pair", SessionID: "child-session", InitialTurnID: "turn-1", Provider: "openai", Model: "gpt-5"}
485	if err := journal.Accept(spec, "task"); err != nil {
486		t.Fatal(err)
487	}
488	if err := journal.RecordTurnStarted(spec, "turn-1"); err != nil {
489		t.Fatal(err)
490	}
491	if err := journal.RecordAgentEvent(core.EvToolCall{ID: "call-1", Name: "bash", Args: json.RawMessage(`{"command":"pwd"}`)}); err != nil {
492		t.Fatal(err)
493	}
494	if err := journal.RecordTurnFinished(spec, "turn-1", nil); err != nil {
495		t.Fatal(err)
496	}
497	if err := journal.Close(); err != nil {
498		t.Fatal(err)
499	}
500	metadata, err := ReconcileResidentJournal(filepath.Join(root, spec.ID))
501	if err != nil {
502		t.Fatal(err)
503	}
504	if metadata.State != ResidentIdle {
505		t.Fatalf("state = %q, want idle", metadata.State)
506	}
507	records, err := ReadResidentJournal(filepath.Join(root, spec.ID, residentTranscriptName))
508	if err != nil {
509		t.Fatal(err)
510	}
511	if got := records[len(records)-1]; got.Type != residentRecordToolResult || got.ToolID != "call-1" {
512		t.Fatalf("last record = %#v, want repaired tool result", got)
513	}
514}
515
516func TestReconcileResidentJournalRebuildsMissingTerminalResult(t *testing.T) {
517	root := t.TempDir()
518	journal, err := OpenResidentJournal(root, "terminal-result")
519	if err != nil {
520		t.Fatal(err)
521	}
522	spec := ResidentChildSpec{ID: "terminal-result", SessionID: "child-session", InitialTurnID: "turn-1", Provider: "openai", Model: "gpt-5"}
523	if err := journal.Accept(spec, "task"); err != nil {
524		t.Fatal(err)
525	}
526	if err := journal.RecordTurnStarted(spec, "turn-1"); err != nil {
527		t.Fatal(err)
528	}
529	if err := journal.RecordAgentEvent(core.EvAssistantMessage{Message: provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "recovered final answer"}}}}); err != nil {
530		t.Fatal(err)
531	}
532	if err := journal.RecordTurnFinished(spec, "turn-1", nil); err != nil {
533		t.Fatal(err)
534	}
535	if err := journal.Close(); err != nil {
536		t.Fatal(err)
537	}
538	resultPath := filepath.Join(root, spec.ID, residentResultName)
539	if err := os.WriteFile(resultPath, []byte("not json"), 0o600); err != nil {
540		t.Fatal(err)
541	}
542	if _, err := ReconcileResidentJournal(filepath.Join(root, spec.ID)); err != nil {
543		t.Fatal(err)
544	}
545	result, err := ReadResidentResult(resultPath)
546	if err != nil {
547		t.Fatal(err)
548	}
549	if result.TurnID != "turn-1" || result.State != ResidentIdle || result.Summary != "recovered final answer" {
550		t.Fatalf("result = %#v", result)
551	}
552}
553
554func TestReconcileResidentJournalPreservesTerminalLifecycleTime(t *testing.T) {
555	root := t.TempDir()
556	journal, err := OpenResidentJournal(root, "terminal-time")
557	if err != nil {
558		t.Fatal(err)
559	}
560	spec := ResidentChildSpec{ID: "terminal-time", SessionID: "child-session", InitialTurnID: "turn-1", Provider: "openai", Model: "gpt-5"}
561	if err := journal.Accept(spec, "task"); err != nil {
562		t.Fatal(err)
563	}
564	if err := journal.RecordTurnStarted(spec, "turn-1"); err != nil {
565		t.Fatal(err)
566	}
567	if err := journal.RecordTurnFinished(spec, "turn-1", nil); err != nil {
568		t.Fatal(err)
569	}
570	if err := journal.Close(); err != nil {
571		t.Fatal(err)
572	}
573	records, err := ReadResidentJournal(filepath.Join(root, spec.ID, residentTranscriptName))
574	if err != nil {
575		t.Fatal(err)
576	}
577	want := records[len(records)-1].Time
578	metadata, err := ReconcileResidentJournal(filepath.Join(root, spec.ID))
579	if err != nil {
580		t.Fatal(err)
581	}
582	if !metadata.UpdatedAt.Equal(want) {
583		t.Fatalf("updated_at = %s, want terminal record time %s", metadata.UpdatedAt, want)
584	}
585}
586
587func TestResidentJournalStoresBoundedFinalAssistantSummary(t *testing.T) {
588	root := t.TempDir()
589	journal, err := OpenResidentJournal(root, "final-summary")
590	if err != nil {
591		t.Fatal(err)
592	}
593	defer journal.Close()
594	spec := ResidentChildSpec{ID: "final-summary", SessionID: "child-session", InitialTurnID: "turn-1", Provider: "openai", Model: "gpt-5"}
595	if err := journal.Accept(spec, "task"); err != nil {
596		t.Fatal(err)
597	}
598	if err := journal.RecordTurnStarted(spec, "turn-1"); err != nil {
599		t.Fatal(err)
600	}
601	if err := journal.RecordAgentEvent(core.EvAssistantMessage{Message: provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "the child final answer"}}}}); err != nil {
602		t.Fatal(err)
603	}
604	if err := journal.RecordTurnFinished(spec, "turn-1", nil); err != nil {
605		t.Fatal(err)
606	}
607	result, err := ReadResidentResult(filepath.Join(root, spec.ID, residentResultName))
608	if err != nil {
609		t.Fatal(err)
610	}
611	if result.Summary != "the child final answer" {
612		t.Fatalf("result summary = %q", result.Summary)
613	}
614}
615
616func TestTruncateResidentResultSummaryPreservesUTF8Boundary(t *testing.T) {
617	if residentResultSummaryBytes != 256<<10 {
618		t.Fatalf("summary limit = %d, want %d", residentResultSummaryBytes, 256<<10)
619	}
620	text := strings.Repeat("é", residentResultSummaryBytes)
621	summary := truncateResidentResultSummary(text)
622	if !utf8.ValidString(summary) || !strings.HasSuffix(summary, "…") || len(summary) > residentResultSummaryBytes {
623		t.Fatalf("summary = %q", summary[:min(len(summary), 32)])
624	}
625}
626
627func TestReconcileResidentJournalRepairsLegacyFalseRecovery(t *testing.T) {
628	root := t.TempDir()
629	journal, err := OpenResidentJournal(root, "legacy-race")
630	if err != nil {
631		t.Fatal(err)
632	}
633	spec := ResidentChildSpec{ID: "legacy-race", SessionID: "child-session", InitialTurnID: "turn-1", Provider: "openai", Model: "gpt-5"}
634	if err := journal.Accept(spec, "task"); err != nil {
635		t.Fatal(err)
636	}
637	if err := journal.RecordTurnStarted(spec, spec.InitialTurnID); err != nil {
638		t.Fatal(err)
639	}
640	if err := journal.appendSync(residentRecord{Version: residentJournalVersion, Type: residentRecordToolCall, Time: time.Now().UTC(), ToolID: "call-1", ToolName: "bash", ToolArgs: json.RawMessage(`{"command":"pwd"}`)}); err != nil {
641		t.Fatal(err)
642	}
643	if err := journal.appendSync(residentRecord{Version: residentJournalVersion, Type: residentRecordToolResult, Time: time.Now().UTC(), ToolID: "call-1", ToolResult: json.RawMessage(`{"Content":[{"text":"tool interrupted by resident host restart"}],"IsError":true}`)}); err != nil {
644		t.Fatal(err)
645	}
646	if err := journal.RecordTurnInterrupted(spec, spec.InitialTurnID); err != nil {
647		t.Fatal(err)
648	}
649	if err := journal.appendSync(residentRecord{Version: residentJournalVersion, Type: residentRecordToolResult, Time: time.Now().UTC(), ToolID: "call-1", ToolResult: json.RawMessage(`{"Content":[{"text":"ok"}],"IsError":false}`)}); err != nil {
650		t.Fatal(err)
651	}
652	if err := journal.RecordTurnFinished(spec, spec.InitialTurnID, nil); err != nil {
653		t.Fatal(err)
654	}
655	if err := journal.Close(); err != nil {
656		t.Fatal(err)
657	}
658
659	metadata, err := ReconcileResidentJournal(filepath.Join(root, spec.ID))
660	if err != nil {
661		t.Fatal(err)
662	}
663	if metadata.State != ResidentIdle {
664		t.Fatalf("state = %q, want idle", metadata.State)
665	}
666	records, err := ReadResidentJournal(filepath.Join(root, spec.ID, residentTranscriptName))
667	if err != nil {
668		t.Fatal(err)
669	}
670	for _, record := range records {
671		if record.Type == residentRecordInterrupted || isSyntheticInterruption(record.ToolResult) {
672			t.Fatalf("repaired records retained false recovery: %#v", record)
673		}
674	}
675	backups, err := filepath.Glob(filepath.Join(root, spec.ID, ".transcript-backup-*"))
676	if err != nil {
677		t.Fatal(err)
678	}
679	if len(backups) != 1 {
680		t.Fatalf("backups = %v, want one", backups)
681	}
682}
683
684func TestRepairLegacyFalseRecoveryRefusesAmbiguousBlock(t *testing.T) {
685	synthetic := json.RawMessage(`{"Content":[{"text":"tool interrupted by resident host restart"}],"IsError":true}`)
686	real := json.RawMessage(`{"Content":[{"text":"ok"}],"IsError":false}`)
687	records := []residentRecord{
688		{Type: residentRecordTurnStarted, TurnID: "turn-1"},
689		{Type: residentRecordToolResult, ToolID: "call-1", ToolResult: synthetic},
690		{Type: residentRecordToolResult, ToolID: "call-2", ToolResult: synthetic},
691		{Type: residentRecordInterrupted, TurnID: "turn-1"},
692		{Type: residentRecordToolResult, ToolID: "call-1", ToolResult: real},
693	}
694	if repaired, ok := repairLegacyFalseRecovery(records); ok || repaired != nil {
695		t.Fatalf("repairLegacyFalseRecovery = %#v, %v; want no repair", repaired, ok)
696	}
697}
698
699func TestRepairLegacyFalseRecoveryPreservesOtherTurnInterruption(t *testing.T) {
700	synthetic := json.RawMessage(`{"Content":[{"text":"tool interrupted by resident host restart"}],"IsError":true}`)
701	real := json.RawMessage(`{"Content":[{"text":"ok"}],"IsError":false}`)
702	records := []residentRecord{
703		{Type: residentRecordTurnStarted, TurnID: "turn-1"},
704		{Type: residentRecordToolResult, ToolID: "call-1", ToolResult: synthetic},
705		{Type: residentRecordInterrupted, TurnID: "turn-1"},
706		{Type: residentRecordInterrupted, TurnID: "turn-2"},
707		{Type: residentRecordToolResult, ToolID: "call-1", ToolResult: real},
708	}
709	if repaired, ok := repairLegacyFalseRecovery(records); ok || repaired != nil {
710		t.Fatalf("repairLegacyFalseRecovery = %#v, %v; want no repair", repaired, ok)
711	}
712}
713
714func TestReconcileResidentJournalRejectsConflictingToolPairs(t *testing.T) {
715	for _, tc := range []struct {
716		name    string
717		records []residentRecord
718		want    string
719	}{
720		{
721			name:    "orphan-result",
722			records: []residentRecord{{Version: residentJournalVersion, Type: residentRecordToolResult, ToolID: "missing", ToolResult: json.RawMessage(`{"is_error":true}`)}},
723			want:    "orphan tool result",
724		},
725		{
726			name: "duplicate-result",
727			records: []residentRecord{
728				{Version: residentJournalVersion, Type: residentRecordToolCall, ToolID: "call-1", ToolName: "bash", ToolArgs: json.RawMessage(`{"command":"pwd"}`)},
729				{Version: residentJournalVersion, Type: residentRecordToolResult, ToolID: "call-1", ToolResult: json.RawMessage(`{"is_error":false}`)},
730				{Version: residentJournalVersion, Type: residentRecordToolResult, ToolID: "call-1", ToolResult: json.RawMessage(`{"is_error":false}`)},
731			},
732			want: "duplicate tool result",
733		},
734	} {
735		t.Run(tc.name, func(t *testing.T) {
736			root := t.TempDir()
737			journal, err := OpenResidentJournal(root, "corrupt")
738			if err != nil {
739				t.Fatal(err)
740			}
741			spec := ResidentChildSpec{ID: "corrupt", SessionID: "child-session", Provider: "openai", Model: "gpt-5"}
742			if err := journal.Accept(spec, "task"); err != nil {
743				t.Fatal(err)
744			}
745			for _, record := range tc.records {
746				record.Time = time.Now().UTC()
747				if err := journal.appendSync(record); err != nil {
748					t.Fatal(err)
749				}
750			}
751			if err := journal.Close(); err != nil {
752				t.Fatal(err)
753			}
754			if _, err := ReconcileResidentJournal(filepath.Join(root, spec.ID)); err == nil || !strings.Contains(err.Error(), tc.want) {
755				t.Fatalf("ReconcileResidentJournal error = %v, want %q", err, tc.want)
756			}
757		})
758	}
759}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close