1package core
2
3import (
4 "context"
5 "strings"
6 "testing"
7
8 "github.com/bnema/zut/packages/provider"
9)
10
11type compactLifecycleClient struct {
12 hadLifecycle bool
13 req provider.Request
14 summary string
15}
16
17func (c *compactLifecycleClient) Name() string { return "compact-lifecycle" }
18
19func (c *compactLifecycleClient) Stream(_ context.Context, req provider.Request) (<-chan provider.Event, error) {
20 c.req = req
21 c.hadLifecycle = req.Lifecycle != nil
22 if req.Lifecycle != nil {
23 req.Lifecycle.RequestAttempt(1, 1)
24 }
25 summary := c.summary
26 if summary == "" {
27 summary = "summary"
28 }
29 out := make(chan provider.Event, 2)
30 out <- provider.EventTextDelta{Delta: summary}
31 out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{
32 Role: provider.RoleAssistant,
33 Content: []provider.Content{provider.TextBlock{Text: summary}},
34 }}
35 close(out)
36 return out, nil
37}
38
39func TestCompactWithEventsEmitsRequestLifecycle(t *testing.T) {
40 client := &compactLifecycleClient{}
41 agent := NewAgent(client, "compact-model", "system", Registry{})
42 agent.SetMessages([]provider.Message{{
43 Role: provider.RoleUser,
44 Content: []provider.Content{provider.TextBlock{Text: "source"}},
45 }})
46
47 var events []AgentEvent
48 summary, err := agent.CompactWithEvents(context.Background(), 0, func(event AgentEvent) {
49 events = append(events, event)
50 })
51 if err != nil {
52 t.Fatalf("CompactWithEvents: %v", err)
53 }
54 if summary != "summary" {
55 t.Fatalf("summary = %q, want summary", summary)
56 }
57 if !client.hadLifecycle {
58 t.Fatal("compaction request did not receive a lifecycle observer")
59 }
60
61 if len(events) != 4 {
62 t.Fatalf("events = %#v, want agent request, provider request, assistant start, text delta", events)
63 }
64 agentRequest, ok := events[0].(EvRequestStarted)
65 if !ok || agentRequest.Scope != RetryScopeAgent || agentRequest.Attempt != 1 || agentRequest.MaxAttempts != 1 {
66 t.Fatalf("first event = %#v, want initial agent request", events[0])
67 }
68 providerRequest, ok := events[1].(EvRequestStarted)
69 if !ok || providerRequest.Scope != RetryScopeProvider || providerRequest.Attempt != 1 || providerRequest.MaxAttempts != 1 {
70 t.Fatalf("second event = %#v, want initial provider request", events[1])
71 }
72 if _, ok := events[2].(EvAssistantStart); !ok {
73 t.Fatalf("third event = %#v, want assistant start", events[2])
74 }
75 if delta, ok := events[3].(EvTextDelta); !ok || delta.Delta != "summary" {
76 t.Fatalf("fourth event = %#v, want summary text delta", events[3])
77 }
78}
79
80func TestCompactPromptPreservesActiveInstructions(t *testing.T) {
81 const activeInstruction = "delegate independent work to subagents"
82 client := &compactLifecycleClient{summary: "Active instruction still in force: " + activeInstruction}
83 agent := NewAgent(client, "compact-model", "system", Registry{})
84 agent.SetMessages([]provider.Message{
85 {
86 Role: provider.RoleUser,
87 Content: []provider.Content{provider.TextBlock{Text: "When coding, delegate independent work to subagents."}},
88 },
89 {
90 Role: provider.RoleAssistant,
91 Content: []provider.Content{provider.TextBlock{Text: "I will do that."}},
92 },
93 })
94
95 if _, err := agent.Compact(context.Background(), 0, nil); err != nil {
96 t.Fatalf("Compact: %v", err)
97 }
98 messages := agent.Messages()
99 if len(messages) != 1 {
100 t.Fatalf("compacted messages = %d, want 1", len(messages))
101 }
102 if messages[0].Role != provider.RoleUser {
103 t.Fatalf("compacted message role = %q, want user", messages[0].Role)
104 }
105 if messages[0].Meta["compaction"] != "true" {
106 t.Fatalf("compacted message meta = %#v, want compaction=true", messages[0].Meta)
107 }
108 compacted, ok := messages[0].Content[0].(provider.TextBlock)
109 if !ok {
110 t.Fatalf("compacted message content = %#v, want TextBlock", messages[0].Content[0])
111 }
112 if !strings.Contains(compacted.Text, "## Context Summary (compacted)") || !strings.Contains(compacted.Text, activeInstruction) {
113 t.Fatalf("compacted transcript did not preserve active instruction:\n%s", compacted.Text)
114 }
115 if !strings.Contains(client.req.System, "Preserve active user instructions") {
116 t.Fatalf("compaction system prompt does not preserve active instructions: %q", client.req.System)
117 }
118 if len(client.req.Messages) != 1 {
119 t.Fatalf("compaction request messages = %d, want 1", len(client.req.Messages))
120 }
121 text, ok := client.req.Messages[0].Content[0].(provider.TextBlock)
122 if !ok {
123 t.Fatalf("compaction request content = %#v, want TextBlock", client.req.Messages[0].Content[0])
124 }
125 for _, want := range []string{"## Active Instructions & Preferences", "delegation/subagent", activeInstruction} {
126 if !strings.Contains(text.Text, want) {
127 t.Fatalf("compaction prompt missing %q:\n%s", want, text.Text)
128 }
129 }
130}