# CF Go-Live — Testing Methodology + Phased Plan

Audience: AI coding agents first.

**Mission:** `international-press-zone` plugin connects, auths, and runs all critical paths against new CF backend (product repo `alexcodeplace/press.zone` → `backend/`) in production, verified by automated tests, before leased VPS expires (>2 months out). Strategy locked: race the deadline; capture old-prod behavior NOW as insurance.

**Gate registry:** every phase exit maps to MR-xx criteria in `GOLIVE.md` § Migration readiness. GOLIVE.md = single source of truth for criteria statements. This doc = methodology + sequencing only. Do NOT restate criteria text here.

---

## Ground truth (verified 2026-07-19)

- Old prod: `https://api.press.zone` — Express/Prisma/Bull on VPS (`100.116.176.87`). Alive, no customers, test data only. THE behavioral oracle. Dies at VPS expiry.
- New backend CANONICAL repo (established 2026-07-19): `github.com/alexcodeplace/press.zone` → `backend/` (ported from platform monorepo, PR #1). Hono on CF Workers, Hyperdrive→Neon, Queues, cron. Spec 1 core BUILT + DEPLOYED to `press-zone.dry-salad-ffa1.workers.dev` FROM product repo (routes: health, oauth, webhooks, plugin, subscribe, account, staff; unit + integration tests green). KV FORBIDDEN (product doctrine) — rate limiting MUST use CF-native binding; verify in Phase 1.
- Platform repo (`~/Projects/platform`) = module supplier ONLY (`@platform-modules/*`). Product credentials, bindings, invoice policy, adapter code NEVER in platform repo. `@platform-modules/invoicing@0.1.1` withdrawn — product pins exact `0.1.0`.
- Morning (Green Invoice) = invoice issuer ONLY, PayPal = payment processor. Morning sandbox adapter integrated on product `main` (PR #2: `backend/api/src/lib/morning.ts`, billing-doc, refund + webhook-idempotency test hardening).
- Staff dashboard scaffold EXISTS: `backend/web/` (staff routes, web tests green) — journey coverage (MR-13/14) still pending.
- NOT built: Spec 2 translation service (`/translate`, `/estimate`, jobs surface), plugin-side new client.
- New API intentionally NOT wire-compatible with old (`/api/*` not `/v1/*`; `Authorization: Bearer <prefix>.<secret>` not `X-API-Key`; OAuth connect not manual keys). Parity = CAPABILITY level, NEVER byte level.
- Go-live plugin: `international-press-zone` ONLY. It supersedes translate-press-zone + multilingual-press-zone (their union). Adjusting/fixing it is in scope. translate/multilingual plugins: NOT migrated, retired with old backend.
- Dashboard gate: STAFF ADMIN face only. Old system never had customer web dashboard — WP plugin admin is the customer face. Customer web dashboard = post-cutover scope, NOT a blocker.
- Domain (default, user can veto): at cutover move `api.press.zone` DNS to CF Worker custom domain. Staging on `*.workers.dev` meanwhile. Old stack stays reachable via Tailscale IP until expiry.
- Decision log: locked architecture decisions live in project memory `cf-migration-backend` + `platform/docs/specs`. Read before changing any seam.

## Methodology — building source of truth where none exists

No spec exists. Construct FOUR artifacts; they become the spec. All committed under `docs/contracts/`.

1. **SoT-1 Client-demand contract** — extract from `international-press-zone` client code (`includes/Translation/TranslationService.php`, `JobSender.php`, `SiteRegistrar.php`, `OnboardingApi.php`, `CharacterEstimator.php`, `Licensing/LicenseClient.php`, `Integration/TranslationAPI.php`): every endpoint called, method, headers, request fields sent, response fields CONSUMED (only consumed fields are contract), error codes HANDLED. Output: `docs/contracts/plugin-client-contract.md`. Rule: plugin code defines demand side — if plugin never reads a field, field is NOT contract.
2. **SoT-2 Behavioral oracle fixtures** — drive live `api.press.zone` through every SoT-1 entry; record request/response pairs for: happy path, invalid auth, insufficient credits, rate limit, malformed input. Output: `docs/contracts/fixtures/*.json`. MUST complete while VPS alive — this is the only executable record of "at least as capable as old". Deadline insurance: if greenfield slips past expiry, fixtures preserve the oracle.
3. **SoT-3 Capability parity matrix** — table: old capability (from SoT-1 + old routes) → new-app equivalent (route/mechanism) → L1 test ID → status (`PASS`/`GAP`/`DROPPED:<reason>`). Output: `docs/contracts/parity-matrix.md`. Every `GAP` = Spec 2 backlog item. Every `DROPPED` MUST carry explicit reason (e.g. migration/* endpoints = old-backend internal, not migrated).
4. **SoT-4 Journey catalog** — UJ contracts (use `create-journeys` skill) for: WP plugin admin journeys (connect, estimate, translate, bulk, credits, errors) + staff admin journeys (login, lookup, subs/usage, refund, credit, suspend). Output: `docs/contracts/journeys/UJ-*.md`. Each UJ later derives one E2E spec (`creating-journey-e2e-tests` skill).

## Test pyramid

| Layer | What | Runs against | Tooling |
|---|---|---|---|
| L0 | Unit + integration (exists, keep green) | in-process | vitest, platform app |
| L1 | Black-box API capability suite, parameterized `BASE_URL` + auth adapter | CF staging; oracle mode vs old prod where capability overlaps | vitest or node scripts, own repo dir `tests/capability/` |
| L2 | Plugin integration E2E: real WP + international plugin → CF staging | localhost:8080 WP + cloudflared tunnel for callbacks | Playwright driving wp-admin |
| L3 | Staff admin dashboard UI journeys + per-page UI matrix | CF staging dashboard, data seeded VIA API (never direct DB inserts — "showing real data" is the assertion) | Playwright (`creating-journey-e2e-tests`, `creating-ui-matrix-tests`) |
| L4 | Cutover smoke: `@smoke`-tagged L1 subset + synthetic canary cron | production | same harness, prod creds |

Rules:
- Write L1 tests FROM SoT-1/SoT-3 BEFORE building missing endpoints (system-level TDD). Gap report = build backlog.
- L1 asserts capabilities, not bytes: "valid credential → translate 200 + wallet debited exactly char-count", "revoked credential → 401", "over quota → 402 `INSUFFICIENT_CREDITS`", "burst N+1 → 429 + Retry-After", "seat cap exceeded → 409 `SITE_LIMIT_REACHED`", "duplicate PayPal webhook event → single credit grant".
- L1 MUST run green in oracle mode against old prod first (validates the harness itself before it judges the new app).
- L3 seeds data through public API + staff API only. Direct DB seeding = reject (defeats "real data" assertion).
- Every MR-xx criterion in GOLIVE.md maps to ≥1 automated test ID. Code inspection NEVER satisfies an MR criterion.

## Rate-limit design constraint (user directive 2026-07-19)

Legit bulk bursts exist: site-wide translation = ~20k strings + ~1k pages → hundreds of authed API calls within minutes. Rate limiting MUST NOT fail this flow.

- Limits per ENDPOINT CLASS, never one global number: unauth per-IP (tight) | sync translate (moderate) | authed job submission + status reads (generous token bucket with real burst capacity, NOT small fixed window) | login (backoff).
- Bulk work arrives BATCHED: plugin chunks site-wide translation into `/jobs` + `bulk-strings` submissions. Spec 2 MUST size submission limits from this scenario.
- Real cost protection = per-account job-concurrency cap + credit debit (locked decisions) — NOT request-rate starvation.
- Contract: every 429 carries `Retry-After`; plugin client MUST honor it with backoff + resume (client-side queue). Throttling outcome = slower, NEVER failed or lost work. L1/L2 assert this.
- Enforced by MR-06 bulk-burst test (GOLIVE.md).

## Phases

Exit gates reference GOLIVE.md MR-xx. Do not start a phase's build work before its listed inputs exist.

### Phase 0 — Freeze + capture (week 1) — do FIRST, insurance against expiry
1. Extract SoT-1 client contract.
2. Capture SoT-2 fixtures vs live old prod.
3. Draft SoT-3 parity matrix (status column mostly `GAP` — fine).
4. Write SoT-4 journey catalog.
Exit: MR-01, MR-02, MR-03 (matrix drafted), MR-04 inputs ready.

### Phase 1 — Capability harness (weeks 1–2, overlaps 0)
1. Build L1 suite from SoT-1/SoT-3, parameterized backend adapter (old: `X-API-Key`+`/v1`; new: Bearer+`/api`).
2. Run oracle mode vs old prod → harness green (MR-04).
3. Run vs CF staging → gap report; feed `GAP` rows into Spec 2 scope.
Exit: MR-04; gap report committed.

### Phase 2 — Build missing surface (weeks 2–6)

Locked by user 2026-07-20: staff dashboard ships FULL old-panel parity (UJ-101..UJ-117, all MUST-HAVE — see journeys/INDEX.md § Locked decisions). Entitlement/metering MUST be plugin-generic (forum plugin meters different units under different limits): per-plugin meter definitions over one shared ledger, adding a plugin is CONFIGURATION not schema change. Reject any design hardcoding characters/translation as THE metered unit. Gate: MR-21.

1. Spec 2 translation service: brainstorm→spec→plan per platform flow (seams already verified in memory: `TranslationStore` port, `ledger.debit` off usage, `jobs` queue). Repo: product `alexcodeplace/press.zone` → `backend/`; reusable seams upstream as `@platform-modules/*` PRs (never auto-merge).
2. Staff admin dashboard MVP: ops journeys only (SoT-4 staff UJs). CF Pages + auth-react per Spec 1.
3. Each landed feature flips its L1 tests green — L1 is the acceptance gate, not new ad-hoc tests.
Exit: MR-05..MR-10 green on staging.

### Phase 3 — Plugin adaptation (weeks 5–7, overlaps 2)
1. New client layer in `international-press-zone`: OAuth connect flow, Bearer credential storage (encrypted `wp_options`), `/api` paths, new `{data}/{error:{code,message}}` envelope.
2. Backend-switch setting (`legacy` | `platform`) so old path keeps working during transition; remove after decommission.
3. L2 E2E on real WP vs CF staging; callbacks via cloudflared tunnel.
Exit: MR-11, MR-12.

### Phase 4 — Dashboard journeys + nonfunctional gates (weeks 7–8)
1. L3 staff admin journey suite + UI matrix green.
2. Live rate-limit verification (3 layers: per-IP unauth, per-credential, per-account concurrency + login backoff).
3. `security-sweep` on new app surface; fix P0/P1.
4. Load sanity (k6, modest mixed RPS), observability drill (force error → Sentry event → alert received; Logpush delivering), Neon PITR restore drill.
Exit: MR-13..MR-18.

### Phase 5 — Cutover (weeks 8–9)
1. Preflight: `plan-preflight` + `prod-env` skill doctrine; secrets audit; rollback plan written (DNS revert to VPS, TTL 300).
2. Move `api.press.zone` → CF custom domain. Plugin default base URL already points at it.
3. Run L4 smoke vs production; enable synthetic canary cron.
4. Soak 72h monitored. Old VPS untouched as fallback until expiry.
5. Decommission: final DB snapshot archived, fixtures + snapshot retained, VPS released.
Exit: MR-19, MR-20; scoreboard in GOLIVE.md all green.

## Standing rules

- Old GOLIVE.md AC-xx criteria audit the OLD backend — do NOT spend effort fixing old-backend BROKEN items; they inform SoT-3 only (known-broken behaviors are NOT parity targets; mark `DROPPED:old-defect`).
- Fixtures, contracts, matrix, journeys: commit to this repo under `docs/contracts/`. Never leave them only on the VPS.
- Any new-vs-old behavioral difference discovered during L1/L2: record in parity matrix with verdict (`intended` per locked decisions | `defect`). NEVER silently absorb.
