package mysql import ( "context" "testing" roomeventsv1 "hyapp.local/api/proto/events/room/v1" "hyapp/internal/testutil/mysqlschema" "hyapp/pkg/appcode" "hyapp/services/room-service/internal/room/outbox" ) func retentionTestRecord(eventID string, createdAtMS int64) outbox.Record { return outbox.Record{ AppCode: appcode.Default, EventID: eventID, EventType: "RoomGiftSent", RoomID: "room-retention", Status: outbox.StatusPending, Envelope: &roomeventsv1.EventEnvelope{ AppCode: appcode.Default, EventId: eventID, RoomId: "room-retention", EventType: "RoomGiftSent", }, CreatedAtMS: createdAtMS, } } // 清理只允许命中 delivered 且早于 cutoff 的行;其他状态和窗口内的 delivered 必须原样保留, // 否则统计重放和人工补偿会丢事实。 func TestDeleteDeliveredOutboxBeforeOnlyPurgesOldDelivered(t *testing.T) { schema := mysqlschema.New(t, mysqlschema.Config{ EnvVar: "ROOM_SERVICE_MYSQL_TEST_DSN", InitDBPath: mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_room_service.sql"), DatabasePrefix: "hyapp_room_retention_test", }) ctx := context.Background() repo, err := Open(ctx, schema.DSN, 4, 4) if err != nil { t.Fatalf("open repository: %v", err) } defer repo.Close() records := []outbox.Record{ retentionTestRecord("evt-old-delivered-1", 1000), retentionTestRecord("evt-old-delivered-2", 2000), retentionTestRecord("evt-old-delivered-3", 3000), retentionTestRecord("evt-fresh-delivered", 4000), retentionTestRecord("evt-old-pending", 1000), retentionTestRecord("evt-old-failed", 1000), } if err := repo.SaveOutbox(ctx, records); err != nil { t.Fatalf("save outbox: %v", err) } const cutoffMS = int64(1_000_000) setStatusAt := func(eventID string, status string, updatedAtMS int64) { t.Helper() if _, err := schema.DB.ExecContext(ctx, `UPDATE room_outbox SET status = ?, updated_at_ms = ? WHERE app_code = ? AND event_id = ?`, status, updatedAtMS, appcode.Default, eventID); err != nil { t.Fatalf("rewrite outbox row %s: %v", eventID, err) } } setStatusAt("evt-old-delivered-1", outbox.StatusDelivered, 1000) setStatusAt("evt-old-delivered-2", outbox.StatusDelivered, 2000) setStatusAt("evt-old-delivered-3", outbox.StatusDelivered, 3000) setStatusAt("evt-fresh-delivered", outbox.StatusDelivered, cutoffMS+1) setStatusAt("evt-old-failed", outbox.StatusFailed, 1000) // 批量上限必须生效:3 条可删行按 limit=2 分两批删完,第三批确认删空。 for step, want := range []int64{2, 1, 0} { deleted, err := repo.DeleteDeliveredOutboxBefore(ctx, cutoffMS, 2) if err != nil { t.Fatalf("delete batch %d: %v", step, err) } if deleted != want { t.Fatalf("delete batch %d deleted %d rows, want %d", step, deleted, want) } } var survivors []string rows, err := schema.DB.QueryContext(ctx, `SELECT event_id FROM room_outbox WHERE app_code = ? ORDER BY event_id`, appcode.Default) if err != nil { t.Fatalf("list survivors: %v", err) } defer rows.Close() for rows.Next() { var eventID string if err := rows.Scan(&eventID); err != nil { t.Fatalf("scan survivor: %v", err) } survivors = append(survivors, eventID) } if err := rows.Err(); err != nil { t.Fatalf("iterate survivors: %v", err) } want := []string{"evt-fresh-delivered", "evt-old-failed", "evt-old-pending"} if len(survivors) != len(want) { t.Fatalf("survivors mismatch got=%v want=%v", survivors, want) } for index := range want { if survivors[index] != want[index] { t.Fatalf("survivors mismatch got=%v want=%v", survivors, want) } } }