1package subagents
2
3import (
4 "encoding/json"
5 "errors"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/bnema/zut/packages/core"
11 "github.com/bnema/zut/packages/provider"
12)
13
14func TestResidentHandoffStaysEmptyForNewResults(t *testing.T) {
15 journal, err := OpenResidentJournal(t.TempDir(), "handoff-secrets")
16 if err != nil {
17 t.Fatal(err)
18 }
19 t.Cleanup(func() { _ = journal.Close() })
20 spec := ResidentChildSpec{ID: "handoff-secrets", SessionID: "session", InitialTurnID: "turn", Provider: "openai", Model: "test"}
21 if err := journal.Accept(spec, "check service"); err != nil {
22 t.Fatal(err)
23 }
24 if err := journal.RecordTurnStarted(spec, spec.InitialTurnID); err != nil {
25 t.Fatal(err)
26 }
27 args := json.RawMessage(`{"command":"curl -H 'Authorization: Bearer synthetic-argument-secret' localhost"}`)
28 for _, event := range []core.AgentEvent{
29 core.EvAssistantMessage{Message: provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "Service verification is incomplete."}, provider.ToolCallBlock{ID: "call", Name: "bash", Arguments: args}}}},
30 core.EvToolCall{ID: "call", Name: "bash", Args: args},
31 core.EvToolResult{ID: "call", Result: core.ToolResult{IsError: true, Content: []provider.Content{provider.TextBlock{Text: "response token=synthetic-output-secret"}}}},
32 core.EvAssistantMessage{Message: provider.Message{Role: provider.RoleAssistant, Content: []provider.Content{provider.TextBlock{Text: "Verification failed: service down."}}}},
33 } {
34 if err := journal.RecordAgentEvent(event); err != nil {
35 t.Fatal(err)
36 }
37 }
38 turnErr := errors.New("turn failed")
39 if err := journal.RecordTurnFinished(spec, spec.InitialTurnID, turnErr); err != nil {
40 t.Fatal(err)
41 }
42 result, err := journal.Result()
43 if err != nil {
44 t.Fatal(err)
45 }
46 // Handoff is deprecated: new results stay empty, so no raw tool
47 // payload can leak through the terminal projection. The completion
48 // update carries the production summary instead. Archived v2
49 // handoffs remain readable.
50 if result.Handoff != "" {
51 t.Fatalf("new result carries handoff %q, want empty", result.Handoff)
52 }
53 if result.Summary != "Verification failed: service down." {
54 t.Fatalf("new result summary = %q", result.Summary)
55 }
56 update := FormatCompletionUpdate([]Completion{(ResidentCompletion{ChildID: spec.ID, TurnID: spec.InitialTurnID, Err: turnErr, Summary: result.Summary}).Completion()}, "")
57 if !strings.Contains(update, "Verification failed: service down.") {
58 t.Fatalf("completion update missing summary: %q", update)
59 }
60 for _, secret := range []string{"synthetic-argument-secret", "synthetic-output-secret", "Authorization", "curl"} {
61 if strings.Contains(update, secret) {
62 t.Fatalf("completion update disclosed raw tool payload %q", secret)
63 }
64 }
65 items, err := ReadResidentHistory(journal.Dir(), 16)
66 if err != nil {
67 t.Fatal(err)
68 }
69 if len(items) == 0 || items[len(items)-1].Type != residentRecordAssistant {
70 t.Fatalf("history tail = %#v, want latest assistant message", items)
71 }
72 // The bounded history page starts at the latest prompt; the full
73 // secrets remain in the authoritative transcript records.
74 records, err := ReadResidentJournal(filepath.Join(journal.Dir(), residentTranscriptName))
75 if err != nil {
76 t.Fatal(err)
77 }
78 raw, err := json.Marshal(records)
79 if err != nil || !strings.Contains(string(raw), "synthetic-argument-secret") || !strings.Contains(string(raw), "synthetic-output-secret") {
80 t.Fatal("empty handoff must not destroy durable recovery history")
81 }
82}