package docflow_test import ( "context" "errors" "os" "strings" "testing" "time" "chat-agent-system/internal/app/uow" "chat-agent-system/internal/artifactvault" "chat-agent-system/internal/db" "chat-agent-system/internal/events" "github.com/jackc/pgx/v5/pgxpool" ) func init() { if _, err := os.Stat("deploy/postgres/goose"); err == nil { return } for _, cand := range []string{"../../..", "../../../.."} { if fi, err := os.Stat(cand + "/deploy/postgres/goose"); err == nil && fi.IsDir() { _ = os.Chdir(cand) return } } } func TestOutboxAndStagingSameTxRollback(t *testing.T) { if testing.Short() { t.Skip("same-tx outbox/staging is not part of -short") } dsn := db.SkipIfNoTestDatabaseURL(t) if strings.Contains(dsn, "5433") || strings.Contains(dsn, "agent_system_dev") || (strings.Contains(dsn, "5434") && strings.Contains(dsn, "agent_system_test")) || strings.Contains(dsn, "5435") { t.Fatal("TEST_DATABASE_URL points at a shared env port; refuse agent_system_dev/test/main") } ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatal(err) } t.Cleanup(pool.Close) if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil { t.Fatalf("migrate: %v", err) } body := []byte(`{"doc_id":"doc_m2_tx","version_no":1}`) env := events.Envelope{ EventID: "document:doc_m2_tx:v1", EventName: "document.agent_version_committed", SchemaVersion: events.EnvelopeSchema, Environment: "dev", ProjectID: 9, AggregateType: "document", AggregateID: "doc_m2_tx", AggregateVersion: 1, ActorRef: "user:1", IdempotencyKey: "commit:doc_m2_tx:1", PayloadDigest: events.Digest(body), Producer: "docflow", OccurredAt: time.Now().UTC(), Payload: body, } boom := errors.New("boom after staging") unit := uow.NewPG(pool) err = unit.Within(ctx, func(scope uow.TxScope) error { ctx = uow.WithScope(ctx, scope) if err := (events.PGSink{}).Append(ctx, env, []string{"projection", "notify"}); err != nil { return err } ing := &artifactvault.Ingest{Env: "dev"} if err := ing.StageAgentCommit(ctx, 9, "doc_m2_tx", env.EventID, 4, body); err != nil { return err } return boom }) if !errors.Is(err, boom) { t.Fatalf("want boom, got %v", err) } var envelopes, artifacts int if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM event_envelopes WHERE event_id=$1`, env.EventID).Scan(&envelopes); err != nil { t.Fatal(err) } if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM final_artifacts WHERE source_event_id=$1`, env.EventID).Scan(&artifacts); err != nil { t.Fatal(err) } if envelopes != 0 || artifacts != 0 { t.Fatalf("rollback leaked envelopes=%d artifacts=%d", envelopes, artifacts) } }