# Backend-Journey Test Plan

Cross-functional planning artifact for replacing manual end-to-end testing
with backend-only Pest journey tests against in-process simulators of all
external services.

Context: the existing Playwright suite is the wrong tool. Browser overhead,
slow feedback, requires `php artisan e2e:seed` + a running server, doesn't
get run locally before commit, and weak assertions mean it doesn't catch
the bugs that are biting. We are switching to backend-only Pest Feature
tests for the user journeys you currently verify manually.

## Cross-functional perspectives synthesized

### Product / what to test

The "manual click-throughs that we want to replace" are the core value-delivery
loops. From RankWizAI's data flow doc and journey tests, the top user-facing
journeys ranked by user impact:

| Rank | Journey | Why this matters |
|---|---|---|
| 1 | **Analysis Run** — site has synced data → user triggers analysis → recommendations appear | Core product loop. If broken, everything downstream is dead. |
| 2 | **Draft Generation** — user picks a recommendation → AI generates content rewrite → draft is saved | Where AI bugs show up most concretely; OpenAI integration is its own bug surface. |
| 3 | **Draft Publishing** — user publishes draft → WP REST receives correctly-signed request → published | Last mile to user value; WP HMAC + content shape bugs land here. |
| 4 | **GSC Sync** — OAuth callback → metrics persisted | Entry point of all data; sync bugs corrupt analysis silently. |
| 5 | **WP Connect + Initial Sync** — HMAC handshake → posts ingested | Sister of #4 on the inbound-data side. |
| 6 | **ROI Tracking** — apply rec → next sync → ROI snapshot updated | The retention story; broken here = customer churn. |
| 7 | **Limit Enforcement** — user at plan limit attempts to create draft → blocked correctly | Revenue protection; AI changes here can silently leak free usage. |
| 8 | **Onboarding First Analysis** — register → connect GSC + WP → first analysis runs | New-user activation; bugs here block all growth. |

This session ships **Journeys 1, 2, 3** as deep, strong tests with simulators.
The remaining five follow the same pattern and are tracked as follow-up work.

### QA / what makes these tests useful

Three rules, applied without exception:

1. **Strong assertions.** Each test fails for at least one specific class of bug.
   No `assertTrue(true)` style "it didn't crash" assertions.
2. **Simulators, never live calls.** External services are simulated by `Http::fake()`-bound
   classes that match real request shapes and return real-shape responses. Tests
   that hit a live API are forbidden.
3. **Failure-mode coverage.** Every simulator exposes deliberate failures (rate-limit,
   timeout, malformed response, empty data). Each journey has at least one test
   that exercises a failure mode and asserts the system handles it gracefully.

### Backend / how the tests are structured

```
tests/
├── Simulators/                     ← NEW — one class per external service
│   ├── Contracts/
│   │   └── Simulator.php           ← interface: install(), assertCalled()
│   ├── OpenAiSimulator.php
│   ├── GscApiSimulator.php
│   ├── WpApiSimulator.php
│   └── README.md                   ← catalog + usage
├── Feature/
│   └── Journeys/                   ← NEW — one file per journey
│       ├── AnalysisRunJourneyTest.php
│       ├── DraftGenerationJourneyTest.php
│       └── DraftPublishingJourneyTest.php
└── Scenarios/                      ← Existing from M2 — site state seeders
```

Each journey test file follows this shape:

```php
beforeEach(function () {
    Queue::fake();                  // jobs are dispatched but we drive them
    Mail::fake();
    Notification::fake();
    OpenAiSimulator::install();     // bind Http::fake for /v1/chat/completions
    GscApiSimulator::install();     // bind Http::fake for searchanalytics
    WpApiSimulator::install();      // bind Http::fake for wp-json/*
});

test('happy path: full journey produces correct DB state and side effects', ...);
test('failure mode A: simulator returns rate limit, system retries gracefully', ...);
test('failure mode B: simulator returns malformed response, no draft saved', ...);
test('authorization: cross-tenant user cannot trigger this journey', ...);
test('limit: user over plan limit is blocked with correct error', ...);
```

### DevOps / how it integrates

