test(gateway): 面板记忆列表过 status 闸的端到端回归(TIDY-1 补证)
真实 sql.DB + 真实 memory.Store + 真实 HTTP handler:两条在册 ⇒ 面板 2 条; 软合并一条 ⇒ 只剩被保留那条;再归档另一条 ⇒ 200 + 非 null 空数组。 复核(主会话亲自):目标测试绿;用 overlay 把 store.List 的 status 闸 变异成恒真 ⇒ 该用例变红(证明它真的在测那条闸,而不是在测夹具)。 顺带确证一件口径:GET /admin/memories/reviews(审查留痕)**不该**过 status 闸 —— 留痕表本身无 status 概念(preinsert_schema.go:29-46),且它是软删行的 查看与撤销入口,加闸等于审计自我抹除。
This commit is contained in:
1 parent
9126d653e5
commit
f847769698
1 file changed
+139
@@ -0,0 +1,139 @@
|
||||
package gateway
|
||||
|
||||
// 本文件为**网关侧(面板读侧)的 status 闸**补证:
|
||||
// `GET /api/v1/admin/memories` 经 `collectMemories` → memory.Store.List,
|
||||
// 必须只显示**在册**(status='active')条目 —— 被软合并('merged')与被归档
|
||||
// ('archived')的行内容与行都还在表里,但绝不进面板列表。
|
||||
//
|
||||
// 整理(tidy)与入库前审查(D3)只做软动作(只改 status,不删行、不改正文),
|
||||
// 所以「清理有没有生效」完全取决于每条读路径过不过 status 闸。memory 包内已有
|
||||
// 读路径单测(internal/runtime/memory/status_gate_test.go);本文件补的是
|
||||
// **端到端**那一层:真实 sql.DB + 真实 memory.Store + 真实 HTTP handler。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
)
|
||||
|
||||
// newMemoryStoreAndDBForGateway 打开隔离内存库,同时返回可执行 SQL 的 *sql.DB
|
||||
// 与构建在其上的真实 memory.Store。既有 newMemoryStoreForGateway 只给 store
|
||||
// (db 句柄被封在 t.Cleanup 里),而本测试要直接改行 status,故两者都要。
|
||||
// db.Open(":memory:") 把连接池钳到单连接,因此经该 *sql.DB 的 UPDATE 与 store
|
||||
// 的读命中同一个库。
|
||||
func newMemoryStoreAndDBForGateway(t *testing.T) (*sql.DB, *memory.Store) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
d, err := db.Open(ctx, ":memory:", gwTestLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("open in-memory db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = d.Close() })
|
||||
s, err := memory.NewStore(ctx, d, gwTestLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("memory.NewStore: %v", err)
|
||||
}
|
||||
return d, s
|
||||
}
|
||||
|
||||
// softDeleteMemoryForTest 直接改一行的 status,模拟 tidy / D3 的真实软动作:
|
||||
// 只改 status,正文与行原样留着。行不存在时 RowsAffected != 1,直接失败,
|
||||
// 以免测试在「其实没改到任何行」的静默状态下给出假绿。
|
||||
func softDeleteMemoryForTest(t *testing.T, d *sql.DB, id, status string) {
|
||||
t.Helper()
|
||||
res, err := d.ExecContext(context.Background(),
|
||||
`UPDATE memory_items SET status = ? WHERE id = ?`, status, id)
|
||||
if err != nil {
|
||||
t.Fatalf("set status %q on %s: %v", status, id, err)
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
t.Fatalf("rows affected for %s: %v", id, err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("set status %q on %s: rows=%d, want 1", status, id, n)
|
||||
}
|
||||
}
|
||||
|
||||
// addMemoryViaAdmin 经 owner 写端点写入一条 persona 记忆并返回其 id。
|
||||
func addMemoryViaAdmin(t *testing.T, ts *httptest.Server, tok, content string) string {
|
||||
t.Helper()
|
||||
resp, body := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/memories",
|
||||
map[string]any{"personaId": "cyrene", "content": content, "scope": "persona"}, tok)
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("add %q status = %d, want 201 (body=%s)", content, resp.StatusCode, body)
|
||||
}
|
||||
var added struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(decodeBody(t, body)["data"], &added); err != nil {
|
||||
t.Fatalf("decode add %q: %v (body=%s)", content, err, body)
|
||||
}
|
||||
if added.ID == "" {
|
||||
t.Fatalf("add %q returned empty id (body=%s)", content, body)
|
||||
}
|
||||
return added.ID
|
||||
}
|
||||
|
||||
// listMemoriesViaAdmin 读管理面板记忆列表,返回已解码条目与原始响应体。
|
||||
func listMemoriesViaAdmin(t *testing.T, ts *httptest.Server, tok string) ([]MemorySummary, []byte) {
|
||||
t.Helper()
|
||||
resp, body := doReq(t, http.MethodGet, ts.URL+"/api/v1/admin/memories", nil, tok)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("list status = %d, want 200 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
var items []MemorySummary
|
||||
if err := json.Unmarshal(decodeBody(t, body)["data"], &items); err != nil {
|
||||
t.Fatalf("decode list: %v (body=%s)", err, body)
|
||||
}
|
||||
return items, body
|
||||
}
|
||||
|
||||
// TestAdminMemoriesListGatesSoftDeletedRows 端到端证明面板记忆列表过 status 闸:
|
||||
// 软合并一条 ⇒ 它从列表消失且剩下的是被保留那条;再归档另一条 ⇒ 列表为空
|
||||
// (200 + 非 nil 空数组)。
|
||||
func TestAdminMemoriesListGatesSoftDeletedRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
rawDB, store := newMemoryStoreAndDBForGateway(t)
|
||||
_, ts, jwt := buildEnv(t, cfgDefault(), Deps{Memories: store})
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
keepID := addMemoryViaAdmin(t, ts, tok, "她喜欢喝冰美式")
|
||||
dropID := addMemoryViaAdmin(t, ts, tok, "她讨厌香菜")
|
||||
|
||||
// ① 两条都在册:面板两条都能看到(status 闸不该误伤 active 行)。
|
||||
items, body := listMemoriesViaAdmin(t, ts, tok)
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("initial list len = %d, want 2 (items=%+v body=%s)", len(items), items, body)
|
||||
}
|
||||
t.Logf("① 两条在册 ⇒ 面板看到 %d 条: %+v", len(items), items)
|
||||
|
||||
// ② 软合并 dropID(只改 status='merged',正文不动):面板只剩 keepID。
|
||||
softDeleteMemoryForTest(t, rawDB, dropID, "merged")
|
||||
items, body = listMemoriesViaAdmin(t, ts, tok)
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("after merged one: list len = %d, want 1 (items=%+v body=%s)", len(items), items, body)
|
||||
}
|
||||
if items[0].ID != keepID || items[0].Text != "她喜欢喝冰美式" {
|
||||
t.Fatalf("after merged one: kept = %+v, want id=%s text=她喜欢喝冰美式", items[0], keepID)
|
||||
}
|
||||
t.Logf("② 软合并 %s ⇒ 面板只剩 %d 条: %+v", dropID, len(items), items)
|
||||
|
||||
// ③ 再把 keepID 归档 ⇒ 面板列表为空(200 + 非 nil 空数组,而非 null)。
|
||||
softDeleteMemoryForTest(t, rawDB, keepID, "archived")
|
||||
items, body = listMemoriesViaAdmin(t, ts, tok)
|
||||
if len(items) != 0 {
|
||||
t.Fatalf("after both soft-deleted: list len = %d, want 0 (items=%+v body=%s)", len(items), items, body)
|
||||
}
|
||||
if raw := string(decodeBody(t, body)["data"]); raw != "[]" {
|
||||
t.Fatalf("empty list data = %s, want [] (non-nil empty array)", raw)
|
||||
}
|
||||
t.Logf("③ 再归档 %s ⇒ 面板空列表 data=%s", keepID, string(decodeBody(t, body)["data"]))
|
||||
}
|
||||
Reference in new issue
Block a user