1package extensions
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "runtime"
9 "testing"
10 "time"
11)
12
13func writeMockToolExtension(t *testing.T, root string) {
14 t.Helper()
15 if runtime.GOOS == "windows" {
16 t.Skip("mock uses /bin/sh; skip on windows")
17 }
18 dir := filepath.Join(root, "tool-mock")
19 if err := os.MkdirAll(dir, 0o755); err != nil {
20 t.Fatal(err)
21 }
22
23 // Shell-script extension: hello, register one tool, ready, then
24 // loop on stdin echoing back tool_call args as tool_result text.
25 script := `#!/bin/sh
26printf '%s\n' '{"type":"hello","name":"tool-mock","version":"0.1","capabilities":["tools"]}'
27printf '%s\n' '{"type":"register_tool","name":"echo","description":"echo back the args","schema":{"type":"object","properties":{"msg":{"type":"string"}}}}'
28printf '%s\n' '{"type":"ready"}'
29while IFS= read -r line; do
30 case "$line" in
31 *'"type":"tool_call"'*)
32 id=$(printf '%s' "$line" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
33 printf '%s\n' "{\"type\":\"tool_result\",\"id\":\"$id\",\"content\":[{\"type\":\"text\",\"text\":\"echoed\"}]}"
34 ;;
35 *'"type":"shutdown"'*)
36 printf '%s\n' '{"type":"shutdown_ack"}'
37 exit 0
38 ;;
39 esac
40done
41`
42 if err := os.WriteFile(filepath.Join(dir, "run.sh"), []byte(script), 0o755); err != nil {
43 t.Fatal(err)
44 }
45 mfb, _ := json.Marshal(map[string]any{"name": "tool-mock", "exec": "./run.sh"})
46 if err := os.WriteFile(filepath.Join(dir, "extension.json"), mfb, 0o644); err != nil {
47 t.Fatal(err)
48 }
49}
50
51func TestManagerToolRegistrationAndInvoke(t *testing.T) {
52 tmp := t.TempDir()
53 writeMockToolExtension(t, filepath.Join(tmp, "extensions"))
54
55 mgr := New(tmp, "", "0.0.0", "anthropic", "opus", &stubHooks{})
56 if errs := mgr.Discover(context.Background()); len(errs) > 0 {
57 t.Fatalf("discover errs: %v", errs)
58 }
59 defer mgr.Stop(2 * time.Second)
60
61 // Wait for ready (the script sends it right after register_tool).
62 mgr.WaitForReady(time.Second)
63
64 tools := mgr.Tools()
65 if len(tools) != 1 || tools[0].Name != "echo" {
66 t.Fatalf("expected one tool 'echo', got %#v", tools)
67 }
68 if !mgr.HasTool("echo") {
69 t.Fatal("HasTool(\"echo\") = false")
70 }
71
72 resp, err := mgr.InvokeTool(context.Background(), "echo", json.RawMessage(`{"msg":"hi"}`), 2*time.Second)
73 if err != nil {
74 t.Fatalf("invoke: %v", err)
75 }
76 if resp.IsError {
77 t.Errorf("expected success, got is_error=true")
78 }
79 if len(resp.Content) != 1 || resp.Content[0].Type != "text" || resp.Content[0].Text != "echoed" {
80 t.Errorf("unexpected content: %#v", resp.Content)
81 }
82}