## 一、`user_appearance.user_id` 存的是**用户名**(不是 UUID)—— 陷阱显式化
审计时实测到的差异:
| 表 | `user_id` 存的是 |
|---|---|
| `user_keys` | **UUID**(`809967c5-…`) |
| `user_sessions` | **UUID** |
| `user_appearance` | **用户名**(`jianf`)← 与兄弟表不同 |
**功能上自洽**(本包读写都用 username,handler 一律传 `user.Username`),
所以**不是活跃 bug**。但它是个**不报错的陷阱**:按兄弟表的习惯写
LEFT JOIN user_appearance a ON u.user_id = a.user_id
会**静默匹配到 0 行**。我自己审计时就先踩了一次 —— 查出来全是空,
差点当成"这些账号从没设置过外观"(实际 jianf 有记录)。
**没有改列名**:表里有生产数据(壁纸本体在 blob 里、由 `image_sha256` 引用),
改列要迁移 + 回滚预案,收益只是"名字更好看"。选择**把语义钉住**:
文件头写清(**指名兄弟表**当锚、说清**失败长什么样**)+ 新判据
`appearance_semantics_test.go` 双向校验(repo 层不得混入 UUID 语义、
handler 层不得出现 `user.UserID`)。
★ 判据从源码读而不是运行时测:这里要钉的是**约定**,
而约定的存在形式就是注释与命名 —— 行为测试证明不了"下一个人不会踩"。
## 二、销掉一份**已经漂移**的欠账副本
`debt_registry_test.go` 里有一份**手写副本** `debts` map(给 `debtSummary()`
打印用),而**没有任何判据校验它与权威 `docs/DEBTS.json` 一致**。
实测漂移得很厉害:
- 副本 3 条 vs 权威 **17 条**;
- 里面还留着 **`gesture-semantics`** —— 那一笔已于 2026-09-19 还清并销账;
- `static-criteria` 的余额还是旧值 **7**(权威已是 5)。
后果很具体:**`TestMain` 打印的余额是错的**,而余额的全部意义就是"能看全"。
处理:**删掉副本**,让 `debtSummary()` 直接读权威 JSON(同一个事实不留两份),
并加判据 `TestDebtSummaryReadsAuthoritativeLedger` 双向钉住:
① 不许再引入手写副本;② 打出来的数字必须与 JSON 一致,
且**每一笔余额>0 的都要出现在打印里**(漏掉的欠账等于不存在)。
同时修了 `TestDebtLedgerMatchesMeasurement` 里一条**硬编码欠账清单**的断言
(它点名要求 `gesture-semantics` 必须存在 —— 还清了反而红)。
改成断言**形状**(跨端净值 + 本包可实测的那笔必须同处登记),不点名具体笔数。
★ 教训:硬编码欠账清单不可维护,漏改的后果是"还清了反而红"。
## 三、写这三条判据时连踩两次**自匹配**
`strings.Contains(src, "var debts = map[string]debt{")` —— 那串字**本身**
出现在断言的 `t.Fatal` 消息里(以及我留的说明注释里)⇒ 恒红。
(`run-all.mjs` 的自检锚点也栽过一次,那里改用 `lastIndexOf`。)
修法:锚点**拼出来**(`"var " + "debts" + " = map..."`)+ 注释措辞避开那个声明。
## 四、验证
`go test ./...` → **13 个包全过**。
新增:`appearance_semantics_test.go`(4 条断言)、
`TestDebtSummaryReadsAuthoritativeLedger`(2 条)。
257 lines
11 KiB
Go
257 lines
11 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|