package loader import ( "errors" "strings" "testing" "gopkg.in/yaml.v3" ) // thinkingPersonaYAML builds a minimal valid persona with an extra `thinking` // block appended verbatim (an empty extra means "no thinking section at all"). func thinkingPersonaYAML(extra string) string { return "id: xilian\nname: 昔涟\ncharacterRef: c\nidentity:\n role: r\n systemPrompt: s\n" + extra } // TestLoad_ThinkingAbsentKeepsCurrentBehavior locks the "optional section" // promise: without a `thinking` key every field stays at its zero value, the // showToUser pointer stays nil (the distinction the design calls out) and // re-marshaling the persona does NOT emit a thinking block, so the admin write // path cannot add noise to an existing persona file. func TestLoad_ThinkingAbsentKeepsCurrentBehavior(t *testing.T) { t.Parallel() path := writeFile(t, t.TempDir(), "xilian.yaml", thinkingPersonaYAML("")) p, err := Load(path) if err != nil { t.Fatalf("Load() error: %v", err) } if p.Thinking.BudgetTokens != 0 || p.Thinking.Effort != "" || p.Thinking.Mode != "" { t.Fatalf("Thinking = %+v, want the zero value", p.Thinking) } if p.Thinking.ShowToUser != nil { t.Fatalf("ShowToUser = %v, want nil (unset)", *p.Thinking.ShowToUser) } if p.Thinking.Enable() != nil { t.Fatalf("Enable() = %v, want nil for an unset mode", *p.Thinking.Enable()) } out, err := yaml.Marshal(p) if err != nil { t.Fatalf("Marshal() error: %v", err) } if strings.Contains(string(out), "thinking") { t.Fatalf("marshal emitted a thinking block for an unset section:\n%s", out) } } // TestLoad_ThinkingParsed asserts the documented YAML keys land on the struct. func TestLoad_ThinkingParsed(t *testing.T) { t.Parallel() yaml := thinkingPersonaYAML("thinking:\n budgetTokens: 4096\n effort: medium\n showToUser: false\n mode: on\n") path := writeFile(t, t.TempDir(), "xilian.yaml", yaml) p, err := Load(path) if err != nil { t.Fatalf("Load() error: %v", err) } if p.Thinking.BudgetTokens != 4096 { t.Errorf("BudgetTokens = %d, want 4096", p.Thinking.BudgetTokens) } if p.Thinking.Effort != "medium" { t.Errorf("Effort = %q, want medium", p.Thinking.Effort) } if p.Thinking.Mode != ThinkingModeOn { t.Errorf("Mode = %q, want on", p.Thinking.Mode) } if p.Thinking.ShowToUser == nil { t.Fatal("ShowToUser = nil, want an explicit false") } if *p.Thinking.ShowToUser { t.Error("ShowToUser = true, want false") } if en := p.Thinking.Enable(); en == nil || !*en { t.Fatalf("Enable() = %v, want true for mode=on", en) } } // TestLoad_ThinkingShowToUserFalseDiffersFromOmitted is the pointer contract: // omitting the key must keep the current behavior (nil, frames are shown) while // an explicit false must be distinguishable from it (store, don't push). A plain // bool field would collapse both into false and fail here. func TestLoad_ThinkingShowToUserFalseDiffersFromOmitted(t *testing.T) { t.Parallel() dir := t.TempDir() omitted, err := Load(writeFile(t, dir, "omitted.yaml", thinkingPersonaYAML("thinking:\n budgetTokens: 1024\n"))) if err != nil { t.Fatalf("Load(omitted) error: %v", err) } off, err := Load(writeFile(t, dir, "off.yaml", thinkingPersonaYAML("thinking:\n showToUser: false\n"))) if err != nil { t.Fatalf("Load(off) error: %v", err) } if omitted.Thinking.ShowToUser != nil { t.Fatalf("omitted ShowToUser = %v, want nil", *omitted.Thinking.ShowToUser) } if off.Thinking.ShowToUser == nil { t.Fatal("explicit false ShowToUser = nil, want a pointer to false") } if *off.Thinking.ShowToUser { t.Fatal("explicit false ShowToUser = true") } } // TestLoad_ThinkingInvalidModeRejected asserts a bad mode is reported with its // own identifiable error instead of being silently ignored. func TestLoad_ThinkingInvalidModeRejected(t *testing.T) { t.Parallel() path := writeFile(t, t.TempDir(), "bad.yaml", thinkingPersonaYAML("thinking:\n mode: sometimes\n")) _, err := Load(path) if err == nil { t.Fatal("Load(mode: sometimes) want error, got nil") } if !errors.Is(err, ErrInvalidThinking) { t.Fatalf("error = %v, want ErrInvalidThinking", err) } if !strings.Contains(err.Error(), "sometimes") { t.Fatalf("error %q does not name the offending value", err) } } // TestLoad_ThinkingNegativeBudgetRejected asserts a negative budget is rejected // (it would otherwise become a nonsensical provider request). func TestLoad_ThinkingNegativeBudgetRejected(t *testing.T) { t.Parallel() path := writeFile(t, t.TempDir(), "bad.yaml", thinkingPersonaYAML("thinking:\n budgetTokens: -1\n")) if _, err := Load(path); !errors.Is(err, ErrInvalidThinking) { t.Fatalf("error = %v, want ErrInvalidThinking", err) } } // TestThinkingParams_EnableModeMapping pins the tri-state mapping the model layer // depends on: only an explicit on/off produces a switch, default/unset yields nil // so provider thinking is never turned on as a side effect. func TestThinkingParams_EnableModeMapping(t *testing.T) { t.Parallel() cases := []struct { mode string want *bool }{ {mode: "", want: nil}, {mode: ThinkingModeDefault, want: nil}, } for _, tt := range cases { got := ThinkingParams{Mode: tt.mode}.Enable() if got != nil { t.Fatalf("Enable(%q) = %v, want nil", tt.mode, *got) } } on := ThinkingParams{Mode: ThinkingModeOn}.Enable() if on == nil || !*on { t.Fatalf("Enable(on) = %v, want true", on) } off := ThinkingParams{Mode: ThinkingModeOff}.Enable() if off == nil || *off { t.Fatalf("Enable(off) = %v, want false", off) } }