v0.3.84 97ae0a2

interactive_tool_test.go RawHistoryFolder

1package ext
2
3import (
4	"context"
5	"encoding/json"
6	"io"
7	"slices"
8	"testing"
9	"time"
10
11	"github.com/patriceckhart/zot/packages/agent/extproto"
12)
13
14func TestInteractiveToolLifecycle(t *testing.T) {
15	for _, end := range []string{"cancel", "shutdown", "eof", "complete"} {
16		t.Run(end, func(t *testing.T) {
17			h := newHarness("interactive")
18			started := make(chan context.Context, 2)
19			finished := make(chan struct{}, 2)
20			answer := make(chan struct{})
21			h.ext.InteractiveTool("ask", "Ask the user", json.RawMessage(`{}`), func(ctx context.Context, _ json.RawMessage) ToolResult {
22				started <- ctx
23				defer func() { finished <- struct{}{} }()
24				select {
25				case <-ctx.Done():
26					return TextErrorResult("cancelled")
27				case <-answer:
28					return TextResult("answer")
29				}
30			})
31			done := make(chan error, 1)
32			go func() { done <- h.ext.Run() }()
33			t.Cleanup(func() {
34				h.hostW.Close()
35				select {
36				case err := <-done:
37					if err != nil {
38						t.Error(err)
39					}
40				case <-time.After(time.Second):
41					t.Error("SDK did not exit")
42				}
43				h.ext.out.(io.Closer).Close()
44				h.ext.in.(io.Closer).Close()
45			})
46			var hello extproto.HelloFromExt
47			if err := json.Unmarshal(h.next(t).raw, &hello); err != nil {
48				t.Fatal(err)
49			}
50			if hello.Type != "hello" || !slices.Contains(hello.Capabilities, "tool_cancel") {
51				t.Fatalf("SDK did not advertise cancellation support: %+v", hello)
52			}
53			h.sendToExt(t, extproto.HelloAckFromHost{Type: "hello_ack"})
54			var registration extproto.RegisterToolFromExt
55			json.Unmarshal(h.drainUntil(t, "register_tool").raw, &registration)
56			if !registration.Interactive || registration.Name != "ask" {
57				t.Fatalf("registration: %+v", registration)
58			}
59			h.drainUntil(t, "ready")
60			h.sendToExt(t, extproto.ToolCallFromHost{Type: "tool_call", ID: "one", Name: "ask", Args: json.RawMessage(`{}`)})
61			// Send cancellation without waiting for the goroutine to start.
62			if end == "cancel" {
63				h.sendToExt(t, extproto.ToolCancelFromHost{Type: "tool_cancel", ID: "one"})
64			}
65			var ctx context.Context
66			select {
67			case ctx = <-started:
68			case <-time.After(time.Second):
69				t.Fatal("handler did not start")
70			}
71			switch end {
72			case "shutdown":
73				h.sendToExt(t, extproto.ShutdownFromHost{Type: "shutdown"})
74				h.drainUntil(t, "shutdown_ack")
75			case "eof":
76				h.hostW.Close()
77			case "complete":
78				close(answer)
79				var result extproto.ToolResultFromExt
80				json.Unmarshal(h.drainUntil(t, "tool_result").raw, &result)
81				if result.ID != "one" || len(result.Content) != 1 || result.Content[0].Text != "answer" {
82					t.Fatalf("result: %+v", result)
83				}
84			}
85			select {
86			case <-finished:
87			case <-time.After(time.Second):
88				t.Fatal("handler did not finish")
89			}
90			if end != "complete" && ctx.Err() != context.Canceled {
91				t.Fatal("handler context was not cancelled")
92			}
93			if end == "cancel" {
94				// Unknown and duplicate cancellation must not affect another call.
95				h.sendToExt(t, extproto.ToolCancelFromHost{Type: "tool_cancel", ID: "one"})
96				h.sendToExt(t, extproto.ToolCancelFromHost{Type: "tool_cancel", ID: "unknown"})
97				h.sendToExt(t, extproto.ToolCallFromHost{Type: "tool_call", ID: "two", Name: "ask", Args: json.RawMessage(`{}`)})
98				select {
99				case ctx = <-started:
100				case <-time.After(time.Second):
101					t.Fatal("second handler did not start")
102				}
103				if ctx.Err() != nil {
104					t.Fatal("second call was cancelled")
105				}
106				close(answer)
107				var result extproto.ToolResultFromExt
108				json.Unmarshal(h.drainUntil(t, "tool_result").raw, &result)
109				if result.ID != "two" {
110					t.Fatalf("unexpected result: %+v", result)
111				}
112			}
113		})
114	}
115}

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