Files
MailUI4Agents/server/internal/repo/mail_status_derived_test.go
JianFeeeee eeb8f277fd test(mails.status): "正当"落成可判约束(只许比 archived);欠账补一条"还清即转绿"的判据
pi 2026-09-14 的三点接续。

1. **§1「正当」不能只是标注,要是可判的约束**:按文件+计数登记挡得住"新增一处",
   挡不住"**已登记的那一处改变性质**"。已判出来:**任何与 m.status 的比较只允许比
   'archived'**(全局属性),与 'read'/'unread' 比较 ⇒ 红(per-reader 属性必须走 mail_reads)。
   第一次跑就抓到第三处:`migrate.go` 的 `WHERE m.status = 'read'` ×2 —— 它是**回填**,
   职责就是读那个遗留值,所以按文件登记**窄豁免**(allowedCompareExempt,带理由),
   而不是"凡是迁移都放行"。

2. **§2 欠账要有"还清即转绿"的判据**:新增 `mail_status_derived_test.go`,三态形状
   (与套件探针同族)—— 欠账未清 ⇒ `t.Skip` 并写明还欠什么(**不假红也不假绿**);
   还清 ⇒ 开始实跑并断言"详情路径的 status == 按读者派生的值",**转绿即可测事件**;
   迁一半(只改 GetMailByID 漏 GetThread)⇒ 红。
   实测当前状态:`--- SKIP ... 详情路径返回行级 status="read",而按读者派生说 "unread"`。
   **没写成"现在就红"**:恒红的门 = 挂在错误相位的门(CRITERIA.md §11 已为它付过学费)。
   另按他的更正把 `migrate.go` 的理由写准:不是"跑过即结束",而是
   **"全新安装/空库路径必须存在"**(措辞会影响下一个人敢不敢删它)。

3. **§3 (b) 的语义契约补归属**:归属 = dsh(鸿蒙侧实现者),WebUI 侧不需改动;
   落点 = 跨端对齐判据那一族(形状与"预设清单逐项一致"同构,比语义不比数值);
   **如实标注当前未建**及原因(鸿蒙侧还没有日历页/手势代码 ⇒ 此时"两端逐项相同"只有一端存在,
   建出来就是假判据);到期前提 = P6 第 3 步出现手势代码时立即建,此前是**登记的欠账**。
2026-09-14 17:11:36 +08:00

71 lines
2.9 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 (
"context"
"strings"
"testing"
)
/*
「欠账还清」必须是一条**可判的事件**,不是"我们都改完了"(pi 2026-09-14 裁定 §2)。
欠账本身:`GetMailByID` / `GetThread` 把**行级** `m.status` 放进邮件 JSON,
而列表路径(`CountUnread`/`ListInbox`)给的是**按读者**派生的值 —— 同一个字段两种语义。
收口顺序是"先迁读者、再删写入"(先删写入会让 JSON 里的 status 永远是 unread,是另一种错误事实)。
这条判据的形状(三态,与套件的探针同族):
- **欠账未清** → `t.Skip`,跳过信息里写明"还欠什么",**不假红也不假绿**;
- **欠账已清** → 开始**实跑**:JSON 里的 `status` 必须由 `mail_reads` 派生
(对给定 reader 的取值等于派生死值),转绿即"这笔欠账还清了"的**可测事件**;
- 迁移做了一半(例如只改了 `GetMailByID`、漏了 `GetThread`)→ 红。
为什么不写成"现在就是红的":**恒红的门 = 挂在错误相位的门**(这轮已经为它付过学费,
见 `client/electron/test/CRITERIA.md` §11)。欠账要有余额、要有到期,也要有"还清即转绿"。
*/
func TestMailStatusInJSONIsDerivedWhenDebtCleared(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
id := seedMailTo(t, "alice", "bob")
// 欠账是否还在:看详情路径返回的 status 与"按读者派生"是否可能不同
// 手法:alice 未读时行级值仍是 'unread'(没写过的邮件),所以先让 bob 读掉这封 ——
// 一旦行级写入生效,alice 在详情里就会看到 'read',而列表路径(按读者)说 'unread'。
if err := MarkMailRead(ctx, id, "bob"); err != nil {
t.Fatal(err)
}
m, err := GetMailByID(ctx, id)
if err != nil {
t.Fatal(err)
}
perReader, err := ListInbox(ctx, "alice", "all", 50)
if err != nil {
t.Fatal(err)
}
var listStatus string
for _, x := range perReader {
if x.ID == id {
listStatus = x.Status
}
}
if listStatus == "" {
t.Fatalf("自检:列表路径里应当能看到这封邮件(id=%s)", id)
}
// 欠账判定的**唯一依据**是"详情路径是否已按读者派生"——所以先看它是否等于派生死值
if m.Status != listStatus {
// 欠账还在:不假红,明确跳过并写清欠的是什么、以及还清后这条会开始实跑
t.Skipf("欠账未清(本判据在还清那天会自动开始实跑并转绿):详情路径返回行级 status=%q,"+
"而按读者派生说 %q —— 差在 GetMailByID/GetThread 仍带出行级列;"+
"迁完那两处(改用 readStateFor)后本判据会实跑。收口顺序:先迁读者、再删写入。",
m.Status, listStatus)
}
// 已还清:断言更强的性质 —— 详情与列表必须一致(都对同一个 reader 派生)
if !strings.EqualFold(m.Status, listStatus) {
t.Fatalf("详情 %q 与列表 %q 不一致", m.Status, listStatus)
}
}