package db /* 一次性回填守卫的判据(2026-10-04)。 ★ 这一格**必须放在 db 包**:守卫(`app_meta` 标记)在 migrate.go 里, 而回填实现在 repo 包。放在 repo 包测就只测到「回填本身幂等」, 测不到「Migrate 二次执行会不会覆盖人工设置」—— 那正是需要防的事。 后果不是理论上的:回填改的是会话**拓扑**。若每次 Migrate 都跑, 「某人手工把 parent 改对」这个意图会在每次重启时被悄悄改回去。 既有 backfillMailReads 踩过同族坑(其注释:每次跑会把「某抄送方读过」 按主收件人写成已读,正是那次要修的语义错误)。 */ import ( "context" "testing" ) func TestSessionParentsBackfillRunsOnlyOnce(t *testing.T) { Close() Connect(context.Background(), "sqlite://"+t.TempDir()+"/once.db") if err := Migrate(context.Background()); err != nil { t.Fatalf("首次迁移: %v", err) } t.Cleanup(Close) ctx := context.Background() // Migrate 已跑过一次 ⇒ 标记已写 ⇒ 之后的回填都不该再动任何东西。 manual := mustSession(t, "manual-parent") child := mustSession(t, "child") if _, err := DB.ExecContext(context.Background(), `UPDATE sessions SET parent_session_id = ? WHERE session_id = ?`, manual, child); err != nil { t.Fatalf("设人工父: %v", err) } // 第二次 Migrate(模拟重启) if err := Migrate(context.Background()); err != nil { t.Fatalf("二次迁移: %v", err) } var got string if err := DB.QueryRowContext(ctx, `SELECT parent_session_id FROM sessions WHERE session_id = ?`, child).Scan(&got); err != nil { t.Fatalf("读父: %v", err) } if got != manual { t.Errorf("★ 二次 Migrate 覆盖了人工设置的 parent_session_id:%s ⇒ 应为 %s", got, manual) } } func mustSession(t *testing.T, alias string) string { t.Helper() ctx := context.Background() var id string if err := DB.QueryRowContext(ctx, `INSERT INTO sessions (session_alias, subject, status, workspace, from_agent) VALUES ($1,'x','active','/tmp','pi') RETURNING session_id`, alias).Scan(&id); err != nil { t.Fatalf("建会话: %v", err) } return id }