1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/patriceckhart/zot/packages/agent/swarm"
11 "github.com/patriceckhart/zot/packages/provider"
12)
13
14type noopSwarmRunner struct{}
15
16func (noopSwarmRunner) Run(context.Context, swarm.Sink) error { return nil }
17
18func newTestSwarm(t *testing.T) *swarm.Swarm {
19 t.Helper()
20 root := t.TempDir()
21 return swarm.New(swarm.Config{
22 Root: filepath.Join(root, "swarm"),
23 RepoRoot: root,
24 NewRunner: func(*swarm.Agent) swarm.Runner {
25 return noopSwarmRunner{}
26 },
27 })
28}
29
30func TestSwarmSpawnInheritsHostModelAndProviderWhenOmitted(t *testing.T) {
31 tool := &SwarmSpawnTool{
32 Swarm: newTestSwarm(t),
33 Enabled: func() bool { return true },
34 DefaultModel: func() string { return "gpt-5" },
35 DefaultProvider: func() string { return "openai-codex" },
36 }
37
38 res, err := tool.Execute(context.Background(), json.RawMessage(`{"task":"research docs"}`), nil)
39 if err != nil {
40 t.Fatal(err)
41 }
42 if res.IsError {
43 t.Fatalf("unexpected tool error: %s", textResult(res.Content))
44 }
45 details, ok := res.Details.(map[string]any)
46 if !ok {
47 t.Fatalf("details type = %T, want map[string]any", res.Details)
48 }
49 if got := details["model"]; got != "gpt-5" {
50 t.Fatalf("model detail = %v, want gpt-5", got)
51 }
52 if got := details["provider"]; got != "openai-codex" {
53 t.Fatalf("provider detail = %v, want openai-codex", got)
54 }
55 text := textResult(res.Content)
56 if !strings.Contains(text, "model: gpt-5") || !strings.Contains(text, "provider: openai-codex") {
57 t.Fatalf("result text missing inherited model/provider:\n%s", text)
58 }
59
60 agents := tool.Swarm.List()
61 if len(agents) != 1 {
62 t.Fatalf("spawned agents = %d, want 1", len(agents))
63 }
64 if agents[0].Model != "gpt-5" || agents[0].Provider != "openai-codex" {
65 t.Fatalf("agent model/provider = %q/%q, want gpt-5/openai-codex", agents[0].Model, agents[0].Provider)
66 }
67}
68
69func TestSwarmSpawnRejectsPartialModelProviderOverride(t *testing.T) {
70 tool := &SwarmSpawnTool{
71 Swarm: newTestSwarm(t),
72 Enabled: func() bool { return true },
73 DefaultModel: func() string { return "gpt-5" },
74 DefaultProvider: func() string { return "openai-codex" },
75 }
76
77 res, err := tool.Execute(context.Background(), json.RawMessage(`{"task":"research docs","provider":"openai"}`), nil)
78 if err != nil {
79 t.Fatal(err)
80 }
81 if !res.IsError {
82 t.Fatalf("expected partial override to fail")
83 }
84 if got := textResult(res.Content); !strings.Contains(got, "omit both model/provider") {
85 t.Fatalf("error text = %q", got)
86 }
87 if got := len(tool.Swarm.List()); got != 0 {
88 t.Fatalf("spawned agents = %d, want 0", got)
89 }
90}
91
92func textResult(content []provider.Content) string {
93 if len(content) == 0 {
94 return ""
95 }
96 if tb, ok := content[0].(provider.TextBlock); ok {
97 return tb.Text
98 }
99 return ""
100}