1package agent
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/zut/packages/agent/subagents"
8 "github.com/bnema/zut/packages/agent/tools"
9 "github.com/bnema/zut/packages/core"
10)
11
12func TestExpandWebCapabilityTools(t *testing.T) {
13 catalogue := append([]string{"read"}, tools.WebCapabilityNames...)
14 got := expandWebCapabilityTools([]string{"read", "web_search"}, catalogue, func(string) bool { return true })
15 for _, name := range append([]string{"read"}, tools.WebCapabilityNames...) {
16 found := false
17 for _, candidate := range got {
18 if candidate == name {
19 found = true
20 break
21 }
22 }
23 if !found {
24 t.Fatalf("expanded child tools = %v, missing %s", got, name)
25 }
26 }
27}
28
29func TestExpandWebCapabilityToolsPreservesPolicyAndNonWebSelections(t *testing.T) {
30 catalogue := append([]string{"read"}, tools.WebCapabilityNames...)
31 partial := expandWebCapabilityTools([]string{"web_open"}, catalogue, func(string) bool { return true })
32 for _, name := range tools.WebCapabilityNames {
33 found := false
34 for _, candidate := range partial {
35 if candidate == name {
36 found = true
37 break
38 }
39 }
40 if !found {
41 t.Fatalf("partial web selection did not expand: %v", partial)
42 }
43 }
44 denied := expandWebCapabilityTools([]string{"read", "web_search"}, catalogue, func(name string) bool { return name == "read" })
45 if len(denied) != 1 || denied[0] != "read" {
46 t.Fatalf("denied web capability = %v, want [read]", denied)
47 }
48 nonWeb := expandWebCapabilityTools([]string{"read"}, catalogue, func(string) bool { return true })
49 if len(nonWeb) != 1 || nonWeb[0] != "read" {
50 t.Fatalf("non-web selection changed to %v", nonWeb)
51 }
52}
53
54func TestResidentChildSpecSnapshotsCurrentProviderTransportSettings(t *testing.T) {
55 runtime := newSubagentRuntime(subagentRuntimeConfig{
56 Args: Args{}, Root: t.TempDir(), RepoRoot: t.TempDir(),
57 Provider: "openai", Model: "gpt-5.6-sol", BaseURL: "https://old.example/v1", InsecureTLS: false, ContextWindow: 500_000,
58 })
59 t.Cleanup(func() { _ = runtime.Close(context.Background()) })
60 runtime.SetProviderSettings("https://current.example/v1", true)
61 runtime.SetModel("gpt-current")
62 spec, err := runtime.buildResidentChildSpec(context.Background(), tools.ResidentSpawnRequest{Task: "review"}, core.Registry{"read": nil})
63 if err != nil {
64 t.Fatal(err)
65 }
66 if spec.BaseURL != "https://current.example/v1" || !spec.InsecureTLS || spec.Model != "gpt-current" {
67 t.Fatalf("resident spec transport/model = %q insecure=%t model=%q", spec.BaseURL, spec.InsecureTLS, spec.Model)
68 }
69}
70
71func TestResidentChildSpecInheritsAndOverridesRolloutBudget(t *testing.T) {
72 ratio := 0.6
73 runtime := newSubagentRuntime(subagentRuntimeConfig{
74 Args: Args{}, Root: t.TempDir(), RepoRoot: t.TempDir(),
75 Provider: "openai", Model: "gpt-5.6-sol", ContextWindow: 500_000,
76 Policy: subagents.SubagentPolicy{BudgetRatio: ratio, BudgetRatioConfigured: true},
77 })
78 t.Cleanup(func() { _ = runtime.Close(context.Background()) })
79
80 spec, err := runtime.buildResidentChildSpec(context.Background(), tools.ResidentSpawnRequest{Task: "review"}, core.Registry{"read": nil})
81 if err != nil {
82 t.Fatal(err)
83 }
84 if spec.BudgetLimit != 300_000 || spec.BudgetRatio != ratio || spec.BudgetSource != "config_ratio" {
85 t.Fatalf("inherited budget = %#v", spec)
86 }
87
88 tokens := int64(42_000)
89 spec, err = runtime.buildResidentChildSpec(context.Background(), tools.ResidentSpawnRequest{
90 Task: "deep review", BudgetTokens: &tokens,
91 }, core.Registry{"read": nil})
92 if err != nil {
93 t.Fatal(err)
94 }
95 if spec.BudgetLimit != tokens || spec.BudgetRatio != 0 || spec.BudgetSource != "spawn_tokens" {
96 t.Fatalf("absolute budget = %#v", spec)
97 }
98}
99
100func TestResolveResidentBudgetPrecedence(t *testing.T) {
101 configRatio, profileRatio, spawnRatio := 0.6, 0.5, 0.4
102 profileTokens, spawnTokens := int64(200_000), int64(150_000)
103 for _, tc := range []struct {
104 name string
105 profile *subagents.Profile
106 spawnRatio *float64
107 spawnTokens *int64
108 wantLimit int64
109 wantRatio float64
110 wantSource string
111 }{
112 {name: "default", wantLimit: 375_000, wantRatio: 0.75, wantSource: "default_ratio"},
113 {name: "config ratio", wantLimit: 300_000, wantRatio: configRatio, wantSource: "config_ratio"},
114 {name: "profile ratio", profile: &subagents.Profile{BudgetRatio: &profileRatio}, wantLimit: 250_000, wantRatio: profileRatio, wantSource: "profile_ratio"},
115 {name: "profile tokens", profile: &subagents.Profile{BudgetTokens: &profileTokens}, wantLimit: profileTokens, wantSource: "profile_tokens"},
116 {name: "spawn ratio over profile tokens", profile: &subagents.Profile{BudgetTokens: &profileTokens}, spawnRatio: &spawnRatio, wantLimit: 200_000, wantRatio: spawnRatio, wantSource: "spawn_ratio"},
117 {name: "spawn tokens over profile ratio", profile: &subagents.Profile{BudgetRatio: &profileRatio}, spawnTokens: &spawnTokens, wantLimit: spawnTokens, wantSource: "spawn_tokens"},
118 } {
119 t.Run(tc.name, func(t *testing.T) {
120 policyRatio := configRatio
121 if tc.name == "default" {
122 policyRatio = 0
123 }
124 limit, ratio, source, err := resolveResidentBudget(500_000, policyRatio, tc.name != "default", tc.profile, tc.spawnRatio, tc.spawnTokens)
125 if err != nil {
126 t.Fatal(err)
127 }
128 if limit != tc.wantLimit || ratio != tc.wantRatio || source != tc.wantSource {
129 t.Fatalf("resolved budget = (%d, %v, %q), want (%d, %v, %q)", limit, ratio, source, tc.wantLimit, tc.wantRatio, tc.wantSource)
130 }
131 })
132 }
133 limit, ratio, source, err := resolveResidentBudget(500_000, 0.75, true, nil, nil, nil)
134 if err != nil || limit != 375_000 || ratio != 0.75 || source != "config_ratio" {
135 t.Fatalf("explicit default ratio = (%d, %v, %q, %v)", limit, ratio, source, err)
136 }
137}
138
139func TestResidentChildSpecDoesNotCarryTransportAcrossProviderOverride(t *testing.T) {
140 runtime := newSubagentRuntime(subagentRuntimeConfig{
141 Args: Args{}, Root: t.TempDir(), RepoRoot: t.TempDir(),
142 Provider: "openai", Model: "gpt-5.6-sol", BaseURL: "https://parent.example/v1", InsecureTLS: true, ContextWindow: 500_000,
143 })
144 t.Cleanup(func() { _ = runtime.Close(context.Background()) })
145 spec, err := runtime.buildResidentChildSpec(context.Background(), tools.ResidentSpawnRequest{
146 Task: "review", Provider: "anthropic", Model: "claude-sonnet-4-5",
147 }, core.Registry{"read": nil})
148 if err != nil {
149 t.Fatal(err)
150 }
151 if spec.BaseURL != "" || spec.InsecureTLS {
152 t.Fatalf("cross-provider spec inherited transport: %#v", spec)
153 }
154}