package runtimeconfig import ( "errors" "os" "path/filepath" "testing" ) // 本文件守 design/12 §N C.5 的**只读加载件**:落点是 // `$DATA_DIR/config/backthink.json`(零新 env 键),文件缺失 = 还没配过(零值 + nil, // 不是错误),存在但解不开 = 返回错(调用方记 WARN 并回落默认,绝不静默变成「无上限」)。 // TestDefaultBackthinkPath 钉住落点:`/config/backthink.json`。 // 它同时守住「新家族从一开始就只写 config/」(design/13 §2.1 第 5 条)—— // 路径里少一段 config 就是静默换家。 func TestDefaultBackthinkPath(t *testing.T) { t.Parallel() got := DefaultBackthinkPath(filepath.Join("root", "data")) want := filepath.Join("root", "data", "config", "backthink.json") if got != want { t.Fatalf("DefaultBackthinkPath = %q, want %q", got, want) } } // TestLoadBackgroundTuning_MissingFileIsNotAnError 钉住「没配过」不是错误: // 返回零值 + nil,由服务侧 normalize 回落代码默认(4 轮/小时)。 func TestLoadBackgroundTuning_MissingFileIsNotAnError(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "config", "backthink.json") got, err := LoadBackgroundTuning(path) if err != nil { t.Fatalf("缺失文件应返回 nil error,得到 %v", err) } if got.SelfWakePerWindow != 0 || got.SelfWakeWindowSec != 0 { t.Fatalf("缺失文件应返回零值,得到 %+v", got) } // 它**不建目录**:读路径不许留下副作用(新家族没有老位置,也没什么要建的)。 if _, statErr := os.Stat(filepath.Dir(path)); !os.IsNotExist(statErr) { t.Fatalf("读路径建了目录或留下了别的痕迹:stat err = %v", statErr) } } // TestLoadBackgroundTuning_EmptyFileIsNotConfigured 钉住「全空白 = 还没配过」, // 与 plugins.json / web-search.json 的既有口径逐字一致。 func TestLoadBackgroundTuning_EmptyFileIsNotConfigured(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "backthink.json") if err := os.WriteFile(path, []byte(" \n\t\n"), 0o600); err != nil { t.Fatalf("写测试文件: %v", err) } got, err := LoadBackgroundTuning(path) if err != nil { t.Fatalf("空白文件应返回 nil error,得到 %v", err) } if got != (BackgroundTuning{}) { t.Fatalf("空白文件应返回零值,得到 %+v", got) } } // TestLoadBackgroundTuning_ReadsSpecShape 钉住规格里给的形状逐字可读 // (design/12 §N C.5 的 JSON 例子),且原样交出**未**回落默认的原始数字 // —— 回落是服务侧 normalize 的事,这里不做第二套默认值。 func TestLoadBackgroundTuning_ReadsSpecShape(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "backthink.json") body := `{ "selfWakePerWindow": 2, "selfWakeWindowSec": 1800 }` if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatalf("写测试文件: %v", err) } got, err := LoadBackgroundTuning(path) if err != nil { t.Fatalf("LoadBackgroundTuning: %v", err) } if got.SelfWakePerWindow != 2 || got.SelfWakeWindowSec != 1800 { t.Fatalf("tuning = %+v, want {2 1800}", got) } } // TestLoadBackgroundTuning_ZeroStaysZero 钉住「配成 0」原样交出去(不在这里改写成默认): // 服务侧的 normalize 才是「≤0 回落默认」的唯一执行点,本层若也回落,就等于有了 // 两套默认值真源。 func TestLoadBackgroundTuning_ZeroStaysZero(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "backthink.json") if err := os.WriteFile(path, []byte(`{ "selfWakePerWindow": 0, "selfWakeWindowSec": -1 }`), 0o600); err != nil { t.Fatalf("写测试文件: %v", err) } got, err := LoadBackgroundTuning(path) if err != nil { t.Fatalf("LoadBackgroundTuning: %v", err) } if got.SelfWakePerWindow != 0 || got.SelfWakeWindowSec != -1 { t.Fatalf("tuning = %+v, want 原样(0 与 -1 都不在本地改写)", got) } } // TestLoadBackgroundTuning_BrokenFileReturnsError 钉住坏文件**必须**报错: // 调用方据此记 WARN 并回落默认。若这里静默返回零值,一个写坏的文件就会安静地 // 变成「用默认值」,与「如实告知」的红线相反。 func TestLoadBackgroundTuning_BrokenFileReturnsError(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "backthink.json") if err := os.WriteFile(path, []byte(`{ "selfWakePerWindow": `), 0o600); err != nil { t.Fatalf("写测试文件: %v", err) } if _, err := LoadBackgroundTuning(path); !errors.Is(err, ErrInvalidBackthink) { t.Fatalf("err = %v, want ErrInvalidBackthink", err) } }