1package agent
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestConfigPonytailModeDefaultsToEnabled(t *testing.T) {
11 cfg := Config{}
12 if !cfg.PonytailModeEnabled() {
13 t.Fatal("Ponytail mode should default to enabled")
14 }
15
16 disabled := false
17 cfg.PonytailEnabled = &disabled
18 if cfg.PonytailModeEnabled() {
19 t.Fatal("Ponytail mode should honor an explicit false value")
20 }
21
22 enabled := true
23 cfg.PonytailEnabled = &enabled
24 if !cfg.PonytailModeEnabled() {
25 t.Fatal("Ponytail mode should honor an explicit true value")
26 }
27}
28
29func TestPonytailSystemAddendumIsCompact(t *testing.T) {
30 addendum := PonytailSystemAddendum()
31 if addendum == "" {
32 t.Fatal("Ponytail system addendum is empty")
33 }
34 if len(addendum) > 2000 {
35 t.Fatalf("Ponytail system addendum is %d bytes, want at most 2000", len(addendum))
36 }
37 for _, want := range []string{"YAGNI", "standard library", "validation", "ordinary conversation"} {
38 if !strings.Contains(addendum, want) {
39 t.Fatalf("Ponytail system addendum missing %q:\n%s", want, addendum)
40 }
41 }
42}
43
44func TestResolvePonytailModeDefaultAndExplicitDisable(t *testing.T) {
45 cases := []struct {
46 name string
47 disabled bool
48 want bool
49 }{
50 {name: "default enabled", want: true},
51 {name: "explicitly disabled", disabled: true, want: false},
52 }
53
54 for _, tc := range cases {
55 t.Run(tc.name, func(t *testing.T) {
56 root := t.TempDir()
57 t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home"))
58 t.Setenv("HOME", filepath.Join(root, "home"))
59 if tc.disabled {
60 disabled := false
61 if err := SaveConfig(Config{PonytailEnabled: &disabled}); err != nil {
62 t.Fatal(err)
63 }
64 }
65
66 resolved, err := Resolve(Args{
67 Provider: "ollama",
68 Model: "any-local-model",
69 CWD: filepath.Join(root, "project"),
70 NoLSP: true,
71 NoSkill: true,
72 NoTools: true,
73 }, false)
74 if err != nil {
75 t.Fatalf("Resolve failed: %v", err)
76 }
77 got := strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum())
78 if got != tc.want {
79 t.Fatalf("Ponytail prompt inclusion = %v, want %v:\n%s", got, tc.want, resolved.SystemPrompt)
80 }
81 if count := strings.Count(resolved.SystemPrompt, PonytailSystemAddendum()); count != btoi(tc.want) {
82 t.Fatalf("Ponytail prompt occurrence count = %d, want %d", count, btoi(tc.want))
83 }
84 })
85 }
86}
87
88func btoi(value bool) int {
89 if value {
90 return 1
91 }
92 return 0
93}
94
95func TestResolvePonytailModeRetainsAddendumWithSystemFile(t *testing.T) {
96 root := t.TempDir()
97 zutHome := filepath.Join(root, "zut-home")
98 t.Setenv("ZUT_HOME", zutHome)
99 t.Setenv("HOME", filepath.Join(root, "home"))
100 if err := os.MkdirAll(zutHome, 0o755); err != nil {
101 t.Fatal(err)
102 }
103 if err := os.WriteFile(filepath.Join(zutHome, "SYSTEM.md"), []byte("custom system identity"), 0o600); err != nil {
104 t.Fatal(err)
105 }
106
107 resolved, err := Resolve(Args{
108 Provider: "ollama",
109 Model: "any-local-model",
110 CWD: filepath.Join(root, "project"),
111 NoLSP: true,
112 NoSkill: true,
113 NoTools: true,
114 }, false)
115 if err != nil {
116 t.Fatalf("Resolve failed: %v", err)
117 }
118 if !strings.Contains(resolved.SystemPrompt, "custom system identity") {
119 t.Fatalf("SYSTEM.md content missing:\n%s", resolved.SystemPrompt)
120 }
121 if !strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum()) {
122 t.Fatalf("SYSTEM.md replacement lost Ponytail addendum:\n%s", resolved.SystemPrompt)
123 }
124}
125
126func TestResolvePonytailModePrecedesExplicitAppendPrompt(t *testing.T) {
127 root := t.TempDir()
128 t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home"))
129 t.Setenv("HOME", filepath.Join(root, "home"))
130
131 const explicit = "explicit invocation guidance"
132 resolved, err := Resolve(Args{
133 Provider: "ollama",
134 Model: "any-local-model",
135 CWD: filepath.Join(root, "project"),
136 NoLSP: true,
137 NoSkill: true,
138 NoTools: true,
139 AppendSystemPrompt: []string{explicit},
140 }, false)
141 if err != nil {
142 t.Fatalf("Resolve failed: %v", err)
143 }
144 ponytailIndex := strings.Index(resolved.SystemPrompt, PonytailSystemAddendum())
145 explicitIndex := strings.Index(resolved.SystemPrompt, explicit)
146 if ponytailIndex < 0 || explicitIndex < 0 || ponytailIndex > explicitIndex {
147 t.Fatalf("prompt ordering = Ponytail %d, explicit %d:\n%s", ponytailIndex, explicitIndex, resolved.SystemPrompt)
148 }
149}
150
151func TestResolvePonytailModeAppliesAcrossHeadlessModesAndCustomIdentity(t *testing.T) {
152 root := t.TempDir()
153 t.Setenv("ZUT_HOME", filepath.Join(root, "zut-home"))
154 t.Setenv("HOME", filepath.Join(root, "home"))
155
156 modes := []Mode{"", ModePrint, ModeStream, ModeJSON, ModeRPC}
157 for _, mode := range modes {
158 resolved, err := Resolve(Args{
159 Mode: mode,
160 Provider: "ollama",
161 Model: "any-local-model",
162 CWD: filepath.Join(root, "project"),
163 NoLSP: true,
164 NoSkill: true,
165 NoTools: true,
166 SystemPrompt: "custom identity",
167 }, false)
168 if err != nil {
169 t.Fatalf("Resolve(%q) failed: %v", mode, err)
170 }
171 if !strings.Contains(resolved.SystemPrompt, "custom identity") {
172 t.Fatalf("Resolve(%q) lost custom identity:\n%s", mode, resolved.SystemPrompt)
173 }
174 if !strings.Contains(resolved.SystemPrompt, PonytailSystemAddendum()) {
175 t.Fatalf("Resolve(%q) omitted Ponytail guidance:\n%s", mode, resolved.SystemPrompt)
176 }
177 }
178}
179
180func TestConfigSettingsStorePersistsPonytailWithoutChangingKnownFields(t *testing.T) {
181 t.Setenv("ZUT_HOME", t.TempDir())
182 if err := SaveConfig(Config{Theme: "dark", Provider: "ollama", Model: "local"}); err != nil {
183 t.Fatal(err)
184 }
185
186 if err := (configSettingsStore{}).SetPonytailEnabled(false); err != nil {
187 t.Fatal(err)
188 }
189 cfg, err := LoadConfig()
190 if err != nil {
191 t.Fatal(err)
192 }
193 if cfg.PonytailEnabled == nil || *cfg.PonytailEnabled {
194 t.Fatal("Ponytail mode was not persisted as disabled")
195 }
196 if cfg.Theme != "dark" || cfg.Provider != "ollama" || cfg.Model != "local" {
197 t.Fatalf("known config fields changed: %#v", cfg)
198 }
199}