- Tests run via existing Pest infrastructure: `vendor/bin/pest --parallel`.
- `phpunit.xml` already includes `tests/Feature/` — new `Journeys/` subdirectory
  is auto-discovered, no config change needed.
- Total runtime budget: full 5-journey suite under 30 seconds. (Pest feature
  tests typical: 50–300 ms each. With 10 tests per journey × 5 journeys =
  ~50 tests at ~200 ms = 10 seconds plus overhead.)
- These run on every PR, every commit, and locally via `--dirty`.
- Simulators are pure PHP, no external services, no network. Air-gapped.
- Playwright suite stays where it is for now — tests browser-specific things
  (Inertia hydration, JS-driven UI). Eventually trim it to ~5 critical
  browser-only journeys, since the backend coverage now subsumes the rest.

### Tech lead / scope discipline and anti-patterns

What this session ships:
- 3 simulators (OpenAI, GSC, WP)
- 3 journey test files, each with 4–6 strong tests covering happy path
  + failure modes + authorization + limit boundary where applicable
- 1 documentation file describing the pattern and "how to add a new journey"

What this session **does not** ship:
- Stripe simulator — Cashier already has good test helpers; defer until needed
- DataForSEO simulator — used by SERP/content scoring, not the core journeys
- Journeys 4–8 — same pattern, follow-up work
- Wiring into a test-quality gate (mutation testing on the journey tests)

Anti-patterns banned:
- **Don't snapshot HTML.** We snapshot DB state and JSON shapes only.
- **Don't assert on Inertia prop *names* alone.** Assert on prop *values*
  matching the seeded state, the way controllers actually compose them.
- **Don't mock the unit under test.** Mocking `RecommendationEngine` to
  return canned recommendations is testing the mock, not the engine. The
  simulators sit at the **external service** boundary, not the internal
  service boundary.
- **Don't catch and ignore expected exceptions silently.** Every failure
  mode test asserts the *specific* exception class or DB state, not just
  `expectException(Exception::class)`.

## Sequencing for this session

1. Reconnaissance: identify the controllers, jobs, and services that handle
   the analysis-run / draft-generation / draft-publishing flows. This locks
   down what the tests actually invoke.
2. Build simulators for the three external services that touch these journeys.
3. Build the three journey test files using the simulators + existing
   factories + the M2 scenario engine.
4. Document the pattern so journeys 4–8 can be added by following a recipe.
5. Verify via TypeScript-equivalent for PHP: read each file once more, check
   namespace + autoload + factory shape against the codebase. Cannot run PHP.

## Honest constraints

- **I cannot execute Pest in this sandbox.** No PHP runtime. Every PHP file
  shipped is the same plausible-but-wrong risk class as any AI output.
  This is the failure mode this whole project exists to defend against.
  Mitigations: every file is modeled on existing in-repo tests verified
  to pass; every assertion targets DB state or simulator-recorded calls
  that follow shapes I read directly from the codebase; the verification
  doc tells you exactly what to run and what to expect.
- **The simulators infer external API shapes from the existing client code
  and config.** If the live OpenAI/GSC/WP API has shifted from what the
  client expects, the simulator will agree with the client and disagree
  with reality. The drift canary (M6 in the implementation milestones)
  is the answer — out of scope this session, in scope before relying on
  these tests as a sole replacement for manual testing.
- **Backend journey tests will not catch frontend bugs** — Inertia hydration
  failures, React component crashes, broken event handlers. Those live in
  the Playwright suite. The honest framing: backend journey tests replace
  ~80% of manual click-throughs (everything that isn't a JS-driven
  interaction); the Playwright suite covers the rest.

## What done looks like at the end of this session

A user with a freshly cloned repo can run:

```bash
vendor/bin/pest tests/Feature/Journeys/ --parallel
```

…and see the three core journey loops execute end-to-end against in-process
simulators in under 10 seconds, with strong assertions that fail loudly when
production code regresses on any of:

- Controllers passing the wrong props or routing to the wrong job
- Jobs touching the wrong DB rows
- Service-layer logic computing wrong results
- External API responses being parsed incorrectly
- Edge cases (rate limit, timeout, malformed response) being unhandled

That is what we mean by "replacing manual testing" for these journeys.
