Files
Cyrene_v2/internal/config/runtimeconfig/memorytidy_save_test.go
T
AskaEth e9e7b7dcb2 test(memory): memory-tidy.json 写路径(补丁保留其它键 / 值没变不落盘 / 坏文件拒写 / 原子写)
塔批准的 scope 里点名了 internal/config/runtimeconfig/memorytidy_save_test.go,
补上这四个口径的包内单测(网关侧另有端到端用例覆盖三态与审计)。
2026-10-13 21:59:18 +08:00

152 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runtimeconfig
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"runtime"
"testing"
)
// 本文件锁定 `memory-tidy.json` **写路径**(PatchMemoryTidyTuning)的四条口径:
// ① 补丁语义:只改提交的键,盘上其它键(t1Judge 等)**原样保留**;
// ② 值没变 ⇒ 一个字节都不写(连 mtime 都不动);
// ③ 盘上那份**读不走**时拒绝覆盖(ErrInvalidMemoryTidy),且**盘上不动**;
// ④ 原子写:temp + rename,成功之后不留 `.tmp`,文件权限 0600(Windows 不校验)。
// patchTuning 是测试用的补丁构造:键 → 原样 JSON 值。
func patchTuning(t *testing.T, pairs map[string]any) map[string]json.RawMessage {
t.Helper()
out := make(map[string]json.RawMessage, len(pairs))
for k, v := range pairs {
raw, err := json.Marshal(v)
if err != nil {
t.Fatalf("marshal %s: %v", k, err)
}
out[k] = raw
}
return out
}
// TestPatchMemoryTidyTuning_MergesAndKeepsOtherKeys 补丁只动提交的键,其余键原样保留;
// 返回值是**重新读盘**的结果(真值),不是「我以为写成了什么」。
func TestPatchMemoryTidyTuning_MergesAndKeepsOtherKeys(t *testing.T) {
t.Parallel()
path := writeMemoryTidyFile(t, `{"t1Judge": false, "t1PerWindow": 120, "intervalSec": 3600}`)
tuning, wrote, err := PatchMemoryTidyTuning(path, patchTuning(t, map[string]any{"intervalSec": 7200}), nil)
if err != nil {
t.Fatalf("patch: %v", err)
}
if !wrote {
t.Fatal("值变了却没写盘")
}
if tuning.IntervalSec == nil || *tuning.IntervalSec != 7200 {
t.Fatalf("返回值不是读回的真值:%+v", tuning.IntervalSec)
}
if tuning.T1Judge == nil || *tuning.T1Judge {
t.Fatalf("没提交的 t1Judge 被动了:%+v", tuning.T1Judge)
}
doc := map[string]json.RawMessage{}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read: %v", err)
}
if err := json.Unmarshal(raw, &doc); err != nil {
t.Fatalf("decode: %v", err)
}
if string(doc["intervalSec"]) != "7200" || string(doc["t1PerWindow"]) != "120" {
t.Fatalf("盘上内容 = %s", raw)
}
if string(doc["t1Judge"]) != "false" {
t.Fatalf("不受管的 t1Judge 被抹掉了:%s", raw)
}
// 清除哨兵:clear 里的键从盘上消失(= 回到下一档兜底)。
if _, _, err := PatchMemoryTidyTuning(path, nil, []string{"intervalSec"}); err != nil {
t.Fatalf("clear: %v", err)
}
doc = map[string]json.RawMessage{}
raw, _ = os.ReadFile(path)
if err := json.Unmarshal(raw, &doc); err != nil {
t.Fatalf("decode: %v", err)
}
if _, ok := doc["intervalSec"]; ok {
t.Fatalf("clear 之后盘上仍有 intervalSec:%s", raw)
}
if _, ok := doc["t1Judge"]; !ok {
t.Fatalf("clear 抹掉了别的键:%s", raw)
}
}
// TestPatchMemoryTidyTuning_NoChangeWritesNothing 值没变 ⇒ 不写盘、不重排格式。
func TestPatchMemoryTidyTuning_NoChangeWritesNothing(t *testing.T) {
t.Parallel()
path := writeMemoryTidyFile(t, `{"intervalSec": 3600}`)
before, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read: %v", err)
}
tuning, wrote, err := PatchMemoryTidyTuning(path, patchTuning(t, map[string]any{"intervalSec": 3600}), nil)
if err != nil {
t.Fatalf("patch: %v", err)
}
if wrote {
t.Fatal("值没变却写了盘")
}
after, _ := os.ReadFile(path)
if string(after) != string(before) {
t.Fatalf("值没变却改了盘上内容:\nbefore=%s\nafter=%s", before, after)
}
if tuning.IntervalSec == nil || *tuning.IntervalSec != 3600 {
t.Fatalf("没写盘也要回读真值:%+v", tuning.IntervalSec)
}
}
// TestPatchMemoryTidyTuning_RefusesUnreadableFile 盘上那份运行期读不走时拒绝覆盖:
// 返回 ErrInvalidMemoryTidy(调用方据此回 409),且**盘上一个字节都不动**。
func TestPatchMemoryTidyTuning_RefusesUnreadableFile(t *testing.T) {
t.Parallel()
for _, body := range []string{`{"intervalSec": "3600"}`, `not json at all`, `[1, 2]`} {
body := body
t.Run(body, func(t *testing.T) {
path := writeMemoryTidyFile(t, body)
_, wrote, err := PatchMemoryTidyTuning(path, patchTuning(t, map[string]any{"intervalSec": 3600}), nil)
if !errors.Is(err, ErrInvalidMemoryTidy) {
t.Fatalf("err = %v, want ErrInvalidMemoryTidy", err)
}
if wrote {
t.Fatal("读不走的文件被写入了")
}
raw, _ := os.ReadFile(path)
if string(raw) != body {
t.Fatalf("坏文件被改动了:%s", raw)
}
})
}
}
// TestPatchMemoryTidyTuning_CreatesMissingFileAtomically 文件缺失 ⇒ 建目录并原子写出
// (temp + rename):成功之后不留 `.tmp`,权限 0600(Windows 不校验)。
func TestPatchMemoryTidyTuning_CreatesMissingFileAtomically(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "config", memoryTidyFileName)
tuning, wrote, err := PatchMemoryTidyTuning(path, patchTuning(t, map[string]any{"tempTtlSec": 604800}), nil)
if err != nil {
t.Fatalf("patch: %v", err)
}
if !wrote {
t.Fatal("缺失文件时应当写出")
}
if tuning.TempTtlSec == nil || *tuning.TempTtlSec != 604800 {
t.Fatalf("回读真值 = %+v", tuning.TempTtlSec)
}
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
t.Fatalf("原子写留下了临时文件:err=%v", err)
}
if info, err := os.Stat(path); err != nil {
t.Fatalf("stat: %v", err)
} else if info.Mode().Perm() != memoryTidySaveFileMode && runtime.GOOS != "windows" {
t.Fatalf("文件权限 = %v, want %v", info.Mode().Perm(), memoryTidySaveFileMode)
}
}