v0.3.90 6f6753c

lifecycle_test.go RawHistoryFolder

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}

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 476

↑↓ moveenter openesc close