## 一、`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 条)。
87 lines
3.7 KiB
Go
87 lines
3.7 KiB
Go
package repo
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
/*
|
||
* `user_appearance.user_id` 存的是**用户名**(不是 UUID)—— 这件事必须被钉住。
|
||
*
|
||
* # 为什么值得一条判据
|
||
*
|
||
* 它**不是**活跃 bug(本包读写都用 username,handler 也一律传 `user.Username`),
|
||
* 但它是一个**不报错的陷阱**:兄弟表的同名列存的是 UUID
|
||
* (`user_keys` / `user_sessions` 实测都是 `809967c5-…`),
|
||
* 而任何人按那个习惯写 JOIN 会**静默匹配到 0 行**。
|
||
*
|
||
* 我自己在 2026-09-19 审计时就先踩了一次:用
|
||
* `LEFT JOIN user_appearance a ON u.user_id = a.user_id` 查出全是空,
|
||
* 差点当成"这些账号从没设置过外观"(而实际上 jianf 有记录)。
|
||
*
|
||
* # 这条判据钉什么
|
||
*
|
||
* ① **值来源**:本包所有涉及该表的查询都必须用 `user.Username`/username 语义
|
||
* (具体表现:函数的参数名与调用契约里出现 username 字样,而不是 UserID);
|
||
* ② **陷阱被写明**:文件头要有一段解释"这一列与兄弟表不同、JOIN 会静默失配",
|
||
* 否则下一个人只会重新踩一遍;
|
||
* ③ **README 级的可见性**:那条注释必须提到一个**具体的兄弟表名**
|
||
* (拿它当"这里是异常的那一个"的锚)—— 空泛的"注意类型"没人会读。
|
||
*
|
||
* ★ 判据从 `*_test.go` 读源码(不是运行时行为):这里要钉的是**约定**,
|
||
* 而约定的存在形式就是注释与命名。行为测试证明不了"下一个人不会踩"。
|
||
*/
|
||
func TestUserAppearanceStoresUsernameNotUUID(t *testing.T) {
|
||
src, err := os.ReadFile("appearance.go")
|
||
if err != nil {
|
||
t.Fatalf("读不到 appearance.go:%v", err)
|
||
}
|
||
text := string(src)
|
||
|
||
/* ① 陷阱必须被写明,且要指名一个兄弟表(拿它当"异常的是这张"的锚) */
|
||
if !strings.Contains(text, "存的是") || !strings.Contains(text, "用户名") {
|
||
t.Fatal("★ appearance.go 的头部必须写明 user_appearance.user_id 存的是**用户名** —— " +
|
||
"它与兄弟表不同,不写清楚下一个人只会重新踩(我踩过一次)")
|
||
}
|
||
siblings := []string{"user_keys", "user_sessions"}
|
||
found := false
|
||
for _, s := range siblings {
|
||
if strings.Contains(text, s) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatal("★ 那段说明必须**指名一个兄弟表**(user_keys / user_sessions)—— " +
|
||
"'注意列名'这种空泛提醒没人会读;指名才让人知道去比对什么")
|
||
}
|
||
if !strings.Contains(text, "JOIN") {
|
||
t.Fatal("★ 必须说清**失败长什么样**(JOIN 会静默匹配到 0 行)—— " +
|
||
"只说'语义不同'不能让人预见到后果")
|
||
}
|
||
|
||
/* ② 本包对该表的所有查询都要走同一个语义(不得混入 UUID 语义的参数名) */
|
||
if strings.Contains(text, "userUUID") || strings.Contains(text, "user_id UUID") {
|
||
t.Fatal("★ 出现 UUID 语义的标识符 —— 该表存的是用户名,两种语义混用必然出错")
|
||
}
|
||
|
||
/*
|
||
* ③ handler 侧也必须一致(跨文件的一致性是这条约定的另一半):
|
||
* 若哪天有人在 handler 里改成传 UUID,功能会**静默失效**
|
||
* (读不到记录 ⇒ 一律返回默认外观,且完全不报错)。
|
||
*/
|
||
hsrc, err := os.ReadFile("../handler/appearance.go")
|
||
if err != nil {
|
||
t.Fatalf("读不到 handler/appearance.go:%v", err)
|
||
}
|
||
htext := string(hsrc)
|
||
if strings.Contains(htext, "user.UserID") {
|
||
t.Fatal("★ handler 里不得出现 `user.UserID` —— " +
|
||
"本表按**用户名**存取;传 UUID 会静默读不到记录(表现:外观永远回到默认值,且没有报错)")
|
||
}
|
||
if !strings.Contains(htext, "user.Username") {
|
||
t.Fatal("★ handler 应显式使用 `user.Username`(这是那条约定的落点)")
|
||
}
|
||
}
|