package store import ( "context" "database/sql" "encoding/json" "errors" "os" "path/filepath" "testing" "time" _ "github.com/jackc/pgx/v5/stdlib" "yovision/bell/contracts" "yovision/bell/internal/event" ) type storeIDs struct{ id string } func (f storeIDs) NewEventID() (string, error) { return f.id, nil } type allowVideo struct{} func (allowVideo) VideoAllowed(context.Context, int64, int64, int64) (bool, error) { return true, nil } func testCandidate(t *testing.T, configVersion string) []byte { t.Helper() path := filepath.Join("..", "..", "..", "docs", "raw", "contracts", "event-v0.1.example-current.json") raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var object map[string]any if err := json.Unmarshal(raw, &object); err != nil { t.Fatal(err) } delete(object, "id") object["config_version"] = configVersion encoded, err := json.Marshal(object) if err != nil { t.Fatal(err) } return encoded } func newEvent(t *testing.T, configVersion string) event.Event { t.Helper() guard, err := event.NewEvidenceGuard() if err != nil { t.Fatal(err) } factory, err := event.NewFactory( contracts.EventV01Schema, storeIDs{id: "evt_01J8XQ2K7M3P5R9T0V4W6Y8Z2B"}, allowVideo{}, guard, ) if err != nil { t.Fatal(err) } value, err := factory.Create(context.Background(), testCandidate(t, configVersion)) if err != nil { t.Fatal(err) } return value } func TestPostgresImmutableEventAndOutcome(t *testing.T) { dsn := os.Getenv("YOVISION_TEST_BELL_POSTGRES_DSN") if dsn == "" { t.Skip("YOVISION_TEST_BELL_POSTGRES_DSN is not set") } db, err := sql.Open("pgx", dsn) if err != nil { t.Fatal(err) } defer db.Close() ctx := context.Background() repo, err := OpenPostgres(ctx, db) if err != nil { t.Fatal(err) } value := newEvent(t, "sp-v1-2026.07.20") created, err := repo.InsertEvent(ctx, value) if err != nil || !created { t.Fatalf("first insert: created=%v err=%v", created, err) } created, err = repo.InsertEvent(ctx, value) if err != nil || created { t.Fatalf("idempotent replay: created=%v err=%v", created, err) } if _, err := repo.InsertEvent(ctx, newEvent(t, "sp-v1-conflict")); !errors.Is(err, ErrConflict) { t.Fatalf("expected immutable conflict, got %v", err) } reason := "confirmed by operator" outcome := Outcome{ ID: "out_01J8XQ2K7M3P5R9T0V4W6Y8Z2B", EventID: value.ID(), Value: "true_positive", Source: "manual", Reason: &reason, ActorType: "user", ActorID: "operator-1", OccurredAt: time.Now().UTC(), } created, err = repo.AppendOutcome(ctx, outcome) if err != nil || !created { t.Fatalf("append outcome: created=%v err=%v", created, err) } created, err = repo.AppendOutcome(ctx, outcome) if err != nil || created { t.Fatalf("idempotent outcome replay: created=%v err=%v", created, err) } if _, err := db.ExecContext(ctx, `UPDATE bell.events SET kind='changed' WHERE id=$1`, value.ID()); err == nil { t.Fatal("runtime unexpectedly updated immutable event") } if _, err := db.ExecContext(ctx, `DELETE FROM bell.event_outcomes WHERE id=$1`, outcome.ID); err == nil { t.Fatal("runtime unexpectedly deleted immutable outcome") } }