Files
MailUI4Agents/server/internal/repo/debt_registry_test.go
JianFeeeee a8d6811cd7 style(server): gofmt 注释规范 + init.sql 补 parent_session_id + 新增 ratelimit 判据
## 主体是 gofmt

`server/` 下 20 个文件的改动里,**142/147 行是注释与缩进** ——
Go 1.19+ 的 gofmt 把块注释里的 `//   x` 规范成 `//\tx`。
功能零变化。

## 一处实质:init.sql 补 parent_session_id

`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS parent_session_id
 TEXT REFERENCES sessions(session_id)`

远端已在 `db/migrate.go` 的 `sqliteAddColumns` 结构迁移通道里加了同一列;
init.sql 是**全新库**的建库脚本,缺这列会让新库少一个字段。

- `IF NOT EXISTS` ⇒ 与结构迁移通道幂等,两者并存无害
- 默认空 = 根节点;**不据历史数据猜父子**(猜错会让线索拓扑失真,比没有更糟)

## 新增 handler/ratelimit_test.go(162 行)

速率限制的判据。此前这一块没有独立判据。

## 验证(本机实跑,非推断)

    go build ./...                              通过
    go vet ./internal/{handler,repo,db,sse}/   通过
    go test -count=1(handler/repo/db/sse/blob)
      handler  41.3s  ok
      repo    101.9s  ok
      db       1.1s  ok
      sse      0.01s ok
      blob     0.08s ok

`-count=1` 是刻意的:第一遍跑出来全是 `(cached)`,那种绿灯不算数。
2026-10-05 09:26:59 +08:00

