package local_event import ( "context" "encoding/json" "os" "path/filepath" "testing" "time" "github.com/google/uuid" "gorm.io/driver/sqlite" "gorm.io/gorm" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/integration/bell_connector" "git.ilapage.cn/ila/yovision/Sense/server/app/sense/outbox" ) func TestCreateWithOutboxCommitsAndRollsBackAtomically(t *testing.T) { db, err := gorm.Open(sqlite.Open("file:"+uuid.NewString()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatal(err) } sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(1) if err = db.AutoMigrate(&EventCandidate{}, &outbox.Message{}, &outbox.DeliveryRecord{}, &outbox.Attempt{}); err != nil { t.Fatal(err) } now := time.Date(2026, 8, 28, 10, 0, 0, 0, time.UTC) candidate := EventCandidate{ID: uuid.NewString(), OccurredAt: now, SourceRef: "SEN-CAM-01", RuleRef: "rule-1", RuleName: "区域闯入", CandidateState: CandidateStateCandidate, EvidenceState: EvidenceStatePending, RetainUntil: now.Add(24 * time.Hour)} fixturePath := filepath.Join("..", "..", "..", "..", "..", "contracts", "events", "v1", "examples", "dangerous-area.json") fixture, err := os.ReadFile(fixturePath) if err != nil { t.Fatal(err) } var payload map[string]interface{} if err = json.Unmarshal(fixture, &payload); err != nil { t.Fatal(err) } payload["producer_id"] = "sense-local" payload["source_event_id"] = candidate.ID if err = CreateWithOutbox(context.Background(), db, candidate, payload, now); err != nil { t.Fatal(err) } var candidates, messages int64 db.Model(&EventCandidate{}).Count(&candidates) db.Model(&outbox.Message{}).Count(&messages) if candidates != 1 || messages != 1 { t.Fatalf("candidates=%d messages=%d", candidates, messages) } var message outbox.Message if err = db.First(&message).Error; err != nil || message.InternalType != bell_connector.OutboxType { t.Fatalf("local event did not enqueue Bell contract delivery: type=%s err=%v", message.InternalType, err) } duplicate := candidate duplicate.ID = candidate.ID if err = CreateWithOutbox(context.Background(), db, duplicate, payload, now); err == nil { t.Fatal("expected duplicate transaction failure") } db.Model(&EventCandidate{}).Count(&candidates) db.Model(&outbox.Message{}).Count(&messages) if candidates != 1 || messages != 1 { t.Fatalf("atomic rollback failed candidates=%d messages=%d", candidates, messages) } }