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