tempTtlSec 落地后,`memory-tidy.json` 的播种契约就是**十一键**,而三处文件的注释仍 写「十键」。这些是**过时的陈述**(不是风格问题):下一位读者会从「快照契约:十键非 nil」里读出一个错误的口径,而 `memoryTidySeedComplete` 实际校验的正是十一键。 改的只是措辞,一行逻辑没动: - internal/config/runtimeconfig/memorytidy_seed.go:25,160 - internal/config/runtimeconfig/memorytidy_seed_test.go:19,27,62,82,97,214 - internal/runtime/memory/tidy_snapshot_test.go:14,27,159 (`cmd/cyrene/wire_memory_tidy.go:65` 还有同类的一处「这十键」,该文件属 M3 的 scope,未动 —— 塔已转 M3。) 验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿。
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)
|
||
}
|
||
}
|
||
}
|