From 27ee0a81491be9dc6c47980b33e088128e7836ab Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 13 Oct 2026 21:34:45 +0800 Subject: [PATCH] =?UTF-8?q?test(memory):=20=E6=8B=86=E5=88=86=E8=B0=83?= =?UTF-8?q?=E4=BC=98=E5=86=99=E7=AB=AF=E7=82=B9=E6=B5=8B=E8=AF=95=EF=BC=88?= =?UTF-8?q?=E5=8E=9F=E6=96=87=E4=BB=B6=20524=20=E8=A1=8C=20>=20AGENTS=20?= =?UTF-8?q?=C2=A710=20=E7=9A=84=20500=20=E8=A1=8C=E4=B8=8A=E9=99=90?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 守卫用例(非法值不落盘 / 坏文件不覆盖 / 同值不重写 / 热生效两种结果)移到 admin_memory_tuning_guard_test.go,helper 与读侧用例留在 admin_memory_tuning_test.go; 口径不变,两个文件都 ≤300 行。 --- .../gateway/admin_memory_tuning_guard_test.go | 263 ++++++++++++++++++ .../gateway/admin_memory_tuning_test.go | 245 ---------------- 2 files changed, 263 insertions(+), 245 deletions(-) create mode 100644 internal/services/gateway/admin_memory_tuning_guard_test.go diff --git a/internal/services/gateway/admin_memory_tuning_guard_test.go b/internal/services/gateway/admin_memory_tuning_guard_test.go new file mode 100644 index 0000000..c5e9e3e --- /dev/null +++ b/internal/services/gateway/admin_memory_tuning_guard_test.go @@ -0,0 +1,263 @@ +package gateway + +import ( + "context" + "errors" + "net/http" + "os" + "testing" + "time" + + "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" + "git.yeij.top/AskaEth/Cyrene_v2/internal/config" + "git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig" + "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" +) + +// 本文件是记忆整理「面板调优」写端点的**守卫用例**(helper 与读侧用例见 +// admin_memory_tuning_test.go):三态里最要紧的那两条(非法值绝不落盘、坏文件绝不 +// 覆盖)与热生效的两种结果(成功 / 如实报失败)都在这里逐条钉死。 +// +// 拆成两个文件只为守 AGENTS §10 的文件长度上限,口径与另一半完全同源。 + +// TestAdminMemoryTidyTuningWrite_InvalidValueNeverTouchesDisk 三态之三(本文件的核心 +// 证据):任何非法输入都 `400 invalid_payload`,且**盘上一个字节都不动**。 +func TestAdminMemoryTidyTuningWrite_InvalidValueNeverTouchesDisk(t *testing.T) { + t.Parallel() + cases := []struct{ name, body string }{ + {"字符串冒充整数", `{"intervalSec": "3600"}`}, + {"小数不是整数", `{"tempTtlSec": 1.5}`}, + {"周期为 0(会让整理空转)", `{"intervalSec": 0}`}, + {"周期为负", `{"quietIntervalSec": -60}`}, + {"钟点越上界", `{"quietStartHour": 24}`}, + {"钟点越下界", `{"quietEndHour": -1}`}, + {"钟点不是数字", `{"quietEndHour": "07"}`}, + {"开关不是布尔", `{"ingestGate": "yes"}`}, + {"未知键(拼错的 quietEnd)", `{"quietEnd": 7}`}, + {"请求体不是对象", `[1, 2]`}, + {"请求体是 null", `null`}, + {"请求体是空串", ``}, + {"合法键与非法键同时提交", `{"intervalSec": 3600, "autoApply": "on"}`}, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + env := newTuneEnv(t, Deps{}) + before := env.seed(t, `{ + "intervalSec": 86400, + "quietStartHour": 23, + "quietEndHour": 7, + "t1Judge": false +}`) + resp, body := env.put(t, env.token(t, auth.RoleOwner), tc.body) + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("status = %d, want 400 (body=%s)", resp.StatusCode, body) + } + assertErrorCode(t, body, "invalid_payload") + if after := env.bytes(t); string(after) != string(before) { + t.Fatalf("非法输入改动了盘上内容:\nbefore=%s\nafter=%s", before, after) + } + }) + } + // 盘上**没有**文件时,非法输入也不得创建它(「先落盘再校验」同样是落盘)。 + t.Run("文件不存在时也不创建", func(t *testing.T) { + env := newTuneEnv(t, Deps{}) + resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"intervalSec": 0}`) + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("status = %d, want 400 (body=%s)", resp.StatusCode, body) + } + if _, err := os.Stat(env.path()); !os.IsNotExist(err) { + t.Fatalf("非法输入创建了文件:err=%v", err) + } + }) +} + +// TestAdminMemoryTidyTuningWrite_NullClearsKey 清除哨兵:`null` 删掉该键(回到下一档 +// 兜底),同文件其它键不受影响。 +func TestAdminMemoryTidyTuningWrite_NullClearsKey(t *testing.T) { + t.Parallel() + env := newTuneEnv(t, Deps{}) + env.seed(t, `{"intervalSec": 3600, "quietIntervalSec": 7200, "t1Judge": false}`) + resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"intervalSec": null}`) + if resp.StatusCode != http.StatusOK { + t.Fatalf("clear status = %d, want 200 (body=%s)", resp.StatusCode, body) + } + view := env.view(t, body) + if _, ok := view.Values["intervalSec"]; ok { + t.Fatalf("清除后不该再回显 intervalSec:%#v", view.Values) + } + if got := view.Values["quietIntervalSec"]; got != float64(7200) { + t.Fatalf("清除一个键影响了别的键:%#v", view.Values) + } + doc := env.diskDoc(t) + if _, ok := doc["intervalSec"]; ok { + t.Fatalf("盘上仍有 intervalSec:%s", string(env.bytes(t))) + } + if _, ok := doc["t1Judge"]; !ok { + t.Fatalf("清除一个键抹掉了别的键:%s", string(env.bytes(t))) + } +} + +// TestAdminMemoryTidyTuningWrite_RefusesCorruptFile 盘上那份运行期读不走时,**读与写 +// 都**拒绝(`409 tuning_corrupt`)且绝不覆盖 —— 否则面板会说「已保存」而运行期仍在 +// 整体回落 `.env` 并记 WARN。 +func TestAdminMemoryTidyTuningWrite_RefusesCorruptFile(t *testing.T) { + t.Parallel() + for _, tc := range []struct{ name, body string }{ + {"类型写错", `{"intervalSec": "3600"}`}, + {"根本不是 JSON", `not json at all`}, + {"顶层不是对象", `[1, 2, 3]`}, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + env := newTuneEnv(t, Deps{}) + before := env.seed(t, tc.body) + tok := env.token(t, auth.RoleOwner) + resp, body := env.put(t, tok, `{"intervalSec": 3600}`) + if resp.StatusCode != http.StatusConflict { + t.Fatalf("write status = %d, want 409 (body=%s)", resp.StatusCode, body) + } + assertErrorCode(t, body, "tuning_corrupt") + if string(env.bytes(t)) != string(before) { + t.Fatalf("坏文件被覆盖了:%s", string(env.bytes(t))) + } + // 读侧同样如实报坏,而不是回一份空配置假装「没配过」。 + rresp, rbody := env.get(t, tok) + if rresp.StatusCode != http.StatusConflict { + t.Fatalf("read status = %d, want 409 (body=%s)", rresp.StatusCode, rbody) + } + assertErrorCode(t, rbody, "tuning_corrupt") + }) + } +} + +// TestAdminMemoryTidyTuningWrite_SameValueDoesNotRewrite 「保存同样的值」不该在盘上留 +// 一次无意义的改写(内容逐字节相同 ⇒ 不落盘、mtime 不动)。 +func TestAdminMemoryTidyTuningWrite_SameValueDoesNotRewrite(t *testing.T) { + t.Parallel() + env := newTuneEnv(t, Deps{}) + env.seed(t, `{"intervalSec": 3600}`) + past := time.Now().Add(-time.Hour) + if err := os.Chtimes(env.path(), past, past); err != nil { + t.Fatalf("chtimes: %v", err) + } + resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"intervalSec": 3600}`) + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body) + } + info, err := os.Stat(env.path()) + if err != nil { + t.Fatalf("stat: %v", err) + } + if !info.ModTime().Equal(past) { + t.Fatalf("同值保存重写了文件(mtime %v ⇒ %v)", past, info.ModTime()) + } + if string(env.bytes(t)) != `{"intervalSec": 3600}` { + t.Fatalf("盘上内容变了:%s", string(env.bytes(t))) + } +} + +// fakeTuningTidy 既是整理服务的只读出口,也是「可热生效」的实现(记录收到的值)。 +type fakeTuningTidy struct { + fakeTidyDryRunner + gotBase *memory.TidyConfig + gotTuning *runtimeconfig.MemoryTidyTuning + err error +} + +func (f *fakeTuningTidy) ApplyPanelTuning(base memory.TidyConfig, panel runtimeconfig.MemoryTidyTuning) error { + f.gotBase = &base + f.gotTuning = &panel + return f.err +} + +// fakeTuningMemories 同时实现 MemoryStore 与「入库侧可热生效」。 +type fakeTuningMemories struct { + got *runtimeconfig.MemoryTidyTuning + err error +} + +func (f *fakeTuningMemories) List(context.Context) ([]memory.MemoryItem, error) { + return []memory.MemoryItem{}, nil +} + +func (f *fakeTuningMemories) ApplyIngestPanelTuning(panel runtimeconfig.MemoryTidyTuning) error { + f.got = &panel + return f.err +} + +// TestAdminMemoryTidyTuningWrite_HotAppliesAndAudits 接线了热生效实现时:保存后当场把 +// **新值**交给运行期(hot=true),并落一条审计 `memory.tidy.tuning`/ok。 +func TestAdminMemoryTidyTuningWrite_HotAppliesAndAudits(t *testing.T) { + t.Parallel() + tidy := &fakeTuningTidy{} + mems := &fakeTuningMemories{} + env := newTuneEnv(t, Deps{MemoryTidy: tidy, Memories: mems}) + env.seed(t, `{"intervalSec": 86400}`) + resp, body := env.put(t, env.token(t, auth.RoleAdmin), `{"intervalSec": 10800, "tempTtlSec": 259200}`) + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body) + } + view := env.view(t, body) + if !view.Hot || view.HotError != "" { + t.Fatalf("hot/hotError = %v/%q, want true/空", view.Hot, view.HotError) + } + if tidy.gotTuning == nil || tidy.gotTuning.IntervalSec == nil || *tidy.gotTuning.IntervalSec != 10800 { + t.Fatalf("整理档没收到面板值:%#v", tidy.gotTuning) + } + if mems.got == nil || mems.got.TempTtlSec == nil || *mems.got.TempTtlSec != 259200 { + t.Fatalf("入库侧没收到面板值:%#v", mems.got) + } + // base 是 `.env` 层(进程生效配置投影),不是面板值本身。 + if tidy.gotBase == nil || tidy.gotBase.IntervalSec != 86400 { + t.Fatalf("base 不是 .env 层:%#v", tidy.gotBase) + } + recs, err := env.srv.obs.audit.List(context.Background(), 50) + if err != nil { + t.Fatalf("list audit: %v", err) + } + found := false + for _, rec := range recs { + if rec.What == memoryTuningAuditAction && rec.Result == "ok" { + found = true + } + } + if !found { + t.Fatalf("审计链里没有 %s/ok 的记录:%#v", memoryTuningAuditAction, recs) + } +} + +// TestAdminMemoryTidyTuningWrite_HotFailureIsHonest 热生效实现报错时:**值照旧留在盘上** +// (不丢),但响应如实 `hot:false` + 原因,绝不说成「已生效」。 +func TestAdminMemoryTidyTuningWrite_HotFailureIsHonest(t *testing.T) { + t.Parallel() + tidy := &fakeTuningTidy{err: errors.New("boom")} + env := newTuneEnv(t, Deps{MemoryTidy: tidy}) + env.seed(t, `{"intervalSec": 86400}`) + resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"intervalSec": 10800}`) + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body) + } + view := env.view(t, body) + if view.Hot || view.HotError == "" { + t.Fatalf("hot/hotError = %v/%q, want false/非空", view.Hot, view.HotError) + } + if got := view.Values["intervalSec"]; got != float64(10800) { + t.Fatalf("热生效失败不该丢值:%#v", view.Values) + } +} + +// TestAdminMemoryTidyTuning_UnavailableWithoutDataDir 数据目录未接线时如实报「没有落点」, +// 绝不猜一个目录去写(端点据此回 500 tuning_unavailable)。 +func TestAdminMemoryTidyTuning_UnavailableWithoutDataDir(t *testing.T) { + t.Parallel() + srv, err := New(cfgDefault(), Deps{ + Auth: auth.New(config.AuthConfig{AdminUser: "a", AdminPass: "b", JWTSecret: testSecret}), + }) + if err != nil { + t.Fatalf("new server: %v", err) + } + if _, ok := srv.memoryTidyTuningPath(); ok { + t.Fatal("没有数据目录时不该解析出落点") + } +} diff --git a/internal/services/gateway/admin_memory_tuning_test.go b/internal/services/gateway/admin_memory_tuning_test.go index b422cfd..2128cd8 100644 --- a/internal/services/gateway/admin_memory_tuning_test.go +++ b/internal/services/gateway/admin_memory_tuning_test.go @@ -1,9 +1,7 @@ package gateway import ( - "context" "encoding/json" - "errors" "net/http" "net/http/httptest" "os" @@ -14,7 +12,6 @@ import ( "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig" - "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" ) // 本文件锁定记忆整理「面板调优」读写端点(`GET|PUT /api/v1/admin/memories/tidy/tuning`) @@ -280,245 +277,3 @@ func TestAdminMemoryTidyTuningWrite_AbsentKeyLeavesBytesUntouched(t *testing.T) t.Fatalf("partial patch 结果 = %s", string(env.bytes(t))) } } - -// TestAdminMemoryTidyTuningWrite_InvalidValueNeverTouchesDisk 三态之三(本文件的核心 -// 证据):任何非法输入都 `400 invalid_payload`,且**盘上一个字节都不动**。 -func TestAdminMemoryTidyTuningWrite_InvalidValueNeverTouchesDisk(t *testing.T) { - t.Parallel() - cases := []struct{ name, body string }{ - {"字符串冒充整数", `{"intervalSec": "3600"}`}, - {"小数不是整数", `{"tempTtlSec": 1.5}`}, - {"周期为 0(会让整理空转)", `{"intervalSec": 0}`}, - {"周期为负", `{"quietIntervalSec": -60}`}, - {"钟点越上界", `{"quietStartHour": 24}`}, - {"钟点越下界", `{"quietEndHour": -1}`}, - {"钟点不是数字", `{"quietEndHour": "07"}`}, - {"开关不是布尔", `{"ingestGate": "yes"}`}, - {"未知键(拼错的 quietEnd)", `{"quietEnd": 7}`}, - {"请求体不是对象", `[1, 2]`}, - {"请求体是 null", `null`}, - {"请求体是空串", ``}, - {"合法键与非法键同时提交", `{"intervalSec": 3600, "autoApply": "on"}`}, - } - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - env := newTuneEnv(t, Deps{}) - before := env.seed(t, `{ - "intervalSec": 86400, - "quietStartHour": 23, - "quietEndHour": 7, - "t1Judge": false -}`) - resp, body := env.put(t, env.token(t, auth.RoleOwner), tc.body) - if resp.StatusCode != http.StatusBadRequest { - t.Fatalf("status = %d, want 400 (body=%s)", resp.StatusCode, body) - } - assertErrorCode(t, body, "invalid_payload") - if after := env.bytes(t); string(after) != string(before) { - t.Fatalf("非法输入改动了盘上内容:\nbefore=%s\nafter=%s", before, after) - } - }) - } - // 盘上**没有**文件时,非法输入也不得创建它(「先落盘再校验」同样是落盘)。 - t.Run("文件不存在时也不创建", func(t *testing.T) { - env := newTuneEnv(t, Deps{}) - resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"intervalSec": 0}`) - if resp.StatusCode != http.StatusBadRequest { - t.Fatalf("status = %d, want 400 (body=%s)", resp.StatusCode, body) - } - if _, err := os.Stat(env.path()); !os.IsNotExist(err) { - t.Fatalf("非法输入创建了文件:err=%v", err) - } - }) -} - -// TestAdminMemoryTidyTuningWrite_NullClearsKey 清除哨兵:`null` 删掉该键(回到下一档 -// 兜底),同文件其它键不受影响。 -func TestAdminMemoryTidyTuningWrite_NullClearsKey(t *testing.T) { - t.Parallel() - env := newTuneEnv(t, Deps{}) - env.seed(t, `{"intervalSec": 3600, "quietIntervalSec": 7200, "t1Judge": false}`) - resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"intervalSec": null}`) - if resp.StatusCode != http.StatusOK { - t.Fatalf("clear status = %d, want 200 (body=%s)", resp.StatusCode, body) - } - view := env.view(t, body) - if _, ok := view.Values["intervalSec"]; ok { - t.Fatalf("清除后不该再回显 intervalSec:%#v", view.Values) - } - if got := view.Values["quietIntervalSec"]; got != float64(7200) { - t.Fatalf("清除一个键影响了别的键:%#v", view.Values) - } - doc := env.diskDoc(t) - if _, ok := doc["intervalSec"]; ok { - t.Fatalf("盘上仍有 intervalSec:%s", string(env.bytes(t))) - } - if _, ok := doc["t1Judge"]; !ok { - t.Fatalf("清除一个键抹掉了别的键:%s", string(env.bytes(t))) - } -} - -// TestAdminMemoryTidyTuningWrite_RefusesCorruptFile 盘上那份运行期读不走时,**读与写 -// 都**拒绝(`409 tuning_corrupt`)且绝不覆盖 —— 否则面板会说「已保存」而运行期仍在 -// 整体回落 `.env` 并记 WARN。 -func TestAdminMemoryTidyTuningWrite_RefusesCorruptFile(t *testing.T) { - t.Parallel() - for _, tc := range []struct{ name, body string }{ - {"类型写错", `{"intervalSec": "3600"}`}, - {"根本不是 JSON", `not json at all`}, - {"顶层不是对象", `[1, 2, 3]`}, - } { - tc := tc - t.Run(tc.name, func(t *testing.T) { - env := newTuneEnv(t, Deps{}) - before := env.seed(t, tc.body) - tok := env.token(t, auth.RoleOwner) - resp, body := env.put(t, tok, `{"intervalSec": 3600}`) - if resp.StatusCode != http.StatusConflict { - t.Fatalf("write status = %d, want 409 (body=%s)", resp.StatusCode, body) - } - assertErrorCode(t, body, "tuning_corrupt") - if string(env.bytes(t)) != string(before) { - t.Fatalf("坏文件被覆盖了:%s", string(env.bytes(t))) - } - // 读侧同样如实报坏,而不是回一份空配置假装「没配过」。 - rresp, rbody := env.get(t, tok) - if rresp.StatusCode != http.StatusConflict { - t.Fatalf("read status = %d, want 409 (body=%s)", rresp.StatusCode, rbody) - } - assertErrorCode(t, rbody, "tuning_corrupt") - }) - } -} - -// TestAdminMemoryTidyTuningWrite_SameValueDoesNotRewrite 「保存同样的值」不该在盘上留 -// 一次无意义的改写(内容逐字节相同 ⇒ 不落盘、mtime 不动)。 -func TestAdminMemoryTidyTuningWrite_SameValueDoesNotRewrite(t *testing.T) { - t.Parallel() - env := newTuneEnv(t, Deps{}) - env.seed(t, `{"intervalSec": 3600}`) - past := time.Now().Add(-time.Hour) - if err := os.Chtimes(env.path(), past, past); err != nil { - t.Fatalf("chtimes: %v", err) - } - resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"intervalSec": 3600}`) - if resp.StatusCode != http.StatusOK { - t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body) - } - info, err := os.Stat(env.path()) - if err != nil { - t.Fatalf("stat: %v", err) - } - if !info.ModTime().Equal(past) { - t.Fatalf("同值保存重写了文件(mtime %v ⇒ %v)", past, info.ModTime()) - } - if string(env.bytes(t)) != `{"intervalSec": 3600}` { - t.Fatalf("盘上内容变了:%s", string(env.bytes(t))) - } -} - -// fakeTuningTidy 既是整理服务的只读出口,也是「可热生效」的实现(记录收到的值)。 -type fakeTuningTidy struct { - fakeTidyDryRunner - gotBase *memory.TidyConfig - gotTuning *runtimeconfig.MemoryTidyTuning - err error -} - -func (f *fakeTuningTidy) ApplyPanelTuning(base memory.TidyConfig, panel runtimeconfig.MemoryTidyTuning) error { - f.gotBase = &base - f.gotTuning = &panel - return f.err -} - -// fakeTuningMemories 同时实现 MemoryStore 与「入库侧可热生效」。 -type fakeTuningMemories struct { - got *runtimeconfig.MemoryTidyTuning - err error -} - -func (f *fakeTuningMemories) List(context.Context) ([]memory.MemoryItem, error) { - return []memory.MemoryItem{}, nil -} - -func (f *fakeTuningMemories) ApplyIngestPanelTuning(panel runtimeconfig.MemoryTidyTuning) error { - f.got = &panel - return f.err -} - -// TestAdminMemoryTidyTuningWrite_HotAppliesAndAudits 接线了热生效实现时:保存后当场把 -// **新值**交给运行期(hot=true),并落一条审计 `memory.tidy.tuning`/ok。 -func TestAdminMemoryTidyTuningWrite_HotAppliesAndAudits(t *testing.T) { - t.Parallel() - tidy := &fakeTuningTidy{} - mems := &fakeTuningMemories{} - env := newTuneEnv(t, Deps{MemoryTidy: tidy, Memories: mems}) - env.seed(t, `{"intervalSec": 86400}`) - resp, body := env.put(t, env.token(t, auth.RoleAdmin), `{"intervalSec": 10800, "tempTtlSec": 259200}`) - if resp.StatusCode != http.StatusOK { - t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body) - } - view := env.view(t, body) - if !view.Hot || view.HotError != "" { - t.Fatalf("hot/hotError = %v/%q, want true/空", view.Hot, view.HotError) - } - if tidy.gotTuning == nil || tidy.gotTuning.IntervalSec == nil || *tidy.gotTuning.IntervalSec != 10800 { - t.Fatalf("整理档没收到面板值:%#v", tidy.gotTuning) - } - if mems.got == nil || mems.got.TempTtlSec == nil || *mems.got.TempTtlSec != 259200 { - t.Fatalf("入库侧没收到面板值:%#v", mems.got) - } - // base 是 `.env` 层(进程生效配置投影),不是面板值本身。 - if tidy.gotBase == nil || tidy.gotBase.IntervalSec != 86400 { - t.Fatalf("base 不是 .env 层:%#v", tidy.gotBase) - } - recs, err := env.srv.obs.audit.List(context.Background(), 50) - if err != nil { - t.Fatalf("list audit: %v", err) - } - found := false - for _, rec := range recs { - if rec.What == memoryTuningAuditAction && rec.Result == "ok" { - found = true - } - } - if !found { - t.Fatalf("审计链里没有 %s/ok 的记录:%#v", memoryTuningAuditAction, recs) - } -} - -// TestAdminMemoryTidyTuningWrite_HotFailureIsHonest 热生效实现报错时:**值照旧留在盘上** -// (不丢),但响应如实 `hot:false` + 原因,绝不说成「已生效」。 -func TestAdminMemoryTidyTuningWrite_HotFailureIsHonest(t *testing.T) { - t.Parallel() - tidy := &fakeTuningTidy{err: errors.New("boom")} - env := newTuneEnv(t, Deps{MemoryTidy: tidy}) - env.seed(t, `{"intervalSec": 86400}`) - resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"intervalSec": 10800}`) - if resp.StatusCode != http.StatusOK { - t.Fatalf("status = %d, want 200 (body=%s)", resp.StatusCode, body) - } - view := env.view(t, body) - if view.Hot || view.HotError == "" { - t.Fatalf("hot/hotError = %v/%q, want false/非空", view.Hot, view.HotError) - } - if got := view.Values["intervalSec"]; got != float64(10800) { - t.Fatalf("热生效失败不该丢值:%#v", view.Values) - } -} - -// TestAdminMemoryTidyTuning_UnavailableWithoutDataDir 数据目录未接线时如实报「没有落点」, -// 绝不猜一个目录去写(端点据此回 500 tuning_unavailable)。 -func TestAdminMemoryTidyTuning_UnavailableWithoutDataDir(t *testing.T) { - t.Parallel() - srv, err := New(cfgDefault(), Deps{ - Auth: auth.New(config.AuthConfig{AdminUser: "a", AdminPass: "b", JWTSecret: testSecret}), - }) - if err != nil { - t.Fatalf("new server: %v", err) - } - if _, ok := srv.memoryTidyTuningPath(); ok { - t.Fatal("没有数据目录时不该解析出落点") - } -}