Operant Studio
OPER-66

Real-DB integration test for OPER-60 admission gate + composed transaction

Suggested
Justin Cooke

Body

feature_id: FEAT-studio-dispatcher

## Context

The OPER-60 admission gate (PR #177, merged [REDACTED-DOB]) ships with 22 tests in `apps/web/src/lib/__tests__/plan-validation-runner.test.ts` and 14 tests in `apps/web/src/lib/__tests__/plan-size-gate-wiring.test.ts`. All 36 tests use a fake `scopedDb` (`h.getScopedDb.mockImplementation(...)`) — no Postgres, no Prisma engine.

The fake covers the shape (`task.findFirst`, `task.update`, `planningValidation.create`, `planningRejection.create`, `agentWorker.findMany`, `$transaction`, `auditLog.create`), but it does NOT cover:

1. The `PlanningRejection` FK/unique constraints (`taskId + planningValidationId` cardinality; whether the composite index the migration created is actually used by the runner's writes).
2. The `NEEDS_RESCOPE` enum on `TaskStatus` in the live schema — the fake accepts any string, so a typo in `status: "NEEDS_RESCOPE"` would still pass fake tests but blow up in production against the check constraint.
3. The composed transaction with the size gate — both gates write inside the same `$transaction`, and only a real DB verifies that a partial commit is impossible (i.e. that admission's `PlanningRejection` row and size gate's `Task.status = AWAITING_HUMAN` cannot both land).

Nika (Chaos Engineer) flagged this during the OPER-60 memo review: "the fake tells us the runtime calls the right method, but not that the DB accepts the payload." This ticket closes that gap.

## Scope

1. Add `apps/web/src/lib/__tests__/plan-validation-runner.integration.test.ts` that runs against a real Postgres database (use the existing test-DB helper — see `apps/web/src/test/db.ts` for the pattern already in use by other `.integration.test.ts` files).
2. Cover exactly these paths against the real DB:
   - Happy path: an admissible ticket with a passing plan promotes to `PLANNING_VALIDATED` and writes a `PlanningValidation` row.
   - Admission-fail path: an unsized ticket writes a `PlanningRejection` row with the correct `reasons[]` and transitions the task to `NEEDS_RESCOPE`.
   - Size-gate-fail path: an admissible size-L ticket writes a `PlanningValidation(verdict='fail')` row and transitions to `AWAITING_HUMAN` with the two labels applied.
   - Composed-transaction path: verify that when admission fails, NO size-gate side effects (no AWAITING_HUMAN transition, no label writes, no PLAN_SIZE_REFUSED_EVENT) land.
3. Do NOT duplicate the 36 unit tests. Real-DB tests are slower (~1s each vs 2ms); pick the 4 above as the coverage floor.
4. Use `beforeEach` teardown that truncates the tables the test touches (`Task`, `PlanningValidation`, `PlanningRejection`, `AuditLog`). Do not `prisma migrate reset` between tests — it is 10x slower and the CI job has no budget for it.

## Acceptance Criteria

- 4 new `.integration.test.ts` cases pass locally against a real Postgres instance.
- Each case runs in <2s including setup/teardown.
- CI runs the integration test file (add to whatever pipeline currently runs the other `.integration.test.ts` files; if none, that is a separate ticket — do NOT add a new CI job in this ticket).
- The test file imports the same `runPlanValidation` symbol the unit tests use; no fork of the runtime.
- Test-DB helper is reused, not copied.

## Non-goals

- End-to-end HTTP test through the `/api/studio/[team]/tasks/[id]/validate-plan` route. That is the existing route test; this ticket is about the runner.
- Load / concurrency testing. If concurrent validations on the same task can corrupt the row, that is a separate ticket surfaced by observability, not by this test.
- Coverage of the PHI-reviewer gate (`phi_no_reviewer` reason). That path fires against a live worker pool and belongs in its own integration test.

## Verification

`pnpm --filter web vitest run src/lib/__tests__/plan-validation-runner.integration.test.ts`

Attachments

Loading attachments…

Comments

Loading comments…