zot/packages/agent/extensions/tool_test.go
patriceckhart fa7d8d8be5 refactor: split source into packages/{provider,core,tui,agent}
Single Go module, four top-level packages under packages/. Import
paths become github.com/patriceckhart/zot/packages/<name>; downstream
consumers can depend on individual packages without pulling the rest.

Layout:
  packages/provider/     LLM clients + catalog
  packages/provider/auth/ credential store + OAuth + login server
  packages/core/         agent loop, sessions, cost
  packages/tui/          terminal toolkit + chat view
  packages/agent/        CLI wiring, system prompt
    extensions/ extproto/ modes/ tools/ skills/ swarm/
    sdk/  (was pkg/zotcore, package renamed zotcore -> sdk)
    ext/  (was pkg/zotext, package renamed zotext -> ext)

internal/ and pkg/ removed. The internal/assets logo moved into
packages/provider/auth/assets.

Public Go SDK identifiers renamed:
  pkg/zotcore (package zotcore) -> packages/agent/sdk (package sdk)
  pkg/zotext  (package zotext)  -> packages/agent/ext (package ext)

This breaks Go-based extensions and embedders; the JSON wire protocol
for extensions and RPC is unchanged, so non-Go extensions, already-
built extension binaries, and zot rpc consumers are unaffected.

Docs, examples, and the built-in write-zot-extension skill updated
for the new paths and identifiers. Shadow-bug fixes in code samples
(ext := ext.New -> e := ext.New).
2026-05-27 09:07:15 +02:00

82 lines
2.5 KiB
Go

package extensions
import (
"context"
"encoding/json"
"os"
"path/filepath"
"runtime"
"testing"
"time"
)
func writeMockToolExtension(t *testing.T, root string) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("mock uses /bin/sh; skip on windows")
}
dir := filepath.Join(root, "tool-mock")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
// Shell-script extension: hello, register one tool, ready, then
// loop on stdin echoing back tool_call args as tool_result text.
script := `#!/bin/sh
printf '%s\n' '{"type":"hello","name":"tool-mock","version":"0.1","capabilities":["tools"]}'
printf '%s\n' '{"type":"register_tool","name":"echo","description":"echo back the args","schema":{"type":"object","properties":{"msg":{"type":"string"}}}}'
printf '%s\n' '{"type":"ready"}'
while IFS= read -r line; do
case "$line" in
*'"type":"tool_call"'*)
id=$(printf '%s' "$line" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
printf '%s\n' "{\"type\":\"tool_result\",\"id\":\"$id\",\"content\":[{\"type\":\"text\",\"text\":\"echoed\"}]}"
;;
*'"type":"shutdown"'*)
printf '%s\n' '{"type":"shutdown_ack"}'
exit 0
;;
esac
done
`
if err := os.WriteFile(filepath.Join(dir, "run.sh"), []byte(script), 0o755); err != nil {
t.Fatal(err)
}
mfb, _ := json.Marshal(map[string]any{"name": "tool-mock", "exec": "./run.sh"})
if err := os.WriteFile(filepath.Join(dir, "extension.json"), mfb, 0o644); err != nil {
t.Fatal(err)
}
}
func TestManagerToolRegistrationAndInvoke(t *testing.T) {
tmp := t.TempDir()
writeMockToolExtension(t, filepath.Join(tmp, "extensions"))
mgr := New(tmp, "", "0.0.0", "anthropic", "opus", &stubHooks{})
if errs := mgr.Discover(context.Background()); len(errs) > 0 {
t.Fatalf("discover errs: %v", errs)
}
defer mgr.Stop(2 * time.Second)
// Wait for ready (the script sends it right after register_tool).
mgr.WaitForReady(time.Second)
tools := mgr.Tools()
if len(tools) != 1 || tools[0].Name != "echo" {
t.Fatalf("expected one tool 'echo', got %#v", tools)
}
if !mgr.HasTool("echo") {
t.Fatal("HasTool(\"echo\") = false")
}
resp, err := mgr.InvokeTool(context.Background(), "echo", json.RawMessage(`{"msg":"hi"}`), 2*time.Second)
if err != nil {
t.Fatalf("invoke: %v", err)
}
if resp.IsError {
t.Errorf("expected success, got is_error=true")
}
if len(resp.Content) != 1 || resp.Content[0].Type != "text" || resp.Content[0].Text != "echoed" {
t.Errorf("unexpected content: %#v", resp.Content)
}
}