Files
DeepSeek-Reasonix/internal/config/edit_test.go
T
SivanColaandreasonix b87a878cad feat(desktop): project topic tabs and context inspector (#2925)
* feat(desktop): add project tabs and context inspector

* feat(desktop): refine project workspace UI

* fix 打开一致加载

* 顶部统一logo+左侧边栏+统一菜单

* 底部最低宽度

* update much

* update code

* update code

* feat(desktop): refine workspace layout and message actions

* fix(desktop): scope file refs to active tab root

* test(desktop): stabilize capabilities config dirs

* fix(desktop): protect sessions open in other tabs

* fix(desktop): resume scoped history in matching tab

* chore(desktop): drop committed scratch artifacts; ignore tmp/ and .codex/

The project-tabs branch carried 30 throwaway screenshots, a mock HTML,
and a codex env file into the tree because tmp/ and .codex/ were never
ignored. Remove them and add the ignore rules so scratch output stays
out of future commits.

* test(desktop): isolate AppData per test so the suite passes on Windows

The new project/topic/workspace tests only overrode HOME and
XDG_CONFIG_HOME, but desktopConfigDir() and config.MemoryUserDir()
resolve through os.UserConfigDir(), which reads %AppData% on Windows and
ignores those vars. With AppData pointed at one shared TestMain dir, the
project/topic/workspace registries leaked across tests: full-package
runs failed (project tree len 2 want 1, trash mixing other tests'
sessions, Plugins[0] reading a neighbor's MCP entry) while each test
passed alone.

Route every test through isolateDesktopUserDirs, which already sets
HOME, USERPROFILE, XDG_CONFIG_HOME, and AppData to a per-test temp dir.
go test ./... (and -race) now pass on Windows.

---------

Co-authored-by: reasonix <[email protected]>
2026-06-04 19:41:16 -07:00

676 lines
20 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/BurntSushi/toml"
)
func TestSetDefaultModel(t *testing.T) {
c := Default()
if err := c.SetDefaultModel("mimo-pro"); err != nil {
t.Fatalf("set valid default: %v", err)
}
if c.DefaultModel != "mimo-pro" {
t.Errorf("default = %q, want mimo-pro", c.DefaultModel)
}
if err := c.SetDefaultModel("nope"); err == nil {
t.Error("expected error for unknown provider")
}
}
func TestUIThemeNormalizes(t *testing.T) {
c := Default()
for _, tt := range []struct {
in string
want string
}{
{"", "auto"},
{"AUTO", "auto"},
{"dark", "dark"},
{" light ", "light"},
{"unknown", "auto"},
} {
c.UI.Theme = tt.in
if got := c.UITheme(); got != tt.want {
t.Errorf("UITheme(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestUIThemeStyleNormalizes(t *testing.T) {
c := Default()
for _, tt := range []struct {
in string
want string
}{
{"", ""},
{"AURORA", "aurora"},
{" glacier ", "glacier"},
{"unknown", ""},
} {
c.UI.ThemeStyle = tt.in
if got := c.UIThemeStyle(); got != tt.want {
t.Errorf("UIThemeStyle(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestUICloseBehaviorNormalizes(t *testing.T) {
c := Default()
for _, tt := range []struct {
in string
want string
}{
{"", "background"},
{"QUIT", "quit"},
{"exit", "quit"},
{" background ", "background"},
{"hide", "background"},
{"unknown", "background"},
} {
c.UI.CloseBehavior = tt.in
if got := c.UICloseBehavior(); got != tt.want {
t.Errorf("UICloseBehavior(%q) = %q, want %q", tt.in, got, tt.want)
}
}
}
func TestDesktopPreferencesAreSeparateFromCLI(t *testing.T) {
c := Default()
c.Language = "zh"
c.UI.Theme = "light"
c.UI.ThemeStyle = "glacier"
if err := c.SetDesktopLanguage("en"); err != nil {
t.Fatalf("SetDesktopLanguage: %v", err)
}
if err := c.SetDesktopAppearance("dark", "graphite"); err != nil {
t.Fatalf("SetDesktopAppearance: %v", err)
}
if c.Language != "zh" {
t.Fatalf("CLI language changed to %q", c.Language)
}
if got := c.UITheme(); got != "light" {
t.Fatalf("CLI theme = %q, want light", got)
}
if got := c.UIThemeStyle(); got != "glacier" {
t.Fatalf("CLI theme style = %q, want glacier", got)
}
if got := c.DesktopLanguage(); got != "en" {
t.Fatalf("desktop language = %q, want en", got)
}
if got := c.DesktopTheme(); got != "dark" {
t.Fatalf("desktop theme = %q, want dark", got)
}
if got := c.DesktopThemeStyle(); got != "graphite" {
t.Fatalf("desktop theme style = %q, want graphite", got)
}
}
func TestDesktopCloseBehaviorFallsBackToLegacyUI(t *testing.T) {
c := Default()
c.UI.CloseBehavior = "quit"
if got := c.DesktopCloseBehavior(); got != "quit" {
t.Fatalf("legacy close behavior = %q, want quit", got)
}
c.Desktop.CloseBehavior = "background"
if got := c.DesktopCloseBehavior(); got != "background" {
t.Fatalf("desktop close behavior = %q, want background", got)
}
}
func TestSetUICloseBehavior(t *testing.T) {
c := Default()
if err := c.SetUICloseBehavior("background"); err != nil {
t.Fatalf("SetUICloseBehavior background: %v", err)
}
if got := c.UICloseBehavior(); got != "background" {
t.Fatalf("close behavior = %q, want background", got)
}
if err := c.SetUICloseBehavior("quit"); err != nil {
t.Fatalf("SetUICloseBehavior quit: %v", err)
}
if got := c.UICloseBehavior(); got != "quit" {
t.Fatalf("close behavior = %q, want quit", got)
}
if err := c.SetUICloseBehavior("later"); err == nil {
t.Fatal("expected error for invalid close behavior")
}
}
func TestSetPlannerModel(t *testing.T) {
c := Default()
if err := c.SetPlannerModel("deepseek-pro"); err != nil {
t.Fatalf("set planner: %v", err)
}
if c.Agent.PlannerModel != "deepseek-pro" {
t.Errorf("planner = %q", c.Agent.PlannerModel)
}
if err := c.SetPlannerModel(""); err != nil || c.Agent.PlannerModel != "" {
t.Errorf("clearing planner failed: err=%v planner=%q", err, c.Agent.PlannerModel)
}
if err := c.SetPlannerModel("ghost"); err == nil {
t.Error("expected error for unknown planner")
}
}
func TestUpsertProvider(t *testing.T) {
c := Default()
n := len(c.Providers)
// Add a new one.
if err := c.UpsertProvider(ProviderEntry{Name: "local", Kind: "openai", BaseURL: "http://localhost:1234/v1", Model: "x"}); err != nil {
t.Fatalf("add: %v", err)
}
if len(c.Providers) != n+1 {
t.Fatalf("provider count = %d, want %d", len(c.Providers), n+1)
}
// Replace it in place (no growth, position preserved).
if err := c.UpsertProvider(ProviderEntry{Name: "local", Kind: "openai", BaseURL: "http://localhost:9999/v1", Model: "y"}); err != nil {
t.Fatalf("replace: %v", err)
}
if len(c.Providers) != n+1 {
t.Errorf("replace grew the list to %d", len(c.Providers))
}
got, _ := c.Provider("local")
if got.BaseURL != "http://localhost:9999/v1" || got.Model != "y" {
t.Errorf("replace didn't apply: %+v", got)
}
// Missing required fields error.
for _, bad := range []ProviderEntry{
{Kind: "openai", BaseURL: "u", Model: "m"}, // no name
{Name: "a", BaseURL: "u", Model: "m"}, // no kind
{Name: "a", Kind: "openai", Model: "m"}, // no base_url
{Name: "a", Kind: "openai", BaseURL: "u"}, // no model
} {
if err := c.UpsertProvider(bad); err == nil {
t.Errorf("expected validation error for %+v", bad)
}
}
}
func TestSetProviderEffort(t *testing.T) {
c := Default()
if err := c.SetProviderEffort("deepseek-flash", "MAX"); err != nil {
t.Fatalf("SetProviderEffort: %v", err)
}
p, _ := c.Provider("deepseek-flash")
if p.Effort != "max" {
t.Fatalf("effort = %q, want max", p.Effort)
}
if err := c.SetProviderEffort("missing", "high"); err == nil {
t.Fatal("SetProviderEffort should reject unknown provider")
}
}
func TestSetLanguage(t *testing.T) {
c := Default()
if err := c.SetLanguage("zh"); err != nil {
t.Fatalf("SetLanguage zh: %v", err)
}
if c.Language != "zh" {
t.Fatalf("language = %q, want zh", c.Language)
}
if err := c.SetLanguage("auto"); err != nil {
t.Fatalf("SetLanguage auto: %v", err)
}
if c.Language != "" {
t.Fatalf("language = %q, want cleared", c.Language)
}
}
func TestNormalizeEffortDeepSeek(t *testing.T) {
e := &ProviderEntry{Name: "deepseek", Kind: "openai", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4"}
cap := EffortCapabilityForEntry(e)
if !cap.Supported || len(cap.Levels) != 3 || cap.Levels[0] != "auto" || cap.Levels[1] != "high" || cap.Levels[2] != "max" {
t.Fatalf("DeepSeek levels = %+v, want auto/high/max", cap)
}
for in, want := range map[string]string{"auto": "", "high": "high", "max": "max", "low": "high", "medium": "high", "xhigh": "max"} {
got, err := NormalizeEffort(e, in)
if err != nil || got != want {
t.Fatalf("NormalizeEffort(%q) = %q/%v, want %q/nil", in, got, err, want)
}
}
if _, err := NormalizeEffort(e, "off"); err == nil {
t.Fatal("DeepSeek /effort must reject off")
}
}
func TestNormalizeLegacyEffortMigratesOff(t *testing.T) {
c := &Config{Providers: []ProviderEntry{
{Name: "deepseek", Effort: "off"},
{Name: "deepseek-upper", Effort: "OFF"},
{Name: "keep", Effort: "high"},
}}
normalizeLegacyEffort(c)
if c.Providers[0].Effort != "" || c.Providers[1].Effort != "" {
t.Fatalf("legacy off should migrate to empty, got %q/%q", c.Providers[0].Effort, c.Providers[1].Effort)
}
if c.Providers[2].Effort != "high" {
t.Fatalf("non-legacy effort changed: %q", c.Providers[2].Effort)
}
}
func TestNormalizeEffortAnthropic(t *testing.T) {
e := &ProviderEntry{Name: "claude", Kind: "anthropic", Model: "claude-opus-4-8"}
cap := EffortCapabilityForEntry(e)
if !cap.Supported || len(cap.Levels) != 6 {
t.Fatalf("Anthropic levels = %+v, want auto plus five levels", cap)
}
for _, level := range []string{"low", "medium", "high", "xhigh", "max"} {
got, err := NormalizeEffort(e, level)
if err != nil || got != level {
t.Fatalf("NormalizeEffort(%q) = %q/%v, want %q/nil", level, got, err, level)
}
}
got, err := NormalizeEffort(e, "auto")
if err != nil || got != "" {
t.Fatalf("NormalizeEffort(auto) = %q/%v, want empty/nil", got, err)
}
}
func TestResolveModelPreservesProviderEffort(t *testing.T) {
c := Default()
c.Providers = append(c.Providers, ProviderEntry{
Name: "deepseek",
Kind: "openai",
BaseURL: "https://api.deepseek.com",
Model: "deepseek-v4-flash",
Models: []string{"deepseek-v4-flash", "deepseek-v4-pro"},
Default: "deepseek-v4-flash",
APIKeyEnv: "DEEPSEEK_API_KEY",
Effort: "max",
})
e, ok := c.ResolveModel("deepseek/deepseek-v4-pro")
if !ok {
t.Fatal("ResolveModel did not find deepseek/deepseek-v4-pro")
}
if e.Name != "deepseek" || e.Model != "deepseek-v4-pro" || e.Effort != "max" {
t.Fatalf("resolved entry = %+v, want provider deepseek model deepseek-v4-pro effort max", e)
}
}
func TestRemoveProvider(t *testing.T) {
c := Default()
c.Agent.PlannerModel = "deepseek-pro"
// Cannot remove the default model.
if err := c.RemoveProvider(c.DefaultModel); err == nil {
t.Error("expected error removing the default model")
}
// Removing the planner provider clears planner_model.
if err := c.RemoveProvider("deepseek-pro"); err != nil {
t.Fatalf("remove planner provider: %v", err)
}
if c.Agent.PlannerModel != "" {
t.Errorf("planner should be cleared, got %q", c.Agent.PlannerModel)
}
if _, ok := c.Provider("deepseek-pro"); ok {
t.Error("provider not actually removed")
}
// Unknown name errors.
if err := c.RemoveProvider("ghost"); err == nil {
t.Error("expected error for unknown provider")
}
}
func TestPermissionMutators(t *testing.T) {
c := Default()
if err := c.SetPermissionMode("DENY"); err != nil || c.Permissions.Mode != "deny" {
t.Errorf("set mode: err=%v mode=%q", err, c.Permissions.Mode)
}
if err := c.SetPermissionMode("nonsense"); err == nil {
t.Error("expected error for bad mode")
}
if err := c.AddPermissionRule("deny", "bash(rm -rf*)"); err != nil {
t.Fatalf("add deny: %v", err)
}
// Duplicate is a no-op, not an error or a second entry.
if err := c.AddPermissionRule("deny", "bash(rm -rf*)"); err != nil {
t.Fatalf("dup add: %v", err)
}
if len(c.Permissions.Deny) != 1 {
t.Errorf("deny list = %v, want one entry", c.Permissions.Deny)
}
// Invalid rule and unknown list both error.
if err := c.AddPermissionRule("deny", " "); err == nil {
t.Error("expected error for empty rule")
}
if err := c.AddPermissionRule("nope", "read_file"); err == nil {
t.Error("expected error for unknown list")
}
removed, err := c.RemovePermissionRule("deny", "bash(rm -rf*)")
if err != nil || !removed {
t.Errorf("remove: removed=%v err=%v", removed, err)
}
if removed, _ := c.RemovePermissionRule("deny", "absent"); removed {
t.Error("removing absent rule should report false")
}
}
func TestSkillPathMutators(t *testing.T) {
c := Default()
root := t.TempDir()
if err := c.AddSkillPath(root); err != nil {
t.Fatalf("add skill path: %v", err)
}
if err := c.AddSkillPath(filepath.Join(root, ".")); err != nil {
t.Fatalf("duplicate skill path: %v", err)
}
if len(c.Skills.Paths) != 1 {
t.Fatalf("paths = %v, want one deduped entry", c.Skills.Paths)
}
if err := c.AddSkillPath(" "); err == nil {
t.Fatal("empty skill path should error")
}
removed, err := c.RemoveSkillPath(filepath.Join(root, "."))
if err != nil || !removed {
t.Fatalf("remove skill path: removed=%v err=%v", removed, err)
}
if len(c.Skills.Paths) != 0 {
t.Fatalf("paths after remove = %v", c.Skills.Paths)
}
if removed, err := c.RemoveSkillPath(root); err != nil || removed {
t.Fatalf("remove absent: removed=%v err=%v", removed, err)
}
}
func TestSkillEnabledMutator(t *testing.T) {
c := Default()
if err := c.SetSkillEnabled("review", false); err != nil {
t.Fatalf("disable skill: %v", err)
}
if err := c.SetSkillEnabled("review", false); err != nil {
t.Fatalf("disable duplicate skill: %v", err)
}
if len(c.Skills.DisabledSkills) != 1 || c.Skills.DisabledSkills[0] != "review" {
t.Fatalf("disabled skills = %v, want [review]", c.Skills.DisabledSkills)
}
if !c.IsSkillDisabled("review") {
t.Fatal("review should be disabled")
}
if err := c.SetSkillEnabled("review", true); err != nil {
t.Fatalf("enable skill: %v", err)
}
if len(c.Skills.DisabledSkills) != 0 {
t.Fatalf("disabled skills after enable = %v, want empty", c.Skills.DisabledSkills)
}
if err := c.SetSkillEnabled("bad name", false); err == nil {
t.Fatal("invalid skill name should error")
}
}
func TestPluginMutators(t *testing.T) {
c := Default()
if err := c.UpsertPlugin(PluginEntry{Name: "ex", Command: "reasonix-plugin-example"}); err != nil {
t.Fatalf("add stdio: %v", err)
}
if err := c.UpsertPlugin(PluginEntry{Name: "stripe", Type: "http", URL: "https://mcp.stripe.com"}); err != nil {
t.Fatalf("add http: %v", err)
}
if len(c.Plugins) != 2 {
t.Fatalf("plugin count = %d, want 2", len(c.Plugins))
}
// Transport validation: stdio needs command, http needs url.
if err := c.UpsertPlugin(PluginEntry{Name: "bad"}); err == nil {
t.Error("stdio without command should error")
}
if err := c.UpsertPlugin(PluginEntry{Name: "bad", Type: "http"}); err == nil {
t.Error("http without url should error")
}
if err := c.UpsertPlugin(PluginEntry{Name: "bad", Type: "carrier-pigeon", Command: "x"}); err == nil {
t.Error("unknown transport should error")
}
// Replace in place.
if err := c.UpsertPlugin(PluginEntry{Name: "ex", Command: "other-cmd"}); err != nil {
t.Fatalf("replace: %v", err)
}
if len(c.Plugins) != 2 {
t.Errorf("replace grew plugins to %d", len(c.Plugins))
}
if !c.RemovePlugin("ex") {
t.Error("remove should report true")
}
if c.RemovePlugin("ex") {
t.Error("second remove should report false")
}
}
func TestAutoStartPlugins(t *testing.T) {
c := Default()
off := false
on := true
c.Plugins = []PluginEntry{
{Name: "implicit", Command: "implicit-bin"},
{Name: "disabled", Command: "disabled-bin", AutoStart: &off},
{Name: "enabled", Command: "enabled-bin", AutoStart: &on},
}
got := c.AutoStartPlugins()
if len(got) != 2 || got[0].Name != "implicit" || got[1].Name != "enabled" {
t.Fatalf("AutoStartPlugins = %+v, want implicit + enabled", got)
}
}
func TestCodegraphDefaultEnabledForUpgrades(t *testing.T) {
c := Default()
if !c.Codegraph.Enabled {
t.Fatal("default codegraph enabled = false; existing configs without a [codegraph] section would lose it on upgrade")
}
if !c.Codegraph.AutoInstall {
t.Fatal("default codegraph auto_install = false, want true")
}
if c.Codegraph.Tier != "" {
t.Fatalf("default codegraph tier = %q, want unset (boot then preserves warm→eager/cold→background)", c.Codegraph.Tier)
}
}
func TestLoadForEditPreservesCodegraphWithoutSection(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "reasonix.toml")
if err := os.WriteFile(path, []byte("default_model = \"x\"\n"), 0o644); err != nil {
t.Fatal(err)
}
if c := LoadForEdit(path); !c.Codegraph.Enabled {
t.Fatal("a config omitting [codegraph] disabled codegraph; an upgrade must keep it on")
}
}
func TestLoadFirstRunDisablesCodegraph(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("USERPROFILE", t.TempDir())
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("AppData", t.TempDir())
t.Chdir(t.TempDir())
cfg, err := Load()
if err != nil {
t.Fatal(err)
}
if cfg.Codegraph.Enabled {
t.Fatal("first run (no config file anywhere) left codegraph enabled; new users should start without it")
}
}
func TestPluginResolvedTierDefaultsToLazy(t *testing.T) {
for _, tc := range []struct {
name string
tier string
want string
}{
{name: "empty", tier: "", want: "lazy"},
{name: "explicit lazy", tier: "lazy", want: "lazy"},
{name: "background", tier: "background", want: "background"},
{name: "eager", tier: "eager", want: "eager"},
{name: "unknown", tier: "startup", want: "lazy"},
} {
t.Run(tc.name, func(t *testing.T) {
got := (PluginEntry{Name: "mcp", Command: "mcp-server", Tier: tc.tier}).ResolvedTier()
if got != tc.want {
t.Fatalf("ResolvedTier(%q) = %q, want %q", tc.tier, got, tc.want)
}
})
}
}
func TestClearPluginAuthentication(t *testing.T) {
c := Default()
c.Plugins = []PluginEntry{{
Name: "dida",
Type: "http",
URL: "https://mcp.dida365.com/mcp?access_token=abc&workspace=main",
Headers: map[string]string{
"Authorization": "Bearer ${DIDA_TOKEN}",
"X-Org": "team",
},
Env: map[string]string{
"DIDA_TOKEN": "${DIDA_TOKEN}",
"DEBUG": "1",
},
Tier: "lazy",
}}
updated, changed, err := c.ClearPluginAuthentication("dida")
if err != nil {
t.Fatalf("ClearPluginAuthentication: %v", err)
}
if !changed {
t.Fatal("ClearPluginAuthentication should report changed")
}
if updated.URL != "https://mcp.dida365.com/mcp?workspace=main" {
t.Fatalf("url = %q", updated.URL)
}
if _, ok := updated.Headers["Authorization"]; ok {
t.Fatalf("auth header should be removed: %v", updated.Headers)
}
if updated.Headers["X-Org"] != "team" {
t.Fatalf("ordinary header should be preserved: %v", updated.Headers)
}
if _, ok := updated.Env["DIDA_TOKEN"]; ok {
t.Fatalf("auth env should be removed: %v", updated.Env)
}
if updated.Env["DEBUG"] != "1" {
t.Fatalf("ordinary env should be preserved: %v", updated.Env)
}
}
// TestSaveToRoundTrips stages several mutations, persists atomically, and
// re-decodes the file to confirm the changes survived a write/read cycle.
func TestSaveToRoundTrips(t *testing.T) {
c := Default()
if err := c.SetDefaultModel("mimo-pro"); err != nil {
t.Fatal(err)
}
if err := c.SetPlannerModel("deepseek-pro"); err != nil {
t.Fatal(err)
}
if err := c.UpsertProvider(ProviderEntry{Name: "local", Kind: "openai", BaseURL: "http://localhost:1234/v1", Model: "llama"}); err != nil {
t.Fatal(err)
}
if err := c.SetPermissionMode("deny"); err != nil {
t.Fatal(err)
}
if err := c.AddPermissionRule("allow", "bash(go test*)"); err != nil {
t.Fatal(err)
}
if err := c.SetNetwork(NetworkConfig{
ProxyMode: "custom",
Proxy: NetworkProxyConfig{
Type: "socks5",
Server: "127.0.0.1",
Port: 7890,
},
}); err != nil {
t.Fatal(err)
}
autoStart := false
if err := c.UpsertPlugin(PluginEntry{Name: "stripe", Type: "http", URL: "https://mcp.stripe.com", AutoStart: &autoStart}); err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "nested", "reasonix.toml")
if err := c.SaveTo(path); err != nil {
t.Fatalf("SaveTo: %v", err)
}
var got Config
if _, err := toml.DecodeFile(path, &got); err != nil {
t.Fatalf("saved file does not parse: %v", err)
}
if got.DefaultModel != "mimo-pro" {
t.Errorf("default_model = %q", got.DefaultModel)
}
if got.Agent.PlannerModel != "deepseek-pro" {
t.Errorf("planner_model = %q", got.Agent.PlannerModel)
}
if _, ok := got.Provider("local"); !ok {
t.Error("added provider 'local' missing after round-trip")
}
if got.Permissions.Mode != "deny" {
t.Errorf("mode = %q", got.Permissions.Mode)
}
if len(got.Permissions.Allow) != 1 || got.Permissions.Allow[0] != "bash(go test*)" {
t.Errorf("allow list = %v", got.Permissions.Allow)
}
if got.Network.ProxyMode != "custom" || got.Network.Proxy.Server != "127.0.0.1" || got.Network.Proxy.Port != 7890 {
t.Errorf("network = %+v", got.Network)
}
if len(got.Plugins) != 1 || got.Plugins[0].Name != "stripe" {
t.Errorf("plugins = %+v", got.Plugins)
}
if got.Plugins[0].AutoStart == nil || *got.Plugins[0].AutoStart {
t.Errorf("auto_start should round-trip false, got %+v", got.Plugins[0].AutoStart)
}
}
func TestSaveToScopesUserAndProjectFiles(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
c := Default()
c.Desktop.Theme = "dark"
c.Desktop.ThemeStyle = "graphite"
c.Desktop.CloseBehavior = "background"
userPath := UserConfigPath()
if err := c.SaveTo(userPath); err != nil {
t.Fatalf("SaveTo user config: %v", err)
}
userBody, err := os.ReadFile(userPath)
if err != nil {
t.Fatalf("read user config: %v", err)
}
if !strings.Contains(string(userBody), "[desktop]") {
t.Fatalf("user config should include desktop preferences:\n%s", userBody)
}
projectPath := filepath.Join(t.TempDir(), "reasonix.toml")
if err := c.SaveTo(projectPath); err != nil {
t.Fatalf("SaveTo project config: %v", err)
}
projectBody, err := os.ReadFile(projectPath)
if err != nil {
t.Fatalf("read project config: %v", err)
}
if strings.Contains(string(projectBody), "[desktop]") || strings.Contains(string(projectBody), "close_behavior") {
t.Fatalf("project config should not include desktop preferences:\n%s", projectBody)
}
}
func TestSetNetworkRejectsIncompleteCustomProxy(t *testing.T) {
c := Default()
if err := c.SetNetwork(NetworkConfig{ProxyMode: "custom"}); err == nil {
t.Fatal("custom proxy without server/port should be rejected")
}
}