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) } } }