super-productivity/packages/super-sync-server/tests/integration
Johannes Millan f91f958a6c
fix(supersync): retry lock-bounded migrations by shape, not by name (#9226)
* fix(supersync): retry lock-bounded migrations by shape, not by name

A production deploy on 2026-07-21 failed twice on the fastupdate migration
with P3018/55P03: ALTER INDEX ... SET (...) needs ACCESS EXCLUSIVE on the
index, and the planner takes AccessShareLock on every index of a table for
any query planning against it, held to end of transaction — so a single slow
query on `operations` starves the deliberate 1s window. One deploy burned
both its allowed attempts; the next won only on its second. One retry is a
coin flip.

- Gate the native retry on SHAPE (SET LOCAL lock_timeout, then a single
  ALTER INDEX ... SET (...), no third statement) instead of the literal SQL
  text with a hardcoded index name. Restores the script's documented
  "hardcodes no migration names" invariant, which name-hardcoding had already
  broken a production deploy over once.
- Raise the budget from 1 retry to 10 attempts, 5s apart (~70s worst case,
  far below both the step timeout and MIGRATION_TIMEOUT). The cap is a
  literal, not an env knob: a non-numeric value would make the `-ge` test
  error inside an `if`, which `set -e` does not catch, and the Helm
  initContainer path has no outer timeout at all.
- Do NOT widen the gate to "any 55P03". A single-statement migration gets no
  implicit transaction, so a lock timeout during a bare CREATE INDEX
  CONCURRENTLY leaves an INVALID index unrolled-back, and a generic branch
  would short-circuit that deliberate fail-loud path (the 2026-05-18
  missing-entity-index outage, #7654).

Tests: replace the three fastupdate-specific refusal cases (two of which now
correctly retry) with cases isolating each gate condition, and add coverage
for a differently-named index and for more than one retry. Every new
assertion was sabotage-verified to fail without its guard.

Also fix two tests that could not fail: the migration-ordering tautology
(compared two adjacent string literals) now reads the real migrations
directory, and the no-hardcoded-names invariant enforced only a denylist of
three legacy index names, so it passed vacuously over the new hardcode it
was meant to catch.

README: correct the claim that editing an applied migration "breaks migrate
deploy" — verified against PG16/Prisma 5.22.0, migrate deploy never re-reads
an applied migration and exits 0. The real reasons not to are that the edit
reaches nobody who already applied it, and that migrate dev shadow-replays it.

* refactor(supersync): drop the lock-retry sleep knob

Prisma CLI start already spaces retries several seconds apart, so the explicit
sleep bought little: ~25% duty cycle of self-inflicted lock queueing without it
vs ~11% with. Removing it deletes an operator-facing env knob, its validation,
and its tests, and shortens the worst case from ~80-160s to ~45-60s.

Gate behaviour verified identical under the shipped node:24-alpine BusyBox sh
and grep, not just dash/bash.

* test(supersync): prove the lock-retry path against real PostgreSQL

The existing spec drives a FAKE npx prisma, so it can only prove control flow.
It cannot prove the gate matches SQL Postgres accepts, that Prisma's real 55P03
output matches the script's log anchors, or that a plain seq-scan reader is
enough to starve the ALTER's lock window.

This holds a real reader open on a real table, deploys the lock-bounded
migration through the real script against real Prisma, and asserts it loses the
race, retries, wins, and leaves reloptions set. Runs in the existing
test:integration:postgres suite; skips without DATABASE_URL.

Sabotage-verified: with MAX_LOCK_ATTEMPTS=1 the test fails.

* fix(supersync): address delta review of the lock-retry work

A three-agent review of the post-review fixes found one CRITICAL and several
real gaps. All verified before fixing.

- CRITICAL: the new integration spec was not in vitest.config.ts's exclude
  list, so `npm test` would run it in CI — where DATABASE_URL is set at job
  level — before `prisma db push` and in parallel workers, twice per run.
- The lock_timeout bound was asymmetric: '9999ms' (~10s) was accepted while
  '6s' was refused, so a conforming migration could still queue traffic for
  ~10s, retried 10x. Capped both branches at 5s.
- Dropped the ';' exclusions from both patterns: proven redundant (the
  anchored paren-free option list is what excludes a smuggled statement) and
  they false-rejected legal exotic index names. Corrected two comments that
  credited the wrong mechanism, and 'all four are enforced below' when only
  three are.
- Removed unmeasured timing claims from the MAX_LOCK_ATTEMPTS comment; the
  measured CLI cost was ~0.45s, not the ~1.6s asserted, making the stated duty
  cycle wrong in the unsafe direction. Inter-attempt spacing is incidental now,
  and says so.
- migration-sql.spec.ts: deleted the legacy three-name denylist (subsumed by
  the derived list), added a sentinel + reloption-keyword check to close the
  coverage hole left by dropping the 'fastupdate' assertion.
- Integration spec: release the blocker on the first observed lock timeout
  instead of a 12s timer, so contention is guaranteed at any runner speed
  (also 13.6s -> 3.3s); drop the schema in beforeAll too, since a killed run
  poisoned later runs; make cleanup failure-robust; assert the retry count
  stays well under the budget rather than merely non-zero.

Sabotage-verified: widening the ms cap, hardcoding a reloption or index name,
and making the gate case-sensitive each kill a test; MAX_LOCK_ATTEMPTS=1 still
kills the integration test. Gate re-probed under the shipped node:24-alpine
BusyBox sh/grep.

* fix(supersync): cover the lock-timeout refusal guard and strip ANSI

Round-4 review, three agents. The headline finding was a coverage hole, not a
code defect: deleting the entire lock-timeout refusal guard left 72/72 green.
That guard is the only thing stopping a 55P03 on a CONCURRENTLY migration from
falling through to out-of-band execution + 'resolve --applied' — the shape of
the 2026-05-18 missing-index outage (#7654). Deleting it now fails 10 tests.

- Assert the REASON in the refusal cases, not just a non-zero exit; every other
  fail_loudly satisfied the old assertions equally.
- New test: a CONCURRENTLY migration that lock-times-out is never auto-recovered.
- Strip ANSI from the migrate log at capture. Prisma colorizes 'Error: P3018'
  under FORCE_COLOR, and that anchor is the first conjunct of all three failure
  detectors, so one stray env var disabled every recovery path — and poisoned
  parse_failing_migration too, which a log_has_line-only fix would have missed.
  Stripping once at capture covers both readers. Verified under BusyBox sed.
- Accept-side coverage for the millisecond bound: dropping the whole ms branch
  used to leave the suite green while refusing every documented ms migration.
- Cover the per-migration counter reset, and correct the comment that implied
  MAX_LOCK_ATTEMPTS bounds a whole deploy — it is N x 10 for N migrations.
- Print the INVALID-index recovery hint when a lock timeout aborts a bare
  CONCURRENTLY build, matching the other fail-loud branches.
- Validate MIGRATE_MAX_ATTEMPTS: the file argued that hazard to justify
  hardcoding MAX_LOCK_ATTEMPTS while leaving its sibling unguarded.
- Sentinel for the reloption denylist; it had exactly one entry, so a drifting
  regex would have gone vacuous.
- Integration spec: 60s hook timeout on beforeAll (it does DB work now), 120s
  backstop, failure output on the assertions that rendered none, drop a dead
  assertion.
- execFileSync timeout so a runaway loop fails instead of hanging CI.

Sabotage-verified: each of the four new guards kills at least one test.
2026-07-21 22:39:06 +02:00
..
clean-slate-atomicity-sql.integration.spec.ts fix(sync): preserve conflict cancellation and harden force overwrite (#8981) 2026-07-13 21:54:07 +02:00
conflict-detection-sql.integration.spec.ts test(supersync): cover real-prisma array winner (#9210) 2026-07-21 00:10:18 +02:00
health-alert-db-probe.integration.spec.ts feat(supersync): add database incident guardrails (#9212) 2026-07-21 13:18:04 +02:00
migrate-deploy-db-timeout.integration.spec.ts feat(supersync): add database incident guardrails (#9212) 2026-07-21 13:18:04 +02:00
migrate-deploy-lock-retry.integration.spec.ts fix(supersync): retry lock-bounded migrations by shape, not by name (#9226) 2026-07-21 22:39:06 +02:00
multi-client-sync.integration.spec.ts refactor: retire GET /api/sync/snapshot, re-scope GET /api/sync/status as diagnostic (#8496) 2026-06-20 12:02:58 +02:00
registration-races.integration.spec.ts fix(sync): harden passkey registration verification (#8985) 2026-07-13 22:58:37 +02:00
repair-causality.integration.spec.ts fix(sync): harden full-state operation recovery (#8973) 2026-07-13 22:58:17 +02:00
snapshot-skip-optimization.integration.spec.ts Cleanup/remove dead super sync surface (#8526) 2026-06-22 14:34:18 +02:00
snapshot-vector-clock-sql.integration.spec.ts fix(sync): preserve own vector clock counter across full-state op resets 2026-04-01 15:41:13 +02:00
websocket-storm.integration.spec.ts test(supersync): cover WS storm fixes with unit + integration tests 2026-05-20 15:57:00 +02:00