1package agent
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestReadAgentsContextLoadsGlobalAndAncestors(t *testing.T) {
11 root := t.TempDir()
12 zotHome := filepath.Join(root, "zot-home")
13 project := filepath.Join(root, "repo")
14 nested := filepath.Join(project, "packages", "app")
15 if err := os.MkdirAll(zotHome, 0o755); err != nil {
16 t.Fatal(err)
17 }
18 if err := os.MkdirAll(nested, 0o755); err != nil {
19 t.Fatal(err)
20 }
21 if err := os.WriteFile(filepath.Join(zotHome, "AGENTS.md"), []byte("global rule"), 0o644); err != nil {
22 t.Fatal(err)
23 }
24 if err := os.WriteFile(filepath.Join(project, "AGENTS.md"), []byte("repo rule"), 0o644); err != nil {
25 t.Fatal(err)
26 }
27 if err := os.WriteFile(filepath.Join(nested, "AGENTS.md"), []byte("app rule"), 0o644); err != nil {
28 t.Fatal(err)
29 }
30
31 got := readAgentsContext(nested, zotHome)
32 for _, want := range []string{"global rule", "repo rule", "app rule"} {
33 if !strings.Contains(got, want) {
34 t.Fatalf("readAgentsContext missing %q in:\n%s", want, got)
35 }
36 }
37 if strings.Index(got, "global rule") > strings.Index(got, "repo rule") || strings.Index(got, "repo rule") > strings.Index(got, "app rule") {
38 t.Fatalf("AGENTS.md files loaded in wrong order:\n%s", got)
39 }
40}
41
42func TestReadAgentsContextMissingFilesIsEmpty(t *testing.T) {
43 got := readAgentsContext(t.TempDir(), t.TempDir())
44 if got != "" {
45 t.Fatalf("expected no context, got %q", got)
46 }
47}
48
49// TestResolveFallsBackWhenConfiguredModelIsGone reproduces the
50// startup failure caught by the user's screenshot: the persisted
51// config.json points at a model id that's no longer in the active
52// catalogue (because they edited models.json or zot's bundled
53// catalogue changed). Resolve must NOT error — strands the user
54// with no way to fix it from the TUI — and should repair the config
55// so the next launch is silent.
56func TestResolveFallsBackWhenConfiguredModelIsGone(t *testing.T) {
57 t.Setenv("ZOT_HOME", t.TempDir())
58 t.Setenv("OPENAI_API_KEY", "test-key")
59 // Persist a stale model id.
60 stale := "gpt-5.5-pro-not-real"
61 if err := SaveConfig(Config{Provider: "openai", Model: stale}); err != nil {
62 t.Fatal(err)
63 }
64
65 r, err := Resolve(Args{}, false)
66 if err != nil {
67 t.Fatalf("Resolve refused to launch with stale model: %v", err)
68 }
69 if r.Model == stale {
70 t.Fatalf("Resolve kept stale model %q", r.Model)
71 }
72 if r.Provider != "openai" {
73 t.Errorf("provider drifted: got %q; want openai", r.Provider)
74 }
75
76 // Config on disk should now hold the fallback so subsequent
77 // launches don't repeat the warning.
78 cfg, _ := LoadConfig()
79 if cfg.Model == stale {
80 t.Errorf("config.json still pins the stale model %q", cfg.Model)
81 }
82 if cfg.Model == "" {
83 t.Errorf("config.json was emptied; expected the fallback model id")
84 }
85}
86
87// TestResolveExplicitFlagStaleDoesNotRepairConfig confirms the
88// repair-on-disk happens ONLY when the stale id came from the
89// persisted config. If the user passed --model X explicitly and X is
90// unknown, we still fall back, but we don't touch their config.
91func TestResolveExplicitFlagStaleDoesNotRepairConfig(t *testing.T) {
92 t.Setenv("ZOT_HOME", t.TempDir())
93 t.Setenv("OPENAI_API_KEY", "test-key")
94 good := "gpt-5"
95 if err := SaveConfig(Config{Provider: "openai", Model: good}); err != nil {
96 t.Fatal(err)
97 }
98
99 r, err := Resolve(Args{Model: "gpt-totally-fake"}, false)
100 if err != nil {
101 t.Fatalf("Resolve errored on unknown --model: %v", err)
102 }
103 if r.Model == "gpt-totally-fake" {
104 t.Errorf("Resolve kept the bogus --model value")
105 }
106 cfg, _ := LoadConfig()
107 if cfg.Model != good {
108 t.Errorf("config.json was clobbered (was %q; now %q)", good, cfg.Model)
109 }
110}