1package core
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "testing"
8
9 "github.com/patriceckhart/zot/packages/provider"
10)
11
12type outcomeTool struct {
13 result ToolResult
14 err error
15 panicValue bool
16 called bool
17 args json.RawMessage
18}
19
20func (t *outcomeTool) Name() string { return "outcome" }
21func (t *outcomeTool) Description() string { return "test" }
22func (t *outcomeTool) Schema() json.RawMessage { return json.RawMessage(`{}`) }
23func (t *outcomeTool) Execute(_ context.Context, args json.RawMessage, _ func(string)) (ToolResult, error) {
24 t.called = true
25 t.args = append(json.RawMessage(nil), args...)
26 if t.panicValue {
27 panic("test panic")
28 }
29 return t.result, t.err
30}
31
32func TestToolResultLifecycle(t *testing.T) {
33 for _, tc := range []struct {
34 name, status string
35 err error
36 isError, panicValue, block, cancelBefore bool
37 resultStatus string
38 }{
39 {name: "success", status: "completed"},
40 {name: "error", status: "failed", err: errors.New("failure")},
41 {name: "error result", status: "failed", isError: true},
42 {name: "panic", status: "failed", panicValue: true},
43 {name: "guard", status: "blocked", block: true},
44 {name: "tool policy", status: "blocked", err: &ToolPolicyError{Err: errors.New("denied")}},
45 {name: "cancel", status: "cancelled", err: context.Canceled},
46 {name: "deadline", status: "timed_out", err: context.DeadlineExceeded},
47 {name: "cancel before execution", status: "cancelled", cancelBefore: true},
48 {name: "tool timeout result", status: "timed_out", isError: true, resultStatus: "timed_out"},
49 } {
50 t.Run(tc.name, func(t *testing.T) {
51 tool := &outcomeTool{err: tc.err, result: ToolResult{IsError: tc.isError, Status: tc.resultStatus}, panicValue: tc.panicValue}
52 a := NewAgent(nil, "test", "", NewRegistry(tool))
53 rewritten := json.RawMessage(`{"effective":true}`)
54 a.BeforeToolExecute = func(provider.ToolCallBlock) (bool, string, json.RawMessage) { return !tc.block, "blocked", rewritten }
55 ctx, cancel := context.WithCancel(context.Background())
56 defer cancel()
57 if tc.cancelBefore {
58 cancel()
59 }
60 var events []EvToolResult
61 msg, hadError := a.executeTools(ctx, provider.Message{Content: []provider.Content{provider.ToolCallBlock{ID: "call", Name: tool.Name(), Arguments: json.RawMessage(`{}`)}}}, func(e AgentEvent) {
62 if result, ok := e.(EvToolResult); ok {
63 events = append(events, result)
64 }
65 })
66 if len(events) != 1 {
67 t.Fatalf("got %d results", len(events))
68 }
69 e := events[0]
70 if e.ID != "call" || e.Name != tool.Name() || e.Status != tc.status || e.Executed != tool.called {
71 t.Fatalf("wrong event: %+v", e)
72 }
73 if !tc.cancelBefore && string(e.Args) != string(rewritten) {
74 t.Fatalf("wrong effective args: %s", e.Args)
75 }
76 if tool.called && string(e.Args) != string(tool.args) {
77 t.Fatal("reported args differ from execution")
78 }
79 if tool.called == (tc.block || tc.cancelBefore) {
80 t.Fatal("unexpected execution")
81 }
82 if hadError != (tc.status != "completed") {
83 t.Fatalf("hadError = %v", hadError)
84 }
85 if len(msg.Content) != 1 || msg.Content[0].(provider.ToolResultBlock).CallID != e.ID {
86 t.Fatal("transcript result pairing lost")
87 }
88 })
89 }
90}
91
92func TestUnknownToolHasTerminalEvent(t *testing.T) {
93 a := NewAgent(nil, "test", "", nil)
94 var got EvToolResult
95 a.runOneTool(context.Background(), provider.ToolCallBlock{ID: "missing", Name: "missing"}, func(e AgentEvent) { got = e.(EvToolResult) })
96 if got.Status != "failed" || got.Executed || !got.Result.IsError {
97 t.Fatalf("wrong result: %+v", got)
98 }
99}
100
101func TestPromptSubmitBeforeQueueConsumption(t *testing.T) {
102 a := NewAgent(nil, "test", "", nil)
103 var events []AgentEvent
104 a.OnEvent = func(e AgentEvent) { events = append(events, e) }
105 if a.QueueMessage(" ") {
106 t.Fatal("empty queue accepted")
107 }
108 if !a.QueueMessage(" queued ") {
109 t.Fatal("queue rejected")
110 }
111 if len(events) != 1 {
112 t.Fatalf("events = %v", events)
113 }
114 got := events[0].(EvPromptSubmit)
115 if got.Text != "queued" || !got.Queued {
116 t.Fatalf("event = %+v", got)
117 }
118 a.appendQueuedAsUser(a.drainQueuedMessages(), a.OnEvent)
119 count := 0
120 for _, e := range events {
121 if _, ok := e.(EvPromptSubmit); ok {
122 count++
123 }
124 }
125 if count != 1 {
126 t.Fatalf("duplicate submission: %d", count)
127 }
128}
129
130func TestCompactionFailureHasPairedEvents(t *testing.T) {
131 a := NewAgent(nil, "test", "", nil)
132 var events []EvCompact
133 a.OnEvent = func(e AgentEvent) { events = append(events, e.(EvCompact)) }
134 if _, err := a.Compact(context.Background(), 0, nil); err == nil {
135 t.Fatal("empty compaction succeeded")
136 }
137 if len(events) != 2 {
138 t.Fatalf("events = %v", events)
139 }
140 if events[0].Phase != "pre" || events[1].Phase != "post" || events[0].ID == "" || events[0].ID != events[1].ID || events[1].Status != "failed" || events[1].Err == nil {
141 t.Fatalf("events = %+v", events)
142 }
143}