256 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repo
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
/*
欠账的**单一余额**(pi 2026-09-14 裁定 §3)。
背景:到这轮为止已经有三笔不同类型、各自表达方式的欠账 ——
鸿蒙静态判据(`RESULT static=5`,有余额、有到期探针)、
`mails.status` 行级派生化(`t.Skip`,**原先无余额**)、
手势语义契约(登记的到期前提,**原先无余额**)。
三笔都"可判",但**没有一处能一眼看全**;而"欠账不显形就等于没有"。
所以:**登记在一处、可打印**。Go 这一侧打印在 TestMain 收尾,
跨端的净值同时登记在 `docs/DEBTS.json`(两端都能读,见该文件)。
*/
type debt struct {
ID string
Count int
Due string // 到期前提(什么时候该还清)
Where string // 判据在哪
}
/*
* ★★ 2026-09-19:原先这里有一份**手写副本**(一个 `debts` map 字面量),
* 已删除。它只被 `debtSummary()` 打印用,而**没有任何判据校验它与权威
* `docs/DEBTS.json` 一致** —— 实测漂移得很厉害:3 条 vs JSON 里的 17 条,
* 而且里面还留着**已销账**的 `gesture-semantics`、`static-criteria` 的余额
* 也还是旧值 7(JSON 已是 5)。
*
* 后果很具体:**`TestMain` 打印的余额是错的**,而余额的全部意义就是"能看全"。
*
* ⇒ 直接删掉副本、让 `debtSummary()` 读权威 JSON(同一个事实不留两份)。
*/
/*
* `debtMark` 由各条欠账的判据在"确认未清"时调用。
*
* ★ 2026-09-19:改为写**内存 overlay**(`measuredDebts`),不再改那份已删除的
* 手写副本。overlay 只影响本次进程内的打印——余额的来源仍只有
* `docs/DEBTS.json` 一处,测量结果叠在它上面。
* (原先它改的是手写 map,而那份 map 与权威 JSON 已经漂移到对不上,
* 于是"标记未清"这件事只改了一份没人看的副本。)
*/
var measuredDebts = map[string]int{}
func debtMark(id string) {
measuredDebts[id] = 1
}
type debtEntry struct {
ID string `json:"id"`
Count int `json:"count"`
Due string `json:"due"`
Where string `json:"where"`
}
type debtLedger struct {
Debts []debtEntry `json:"debts"`
}
// loadDebts 读**那一份**登记(docs/DEBTS.json):两端的余额必须来自同一处。
func loadDebts(t *testing.T) debtLedger {
t.Helper()
b, err := os.ReadFile(filepath.Join("..", "..", "..", "docs", "DEBTS.json"))
if err != nil {
t.Fatalf("读欠账登记 docs/DEBTS.json 失败:%v(登记丢了 = 欠账不显形)", err)
}
var l debtLedger
if err := json.Unmarshal(b, &l); err != nil {
t.Fatalf("欠账登记不是合法 JSON:%v", err)
}
return l
}
/*
★ 登记与**实测**必须一致(pi 2026-09-14 裁定 §1:Skip 的条件要是测量结果,"跳过"要进余额)。
这条判据挡的是两种死法:
① 条件被写死:余额不是从测量来的,而是某人抄的数字 ⇒ 这里用 debtMark 的实测值比对;
② 跳过不可数:`go test` 对 Skip 是退出码 0、`--- SKIP` 只是一行输出
(而且**跑得通时 go test 根本不打印包的输出** —— 我第一次就把余额打在 TestMain 里,
结果常态运行时一个字都看不见,正是"不显形")。所以可见的那份在
electron 套件的 RESULT 行(它读同一个文件),这里保证两处**同源**。
*/
func TestDebtLedgerMatchesMeasurement(t *testing.T) {
l := loadDebts(t)
seen := map[string]bool{}
for _, d := range l.Debts {
seen[d.ID] = true
if d.Due == "" || d.Where == "" {
t.Fatalf("欠账 %s 必须写明到期前提与判据位置(否则它只是「存在」,不是「欠账」)", d.ID)
}
}
/*
* ★★ 2026-09-19 修:原先这里**硬编码**要求三笔都在
* (`static-criteria` / `mails-status-derived` / `gesture-semantics`)。
* 而 `gesture-semantics` 已于 2026-09-19 **还清并销账**
* (P6 第 3 步的左右滑动翻页落地,语义契约判据 `cross-client-gesture.test.mjs` 建好)
* ⇒ 这条断言与事实矛盾,会把"已还清"变成永久红。
*
* 改成断言**两类必须存在的东西**,而不是点名三笔:
* ① 跨端净值(`static-criteria`:鸿蒙静态判据的余额)—— 它必须一直在,
* 哪怕余额最后降到 0(那条笔本身就是"这类欠账的登记处");
* ② 本包能**实测**的那一笔(`mails-status-derived`)—— 它是这台机器上
* 唯一能"测量"而不是"声明"的欠账。
*
* ⚠️ 教训:硬编码欠账清单**不可维护** —— 每还清一笔就得改一次测试,
* 而漏改的后果是"还清了反而红"。**该断言的是形状,不是点名。**
*/
for _, id := range []string{"static-criteria", "mails-status-derived"} {
if !seen[id] {
t.Fatalf("欠账登记里缺 %s —— 跨端净值与本包可实测的那笔必须同处登记,"+
"否则审计只会找到一处就当全部", id)
}
}
// 本包能实测的那一笔:**自己测**(不依赖别的测试先跑过 —— 排序依赖是隐蔽的假绿)
var want int
for _, d := range l.Debts {
if d.ID == "mails-status-derived" {
want = d.Count
}
}
outstanding, detail, derived := measureMailStatusDebt(t)
got := 0
if outstanding {
got = 1
}
if got != want {
if got == 0 {
t.Fatalf("**欠账已还清**(详情 %q 已等于按读者派生 %q),但 docs/DEBTS.json 还记着 %d —— "+
"还清是可测事件,登记要跟着清(这正是这条判据存在的意义)", detail, derived, want)
}
t.Fatalf("mails-status-derived 的余额:登记说 %d,实测说 %d —— 登记与测量分叉了", want, got)
}
}
// debtSummary 供 TestMain 打印:一处能看全的余额 + 到期前提。
//
// ★ 2026-09-19:改为**直接读权威 `docs/DEBTS.json`**(原先读这里的一份手写副本,
// 那份副本已漂移到与 JSON 完全对不上 —— 详见上面删掉它时留的那段)。
//
// 拿不到 `*testing.T`(TestMain 里调),所以这里不 Fatalf、出错时报一行说明即可:
// 余额打印是**给人看的辅助信息**,它坏掉不该让整个测试套件崩,
// 但也不能静默 —— 与"欠账不显形就等于没有"同一条纪律。
func debtSummary() string {
b, err := os.ReadFile(filepath.Join("..", "..", "..", "docs", "DEBTS.json"))
if err != nil {
return fmt.Sprintf(" debts=?(读 docs/DEBTS.json 失败:%v)", err)
}
var l debtLedger
if err := json.Unmarshal(b, &l); err != nil {
return fmt.Sprintf(" debts=?(docs/DEBTS.json 不是合法 JSON:%v)", err)
}
ids := make([]string, 0, len(l.Debts))
total := 0
byID := map[string]debtEntry{}
for _, d := range l.Debts {
byID[d.ID] = d
if d.Count > 0 {
ids = append(ids, d.ID)
total += d.Count
}
}
if len(ids) == 0 {
return " debts=0(全部还清)"
}
sort.Strings(ids)
var sb strings.Builder
fmt.Fprintf(&sb, "\n======= 欠账余额 debts=%d =======", total)
for _, id := range ids {
d := byID[id]
fmt.Fprintf(&sb, "\n · %s ×%d\n 判据:%s\n 到期:%s", d.ID, d.Count, d.Where, d.Due)
}
return sb.String()
}
/*
* ★★ 2026-09-19 新增:**余额打印必须来自权威源**(不许再留手写副本)。
*
* # 为什么需要这一枪
*
* 本文件里有两份欠账数据:
* · `docs/DEBTS.json` —— **权威**(跨端唯一登记,两端都读它);
* · 上面的 `var debts` map —— **手写副本**,只给 `debtSummary()` 打印用。
*
* 而原先**没有任何判据校验后者**(`TestDebtLedgerMatchesMeasurement` 从 JSON 读,
* 从不看 map)。后果实测(这一轮就撞上了):
* · map 里 `static-criteria.Count = 7`,而 JSON 已是 5(admin 升级 + 移出名单);
* · map 里还留着 `gesture-semantics` —— 那一笔**已经还清并销账**了。
* 也就是说 **`TestMain` 打印出来的余额是错的**,而余额的全部意义就是"能看全"。
*
* ⇒ 两处挂钩:map 的 id 集合与 JSON 的 id 集合**必须相同**,
* 且两边都在的 id,`Count` 必须相等。
*
* ★ 为什么不干脆删掉 map、让 `debtSummary()` 直接读 JSON:
* 那当然更好,但 `debtSummary()` 在 `TestMain` 里被调用,而它**拿不到 `*testing.T`**
* (`loadDebts(t)` 需要 t 来 Fatalf)。改动面比"加一条对账判据"大,
* 而在这一轮里优先做**能立刻防住漂移**的那一步。
* (这条本身也记在案:能合并成一处时应当合并,见文件头"登记在一处"的原意。)
*/
func TestDebtSummaryReadsAuthoritativeLedger(t *testing.T) {
/*
* 两件事,缺一不可:
* ① 余额打印**不能**再有手写副本(那是"同一个事实两份实现",
* 实测它漂移到 3 条 vs 17 条,而没人发现);
* ② 它打出来的数字必须与权威 JSON 一致(可执行的对账,不只是"看起来读了")。
*/
src, err := os.ReadFile("debt_registry_test.go")
if err != nil {
t.Fatalf("读不到本文件:%v", err)
}
/*
* ⚠️ 锚点**不能**写成完整的那行声明 —— 它本身出现在上面这条 t.Fatal 的
* 消息串里,于是 `strings.Contains` 会**自匹配**(实测:源码里明明已经没有
* 那份 map 了,这条却恒红)。这是本仓踩过多次的同一个坑
* (`run-all.mjs` 的自检锚点也栽过一次,那里改用 `lastIndexOf`)。
* 这里改成拼出锚点,让它不出现在断言消息里。
*/
handwritten := "var " + "debts" + " = map[string]debt{"
if strings.Contains(string(src), handwritten) {
t.Fatal("★ 不许再引入手写的欠账副本 —— 余额只能来自 docs/DEBTS.json。" +
"(它漂移过:3 条 vs 权威的 17 条,且没人发现,因为打印出来的东西没有判据在管)")
}
if !strings.Contains(string(src), `filepath.Join("..", "..", "..", "docs", "DEBTS.json")`) {
t.Fatal("★ `debtSummary()` 必须直接读 docs/DEBTS.json(唯一权威)")
}
/* ② 实际打出来的数字要与 JSON 对得上 */
l := loadDebts(t)
total := 0
for _, d := range l.Debts {
total += d.Count
}
out := debtSummary()
if !strings.Contains(out, fmt.Sprintf("debts=%d", total)) {
t.Fatalf("★ 余额打印与权威登记不一致:打印里没有 `debts=%d`。\n实际输出:%s", total, out)
}
/* 每一笔余额>0 的都要在打印里出现(漏掉的欠账等于不存在) */
for _, d := range l.Debts {
if d.Count > 0 && !strings.Contains(out, d.ID) {
t.Fatalf("★ 欠账 %s 余额 %d 却没出现在余额打印里 —— 漏掉的欠账等于不存在", d.ID, d.Count)
}
}
}