package gateway import ( "strings" "testing" "gopkg.in/yaml.v3" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader" ) // TestPersonaUpsertRequest_ThinkingRoundTripsThroughYAML pins the admin persona // write path for the optional `thinking` block: the hand-written DTO maps into // the loader struct, yaml.Marshal emits the documented keys, and the reload of // that YAML yields the same values — i.e. the field really round-trips instead of // being dropped by an un-whitelisted DTO. func TestPersonaUpsertRequest_ThinkingRoundTripsThroughYAML(t *testing.T) { t.Parallel() show := false req := personaUpsertRequest{ ID: "xilian", Name: "昔涟", Role: "r", SystemPrompt: "s", Thinking: &personaThinkingRequest{BudgetTokens: 4096, Effort: " medium ", ShowToUser: &show, Mode: " on "}, } p := personaFromRequest(req) if err := p.ValidateThinking(); err != nil { t.Fatalf("ValidateThinking() error: %v", err) } data, err := yaml.Marshal(p) if err != nil { t.Fatalf("Marshal: %v", err) } back := &loader.Persona{} if err := yaml.Unmarshal(data, back); err != nil { t.Fatalf("Unmarshal: %v", err) } if back.Thinking.BudgetTokens != 4096 || back.Thinking.Mode != loader.ThinkingModeOn || back.Thinking.Effort != "medium" { t.Fatalf("thinking = %+v, want 4096/on/medium", back.Thinking) } if back.Thinking.ShowToUser == nil || *back.Thinking.ShowToUser { t.Fatalf("showToUser = %v, want an explicit false", back.Thinking.ShowToUser) } } // TestPersonaUpsertRequest_ThinkingInvalidRejected asserts an invalid thinking // block is rejected through the same validation the loader uses, so the admin API // cannot persist a mode the runtime would refuse to load. func TestPersonaUpsertRequest_ThinkingInvalidRejected(t *testing.T) { t.Parallel() p := personaFromRequest(personaUpsertRequest{ ID: "x", Name: "x", Role: "r", Thinking: &personaThinkingRequest{Mode: "always"}, }) err := p.ValidateThinking() if err == nil { t.Fatal("ValidateThinking() = nil, want an error") } if !strings.Contains(err.Error(), "always") { t.Fatalf("error %q does not name the offending mode", err) } } // TestPersonaUpsertRequest_NoThinkingBlockOmitsTheKey asserts a request without a // thinking block produces none in the YAML, so no existing persona file gains a // noise section. func TestPersonaUpsertRequest_NoThinkingBlockOmitsTheKey(t *testing.T) { t.Parallel() data, err := yaml.Marshal(personaFromRequest(personaUpsertRequest{ID: "x", Name: "x", Role: "r", SystemPrompt: "s"})) if err != nil { t.Fatalf("Marshal: %v", err) } if strings.Contains(string(data), "thinking") { t.Fatalf("marshaled persona carries a thinking block:\n%s", data) } } // TestApplyThinkingEdit_PreservesOnOmission is the non-destructive update rule: a // client that omits `thinking` keeps the stored block, an explicit block replaces // it, and an all-default block clears it. func TestApplyThinkingEdit_PreservesOnOmission(t *testing.T) { t.Parallel() show := false stored := &loader.Persona{Thinking: loader.ThinkingParams{BudgetTokens: 8192, Mode: loader.ThinkingModeOn, ShowToUser: &show}} kept := applyThinkingEdit(nil, stored) if kept.BudgetTokens != 8192 || kept.Mode != loader.ThinkingModeOn || kept.ShowToUser == nil { t.Fatalf("applyThinkingEdit(nil, stored) = %+v, want the stored block", kept) } replaced := applyThinkingEdit(&personaThinkingRequest{BudgetTokens: 1024, Mode: "off"}, stored) if replaced.BudgetTokens != 1024 || replaced.Mode != loader.ThinkingModeOff || replaced.ShowToUser != nil { t.Fatalf("applyThinkingEdit(explicit, stored) = %+v, want the request block", replaced) } cleared := applyThinkingEdit(&personaThinkingRequest{}, stored) if cleared.BudgetTokens != 0 || cleared.Mode != "" || cleared.ShowToUser != nil { t.Fatalf("applyThinkingEdit(empty object, stored) = %+v, want the zero block", cleared) } if created := applyThinkingEdit(nil, nil); created.BudgetTokens != 0 || created.ShowToUser != nil { t.Fatalf("applyThinkingEdit(nil, nil) = %+v, want the zero block (create path)", created) } }