// agentconfig 測試 — AB9 範圍。 package agentconfig import ( "os" "path/filepath" "strings" "testing" ) func TestDefault_MatchesTDD(t *testing.T) { d := Default() if d.Version != currentSchemaVersion { t.Errorf("Version = %d; want %d", d.Version, currentSchemaVersion) } if d.Relay.URL != DefaultRelayURL { t.Errorf("Relay.URL = %q; want %q", d.Relay.URL, DefaultRelayURL) } if d.Relay.Reconnect != ReconnectAuto { t.Errorf("Relay.Reconnect = %q; want auto", d.Relay.Reconnect) } if d.Behavior.Autostart != false { t.Error("Behavior.Autostart should default false") } if d.Log.Level != LogLevelInfo { t.Errorf("Log.Level = %q; want info", d.Log.Level) } } func TestValidate_AcceptsDefaults(t *testing.T) { cfg := Default() if err := cfg.Validate(); err != nil { t.Errorf("Default should be valid; err = %v", err) } } func TestValidate_RejectsBadRelayURL(t *testing.T) { cases := []string{ "", "http://relay.example.com", "https://relay.example.com", "relay.example.com", "wss", } for _, url := range cases { cfg := Default() cfg.Relay.URL = url if err := cfg.Validate(); err == nil { t.Errorf("Validate should reject Relay.URL=%q", url) } } } func TestValidate_AcceptsWsAndWss(t *testing.T) { for _, url := range []string{"ws://localhost:8080/tunnel", "wss://relay.example.com/tunnel/connect"} { cfg := Default() cfg.Relay.URL = url if err := cfg.Validate(); err != nil { t.Errorf("Validate should accept %q; err = %v", url, err) } } } func TestValidate_RejectsBadReconnectMode(t *testing.T) { cfg := Default() cfg.Relay.Reconnect = "never" if err := cfg.Validate(); err == nil { t.Error("Validate should reject Reconnect=never") } } func TestValidate_RejectsBadLogLevel(t *testing.T) { cfg := Default() cfg.Log.Level = "trace" if err := cfg.Validate(); err == nil { t.Error("Validate should reject Log.Level=trace") } } func TestValidate_ReportsAllErrors(t *testing.T) { cfg := Default() cfg.Relay.URL = "bad" cfg.Relay.Reconnect = "maybe" cfg.Log.Level = "trace" err := cfg.Validate() if err == nil { t.Fatal("Validate should fail on 3 bad fields") } msg := err.Error() for _, needle := range []string{"relay.url", "relay.reconnect", "log.level"} { if !strings.Contains(msg, needle) { t.Errorf("Validate error should mention %q; got: %s", needle, msg) } } } func TestNewStore_MissingFileUsesDefaults(t *testing.T) { dir := t.TempDir() s, err := NewStore(dir, nil) if err != nil { t.Fatalf("NewStore: %v", err) } cfg := s.Get() if cfg.Relay.URL != DefaultRelayURL { t.Errorf("missing file should yield default URL; got %q", cfg.Relay.URL) } // 首次啟動應該把預設值寫下去 if _, err := os.Stat(s.Path()); err != nil { t.Errorf("config.yaml should be created on first launch; stat err = %v", err) } } func TestNewStore_SaveThenReload(t *testing.T) { dir := t.TempDir() s1, err := NewStore(dir, nil) if err != nil { t.Fatalf("NewStore s1: %v", err) } newCfg := s1.Get() newCfg.Relay.URL = "wss://custom.relay.example.com/tunnel" newCfg.Relay.Reconnect = ReconnectManual newCfg.Behavior.Autostart = true newCfg.Log.Level = LogLevelDebug if err := s1.Save(newCfg); err != nil { t.Fatalf("Save: %v", err) } // Reopen → 值還在 s2, err := NewStore(dir, nil) if err != nil { t.Fatalf("NewStore s2: %v", err) } got := s2.Get() if got.Relay.URL != newCfg.Relay.URL { t.Errorf("Relay.URL = %q; want %q", got.Relay.URL, newCfg.Relay.URL) } if got.Relay.Reconnect != ReconnectManual { t.Errorf("Relay.Reconnect = %q; want manual", got.Relay.Reconnect) } if !got.Behavior.Autostart { t.Error("Behavior.Autostart = false; want true") } if got.Log.Level != LogLevelDebug { t.Errorf("Log.Level = %q; want debug", got.Log.Level) } } func TestNewStore_CorruptYAMLFallsBackToDefaults(t *testing.T) { dir := t.TempDir() // 先寫一個壞 YAML 檔 path := filepath.Join(dir, "config.yaml") if err := os.WriteFile(path, []byte("relay:\n url: [this is not valid yaml"), 0o600); err != nil { t.Fatalf("write corrupt: %v", err) } s, err := NewStore(dir, nil) if err != nil { t.Fatalf("NewStore should not error on corrupt file; got %v", err) } if s.Get().Relay.URL != DefaultRelayURL { t.Errorf("corrupt file should fall back to defaults; got Relay.URL=%q", s.Get().Relay.URL) } } func TestNewStore_InvalidValuesFallBackToDefaults(t *testing.T) { dir := t.TempDir() // 合法 YAML 但值非法 content := `version: 1 relay: url: http://bad.example.com reconnect: maybe behavior: autostart: false log: level: trace ` path := filepath.Join(dir, "config.yaml") if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("write: %v", err) } s, err := NewStore(dir, nil) if err != nil { t.Fatalf("NewStore: %v", err) } got := s.Get() if got.Relay.URL != DefaultRelayURL || got.Log.Level != DefaultLogLevel { t.Errorf("invalid values should fall back to defaults; got %+v", got) } } func TestSave_RejectsInvalidCfg(t *testing.T) { dir := t.TempDir() s, err := NewStore(dir, nil) if err != nil { t.Fatalf("NewStore: %v", err) } before := s.Get() bad := Default() bad.Relay.URL = "http://no-websocket-scheme.example.com" if err := s.Save(bad); err == nil { t.Error("Save should reject invalid URL") } // 內部狀態不該被改動 if got := s.Get(); got.Relay.URL != before.Relay.URL { t.Errorf("invalid Save should not mutate internal state; got URL=%q", got.Relay.URL) } } func TestNewStore_PartialYAMLFilled(t *testing.T) { dir := t.TempDir() // 只寫 relay.url,其他欄位缺失 content := `relay: url: wss://partial.example.com/tunnel ` path := filepath.Join(dir, "config.yaml") if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("write: %v", err) } s, err := NewStore(dir, nil) if err != nil { t.Fatalf("NewStore: %v", err) } got := s.Get() if got.Relay.URL != "wss://partial.example.com/tunnel" { t.Errorf("should preserve user URL; got %q", got.Relay.URL) } if got.Relay.Reconnect != DefaultReconnectMode { t.Errorf("should fill missing Reconnect; got %q", got.Relay.Reconnect) } if got.Log.Level != DefaultLogLevel { t.Errorf("should fill missing Log.Level; got %q", got.Log.Level) } } func TestNewStore_RequiresDataDir(t *testing.T) { if _, err := NewStore("", nil); err == nil { t.Error("NewStore(\"\") should error") } }