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) } } }