package db import ( "context" "path/filepath" "testing" "time" ) // 抄送方回填的判据:**迁移前**的老邮件要按旧模型(所有人都是已读)补齐, // **迁移后**的邮件一律不碰。后者是这条迁移最容易被写坏的地方 —— // 一旦漏掉时间界线,它会把新邮件里"某个人读过"错记成"所有收件人都读过", // 也就是把这次要修的缺陷又固化回数据里。 func TestBackfillMailReadsCCBoundary(t *testing.T) { ctx := context.Background() if err := Connect(ctx, filepath.Join(t.TempDir(), "t.db")); err != nil { t.Fatal(err) } t.Cleanup(Close) if err := Migrate(ctx); err != nil { t.Fatal(err) } switchover := time.Date(2026, 9, 13, 6, 21, 24, 0, time.UTC) old := seedReadMail(t, "alice", `[{"name":"bob","path":"","session":"","raw":"bob"}]`, switchover.Add(-time.Hour)) fresh := seedReadMail(t, "alice", `[{"name":"carol","path":"","session":"","raw":"carol"}]`, switchover.Add(time.Hour)) n, err := backfillMailReadsCCSince(ctx, switchover) if err != nil { t.Fatal(err) } if n != 1 { t.Fatalf("补记行数 = %d,期望 1(只有迁移前那封的抄送方)", n) } if !hasReadRow(t, old, "bob") { t.Fatal("迁移前的老邮件:抄送方 bob 必须被补记已读(旧模型下他对这封也是已读)") } if hasReadRow(t, fresh, "carol") { t.Fatal("★ 迁移后新建的邮件被补记了抄送已读 —— 这会把'某个人读过'错写成'所有收件人都读过',正是要修的缺陷") } // 幂等:再跑一次不重复插(标记之外的第二道防线) if n2, err := backfillMailReadsCCSince(ctx, switchover); err != nil { t.Fatal(err) } else if n2 != 0 && !hasReadRow(t, old, "bob") { t.Fatalf("重复执行后补记了 %d 行且 bob 仍无记录", n2) } } // seedReadMail 造一封 status='read' 的邮件,created_at 显式指定(判据要卡时间界线)。 func seedReadMail(t *testing.T, to, ccJSON string, createdAt time.Time) string { t.Helper() ctx := context.Background() var sid string if err := DB.QueryRowContext(ctx, `INSERT INTO sessions (from_agent, subject, workspace) VALUES ('sender', 's', '') RETURNING session_id`, ).Scan(&sid); err != nil { t.Fatal(err) } var id string if err := DB.QueryRowContext(ctx, `INSERT INTO mails (session_id, from_name, to_name, subject, body, cc_list, status, created_at) VALUES ($1, 'sender', $2, 's', 'b', $3, 'read', $4) RETURNING mail_id`, sid, to, ccJSON, createdAt).Scan(&id); err != nil { t.Fatal(err) } return id } func hasReadRow(t *testing.T, mailID, reader string) bool { t.Helper() var n int if err := DB.QueryRowContext(context.Background(), `SELECT COUNT(*) FROM mail_reads WHERE mail_id = $1 AND reader_name = $2`, mailID, reader).Scan(&n); err != nil { t.Fatal(err) } return n > 0 }