package postgres import ( "context" "errors" "strings" "testing" "time" "github.com/jackc/pgx/v5" "knowledge-governance/internal/governance" ) func TestAuthorizeReleaseBatchAtomicallyEnqueuesProductionExecution(t *testing.T) { now := time.Date(2026, 9, 6, 5, 0, 0, 0, time.UTC) db := &recordingDB{row: releaseExecutionRow{ id: "rx-safe", batchID: "rb-safe", operation: "publish", state: "pending", target: "production", authorizedBy: "release-owner", authorizationCode: "production_release_authorized", jobID: "job-safe", itemCount: 2, createdAt: now, }} execution, err := NewStore(db).AuthorizeReleaseBatch(context.Background(), governance.AuthorizeReleaseBatchRequest{ BatchID: "rb-safe", AuthorizedBy: "release-owner", AuthorizationCode: "production_release_authorized", Now: now, }) if err != nil { t.Fatal(err) } if execution.ID != "rx-safe" || execution.Operation != governance.ReleaseOperationPublish || execution.State != governance.ReleaseExecutionPending || execution.JobID != "job-safe" || execution.ItemCount != 2 { t.Fatalf("execution = %#v", execution) } sql := strings.ToLower(db.queryRowSQL) for _, required := range []string{ "state = 'sealed'", "release_ready", "count(*) filter", "insert into jobs", "'release.publish'", "insert into release_executions", "'production'", "on conflict (idempotency_key)", "returning", "release_delete_ready", "item.action = 'upsert'", "item.action = 'delete'", } { if !strings.Contains(sql, required) { t.Fatalf("authorize release SQL missing %q", required) } } for _, forbidden := range []string{"api_key", "password", "secret", "production_database_url"} { if strings.Contains(sql, forbidden) { t.Fatalf("authorize release SQL contains forbidden credential field %q", forbidden) } } } func TestAuthorizeReleaseBatchRejectsInvalidAuthorization(t *testing.T) { tests := []governance.AuthorizeReleaseBatchRequest{ {BatchID: "rb-safe", AuthorizedBy: "owner", AuthorizationCode: "wrong", Now: time.Now()}, {BatchID: "rb-safe", AuthorizedBy: "bad\nowner", AuthorizationCode: "production_release_authorized", Now: time.Now()}, } for _, request := range tests { if _, err := NewStore(&recordingDB{}).AuthorizeReleaseBatch(context.Background(), request); !errors.Is(err, ErrInvalidReleaseAuthorization) { t.Fatalf("error = %v", err) } } } func TestAuthorizeReleaseBatchRejectsStaleSealGate(t *testing.T) { _, err := NewStore(&recordingDB{row: sourceScanErrorRow{err: pgx.ErrNoRows}}).AuthorizeReleaseBatch(context.Background(), governance.AuthorizeReleaseBatchRequest{ BatchID: "rb-safe", AuthorizedBy: "owner", AuthorizationCode: "production_release_authorized", Now: time.Now(), }) if !errors.Is(err, ErrReleaseAuthorizationGateConflict) { t.Fatalf("error = %v", err) } } func TestAuthorizeReleaseRollbackRequiresVerifiedPriorItems(t *testing.T) { now := time.Date(2026, 9, 6, 6, 0, 0, 0, time.UTC) db := &recordingDB{row: releaseExecutionRow{ id: "rx-rollback", batchID: "rb-safe", operation: "rollback", state: "pending", target: "production", authorizedBy: "release-owner", authorizationCode: "production_rollback_authorized", jobID: "job-rollback", itemCount: 2, createdAt: now, }} execution, err := NewStore(db).AuthorizeReleaseRollback(context.Background(), governance.AuthorizeReleaseRollbackRequest{ PriorExecutionID: "rx-publish", AuthorizedBy: "release-owner", AuthorizationCode: "production_rollback_authorized", Now: now, }) if err != nil { t.Fatal(err) } if execution.Operation != governance.ReleaseOperationRollback || execution.ID != "rx-rollback" { t.Fatalf("execution=%#v", execution) } sql := strings.ToLower(db.queryRowSQL) for _, required := range []string{ "operation = 'publish'", "state in ('completed', 'failed')", "item.state = 'verified'", "count(*)::integer", "'release.rollback'", "insert into jobs", "insert into release_executions", "prior_execution_id", } { if !strings.Contains(sql, required) { t.Fatalf("rollback authorization SQL missing %q", required) } } } func TestScheduleAutomaticRollbackUsesOnlyVerifiedItemsFromFailedPublish(t *testing.T) { now := time.Date(2026, 9, 10, 2, 0, 0, 0, time.UTC) db := &recordingDB{row: releaseExecutionRow{ id: "rx-auto", batchID: "rb-safe", operation: "rollback", state: "pending", priorExecutionID: "rx-failed", target: "production", authorizedBy: "system:auto-rollback", authorizationCode: "automatic_rollback_after_failure", jobID: "job-auto", itemCount: 1, createdAt: now, }} if err := NewStore(db).ScheduleAutomaticRollback(context.Background(), "rx-failed", now); err != nil { t.Fatal(err) } sql := strings.ToLower(db.queryRowSQL) for _, required := range []string{"state = 'failed'", "item.state = 'verified'", "count(*)::integer", "'system:auto-rollback'", "'automatic_rollback_after_failure'", "'release.rollback'"} { if !strings.Contains(sql, required) { t.Fatalf("automatic rollback SQL missing %q", required) } } } func TestAuthorizeReleaseRollbackRejectsInvalidAuthorization(t *testing.T) { _, err := NewStore(&recordingDB{}).AuthorizeReleaseRollback(context.Background(), governance.AuthorizeReleaseRollbackRequest{ PriorExecutionID: "rx-publish", AuthorizedBy: "release-owner", AuthorizationCode: "wrong", Now: time.Now(), }) if !errors.Is(err, ErrInvalidReleaseAuthorization) { t.Fatalf("error=%v", err) } } func TestRetryReleaseExecutionResetsOnlyFailedItemsAndJob(t *testing.T) { now := time.Date(2026, 9, 6, 17, 0, 0, 0, time.UTC) db := &recordingDB{row: releaseExecutionRow{id: "rx-safe", batchID: "rb-safe", operation: "publish", state: "pending", target: "production", authorizedBy: "release-owner", authorizationCode: "production_release_authorized", jobID: "job-safe", itemCount: 2, completedCount: 1, createdAt: now}} store := NewStore(db) execution, err := store.RetryReleaseExecution(context.Background(), governance.RetryReleaseExecutionRequest{ExecutionID: "rx-safe", Now: now}) if err != nil { t.Fatal(err) } if execution.ID != "rx-safe" || execution.State != governance.ReleaseExecutionPending || execution.CompletedItemCount != 1 || execution.FailedItemCount != 0 { t.Fatalf("execution=%#v", execution) } sql := strings.ToLower(db.queryRowSQL) for _, required := range []string{"release_ready as", "release_delete_ready", "item.action = 'delete'", "update release_item_executions", "state = 'pending'", "and item.state = 'failed'", "update jobs", "attempt = 0", "lease_until = null", "update release_executions", "rollback.prior_execution_id = execution.id"} { if !strings.Contains(sql, required) { t.Fatalf("SQL missing %q: %s", required, sql) } } } type releaseExecutionRow struct { id, batchID, operation, priorExecutionID, state, target, authorizedBy, authorizationCode, jobID, manifestSHA, errorCode string itemCount, batchSize, currentBatch, totalBatches, completedCount, failedCount int createdAt time.Time startedAt, completedAt *time.Time } func (r releaseExecutionRow) Scan(dest ...any) error { *dest[0].(*string), *dest[1].(*string), *dest[2].(*string), *dest[3].(*string), *dest[4].(*string) = r.id, r.batchID, r.operation, r.priorExecutionID, r.state *dest[5].(*string), *dest[6].(*string), *dest[7].(*string), *dest[8].(*string) = r.target, r.authorizedBy, r.authorizationCode, r.jobID *dest[9].(*string), *dest[10].(*int), *dest[11].(*int), *dest[12].(*int), *dest[13].(*int) = r.manifestSHA, r.itemCount, r.batchSize, r.currentBatch, r.totalBatches *dest[14].(*int), *dest[15].(*int), *dest[16].(*string) = r.completedCount, r.failedCount, r.errorCode *dest[17].(*time.Time), *dest[18].(**time.Time), *dest[19].(**time.Time) = r.createdAt, r.startedAt, r.completedAt return nil }