Files
Cyrene_v2/internal/runtime/memory/tidy_snapshot_test.go
T
AskaEth 23b6e3fe1e
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(memory): 启动时自动播种面板配置文件(缺失才写、绝不覆盖)
主人 2026-10-07:「后端自动创建独立的面板配置文件要做」。

- 启动路径(wireMemoryTidy → applyMemoryTidyTuning 之后)若
  $DATA_DIR/config/memory-tidy.json 不存在或全空白 => 写一份「当时生效值」
  (十键:既有六键 + 频率四键),此后面板接管
- 已存在(非空)=> 跳过,该分支没有任何写盘语句;损坏 => WARN + 按 .env 跑、
  一个字不写盘(不「修好它」)
- 幂等(第二次启动必落到跳过分支);原子写(tmp 0600 + rename)
- 落盘前完整性闸:缺键 / 非正周期 / 钟点越界 => WARN + 不落盘
- 值来自 TidyTuningSnapshot(先跑两个归一化再翻回正向键)=> 播种那一刻运行
  行为零变化,有强断言(归一化后配置逐字相等)
- 零新增依赖、零 env 键变更;5 个变异(含「让已存在走写盘」=> 红)
2026-10-07 20:38:37 +08:00

197 lines
9.6 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 memory
import (
"testing"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
)
// 本文件锁定**启动播种的值来源**(tidy_snapshot.go;主人 2026-10-07:「`.env` 只做
// 兜底用 —— 后端**自动创建**独立的面板配置文件**要做**」):
//
// ① 快照十键齐全、值可用(否则 runtimeconfig.SeedMemoryTidy 会拒绝落盘);
// ② 值取「**当时解析出来的生效值**」—— `.env` 给了就用 `.env`,没给就用代码默认
// (默认值来自 normalizeTidyConfig / normalizeReviewConfig,不另写一份);
// ③ 最关键的一条:把快照当成面板值叠回去,**归一化结果与「没有面板文件」逐字相同**
// —— 这就是「播种那一刻行为零变化,之后改文件才生效」。
// snapBoolPtr / snapIntPtr 造「快照里显式写了这个键」的指针。
// (不复用 tidy_tuning_test.go 的 intPtr:那是给**面板**用例造的,两者语义不同,
// 分开命名便于读。)
func snapBoolPtr(v bool) *bool { return &v }
func snapIntPtr(v int) *int { return &v }
// TestTidyTuningSnapshot_CodeDefaults 钉住「什么都没配」时的十键 = 归一化的代码默认值。
// 期望值直接取自默认值常量(本包唯一那处定义),因此这条用例证明的是**接线**:
// 快照确实走了 normalizeTidyConfig / normalizeReviewConfig,而不是另写一份默认值。
func TestTidyTuningSnapshot_CodeDefaults(t *testing.T) {
t.Parallel()
snap := TidyTuningSnapshot(TidyConfig{}, ReviewConfig{})
if snap.AutoApply == nil || !*snap.AutoApply {
t.Fatalf("autoApply = %v, want 显式的 true(代码默认开)", snap.AutoApply)
}
if snap.IngestGate == nil || !*snap.IngestGate {
t.Fatalf("ingestGate = %v, want 显式的 true(代码默认开)", snap.IngestGate)
}
if snap.T1Judge == nil || !*snap.T1Judge {
t.Fatalf("t1Judge = %v, want 显式的 true(代码默认开)", snap.T1Judge)
}
if snap.AutoApplyPerRound != defaultTidyAutoApplyPerRound {
t.Fatalf("autoApplyPerRound = %d, want %d", snap.AutoApplyPerRound, defaultTidyAutoApplyPerRound)
}
if snap.T1PerWindow != defaultT1PerWindow || snap.T1WindowSec != int(defaultT1Window.Seconds()) {
t.Fatalf("T1 限速 = %d/%d, want %d/%d", snap.T1PerWindow, snap.T1WindowSec,
defaultT1PerWindow, int(defaultT1Window.Seconds()))
}
if snap.IntervalSec == nil || *snap.IntervalSec != defaultTidyIntervalSec {
t.Fatalf("intervalSec = %v, want %d", snap.IntervalSec, defaultTidyIntervalSec)
}
// quietIntervalSec 缺省 = intervalSec(normalizeTidyConfig 的既有口径:不因时段换档)。
if snap.QuietIntervalSec == nil || *snap.QuietIntervalSec != defaultTidyIntervalSec {
t.Fatalf("quietIntervalSec = %v, want = intervalSec (%d)", snap.QuietIntervalSec, defaultTidyIntervalSec)
}
if snap.QuietStartHour == nil || *snap.QuietStartHour != defaultTidyQuietStartHour ||
snap.QuietEndHour == nil || *snap.QuietEndHour != defaultTidyQuietEndHour {
t.Fatalf("安静时段 = %v-%v, want %d-%d", snap.QuietStartHour, snap.QuietEndHour,
defaultTidyQuietStartHour, defaultTidyQuietEndHour)
}
}
// TestTidyTuningSnapshot_TakesEnvLayer 钉住「`.env` 给的就用 `.env`」:四键的 env 档
// (MEMORY_TIDY_INTERVAL_SEC / MEMORY_TIDY_QUIET_*_HOUR)必须原样进快照,否则播种写
// 下去的就是代码默认值,等于把主人配的 `.env` 静默抹平。
func TestTidyTuningSnapshot_TakesEnvLayer(t *testing.T) {
t.Parallel()
env := config.MemoryTidyConfig{IntervalSec: 7200, QuietStartHour: 22, QuietEndHour: 6}
snap := TidyTuningSnapshot(TidyConfigFromConfig(env), ReviewConfig{})
if snap.IntervalSec == nil || *snap.IntervalSec != 7200 {
t.Fatalf("intervalSec = %v, want 7200(取 `.env`)", snap.IntervalSec)
}
// env 只给了 intervalSec,quietIntervalSec 没有 env 键 ⇒ 缺省 = intervalSec。
if snap.QuietIntervalSec == nil || *snap.QuietIntervalSec != 7200 {
t.Fatalf("quietIntervalSec = %v, want 7200(缺省 = intervalSec)", snap.QuietIntervalSec)
}
if snap.QuietStartHour == nil || *snap.QuietStartHour != 22 ||
snap.QuietEndHour == nil || *snap.QuietEndHour != 6 {
t.Fatalf("安静时段 = %v-%v, want 22-6(取 `.env`)", snap.QuietStartHour, snap.QuietEndHour)
}
}
// TestTidyTuningSnapshot_KeepsExplicitZeroHour 钉住钟点的零值口径:**0 是合法钟点**
// (0-7 的凌晨窗口)。若快照把「0 起」当成「没配」而写成 23,播种就篡改了主人的窗口。
//
// 注意「两端都是 0」是**另一回事**:那是 normalize 认的「都没配」(回落到 23-07),
// 这与显式写 0-5 的凌晨窗口必须可区分。
func TestTidyTuningSnapshot_KeepsExplicitZeroHour(t *testing.T) {
t.Parallel()
env := config.MemoryTidyConfig{QuietStartHour: 0, QuietEndHour: 5}
snap := TidyTuningSnapshot(TidyConfigFromConfig(env), ReviewConfig{})
if snap.QuietStartHour == nil || *snap.QuietStartHour != 0 || *snap.QuietEndHour != 5 {
t.Fatalf("安静时段 = %v-%v, want 0-5(显式 0 是合法钟点)", snap.QuietStartHour, snap.QuietEndHour)
}
// 两端都是 0 = 「都没配」⇒ 归一成代码默认 23-07(既有口径,快照如实转述)。
both := TidyTuningSnapshot(TidyConfigFromConfig(config.MemoryTidyConfig{}), ReviewConfig{})
if *both.QuietStartHour != defaultTidyQuietStartHour || *both.QuietEndHour != defaultTidyQuietEndHour {
t.Fatalf("两端 0 = 都没配 ⇒ 应归一成 %d-%d,得到 %v-%v",
defaultTidyQuietStartHour, defaultTidyQuietEndHour, *both.QuietStartHour, *both.QuietEndHour)
}
}
// TestTidyTuningSnapshot_PanelRoundTripKeepsEffectiveConfig 是**播种的全部意义**所在:
// 把快照当成面板值叠回去之后,整理与审查两侧归一化出来的配置必须与「面板文件不存在」
// 逐字相同 —— 于是播种那一刻行为零变化,之后改文件才生效。
//
// 两侧各按**运行期真实的叠加路径**走:
// - 整理侧 = 组合根那三步(TidyConfigFromConfig → ApplyTidyScheduleTuning →
// normalizeTidyConfig,即 resolveTidySchedule);
// - 审查侧 = wireMemoryReview 的写法(开关取 IngestGateOff/T1JudgeOff,数值取
// t1PerWindow/t1WindowSec,再交给 normalizeReviewConfig)。
func TestTidyTuningSnapshot_PanelRoundTripKeepsEffectiveConfig(t *testing.T) {
t.Parallel()
envs := []struct {
name string
env config.MemoryTidyConfig
}{
{"`.env` 什么都没配", config.MemoryTidyConfig{}},
{"`.env` 配了频率与窗口", config.MemoryTidyConfig{IntervalSec: 7200, QuietStartHour: 1, QuietEndHour: 5}},
{"`.env` 的窗口从 0 点起(凌晨档)", config.MemoryTidyConfig{IntervalSec: 10800, QuietStartHour: 0, QuietEndHour: 6}},
{"`.env` 把窗口关掉(两端相等且非 0)", config.MemoryTidyConfig{IntervalSec: 3600, QuietStartHour: 5, QuietEndHour: 5}},
{"`.env` 只配了周期", config.MemoryTidyConfig{IntervalSec: 1800}},
}
for _, tc := range envs {
log, _ := captureTidyLog(t)
// ① 没播种前:面板缺失 ⇒ 零值 tuning(= 当前生产里 data/config 下没这个文件的形态)。
before := resolveTidySchedule(tc.env, runtimeconfig.MemoryTidyTuning{}, log)
// ② 快照(这就是要写进面板文件的内容)。
snap := TidyTuningSnapshot(TidyConfigFromConfig(tc.env), ReviewConfig{})
// ③ 播种之后:面板文件就是那份快照,逐键叠回来。
after := resolveTidySchedule(tc.env, snap, log)
if before != after {
t.Fatalf("%s: 播种改变了生效配置:\n before=%+v\n after =%+v", tc.name, before, after)
}
}
// 审查侧同理(闸/T1 四键在 `.env` 里没有出处,只由面板与代码默认决定)。
reviewBefore := normalizeReviewConfig(ReviewConfig{})
snap := TidyTuningSnapshot(TidyConfig{}, ReviewConfig{})
reviewAfter := normalizeReviewConfig(ReviewConfig{
DisableIngestGate: snap.IngestGateOff(),
DisableT1: snap.T1JudgeOff(),
T1PerWindow: snap.T1PerWindow,
T1Window: time.Duration(snap.T1WindowSec) * time.Second,
})
if reviewBefore != reviewAfter {
t.Fatalf("播种改变了审查的生效配置:\n before=%+v\n after =%+v", reviewBefore, reviewAfter)
}
}
// TestTidyTuningSnapshot_IsCompleteAndPositive 钉住快照契约:十键非 nil、数值键为正、
// 钟点在 0..23。runtimeconfig.SeedMemoryTidy 会按这条契约拒收残缺种子,因此这里先
// 在**产生方**把它钉住,免得「播种写不出去」要到启动日志里才发现。
func TestTidyTuningSnapshot_IsCompleteAndPositive(t *testing.T) {
t.Parallel()
snap := TidyTuningSnapshot(TidyConfigFromConfig(config.MemoryTidyConfig{}), ReviewConfig{})
pointers := map[string]*int{
"intervalSec": snap.IntervalSec,
"quietIntervalSec": snap.QuietIntervalSec,
"quietStartHour": snap.QuietStartHour,
"quietEndHour": snap.QuietEndHour,
}
for key, p := range pointers {
if p == nil {
t.Fatalf("%s 是 nil(缺键的种子会被拒收)", key)
}
}
if snap.AutoApply == nil || snap.IngestGate == nil || snap.T1Judge == nil {
t.Fatalf("开关指针不得为 nil:%+v", snap)
}
for key, v := range map[string]int{
"autoApplyPerRound": snap.AutoApplyPerRound,
"t1PerWindow": snap.T1PerWindow,
"t1WindowSec": snap.T1WindowSec,
"intervalSec": *snap.IntervalSec,
"quietIntervalSec": *snap.QuietIntervalSec,
} {
if v <= 0 {
t.Fatalf("%s = %d, want > 0(非正周期会被 runtimeconfig 拒收)", key, v)
}
}
for key, v := range map[string]int{"quietStartHour": *snap.QuietStartHour, "quietEndHour": *snap.QuietEndHour} {
if v < 0 || v > 23 {
t.Fatalf("%s = %d, want 0..23", key, v)
}
}
}