Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .claude/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
Append discoveries as you find them. Read this file at session start. Keep
entries short: date, what, why.

## 2026-06-16 — Phase 46 KI-016 O: intake→completion e2e ("demo spine") (branch `feature/46-intake-completion-e2e`)

- TEST/CI/DOCS only — zero product/runtime code changed; the spine was already correctly wired and now `__tests__/e2e/intake-to-completion.test.ts` proves it against a real DB. Gate green (lint/tc/prisma/2835 unit/build) + e2e 2 tests pass, idempotent across 3 reruns.
- **Real status-transition map (correct one — prompt had two wrong):** intake creates VR `UNTRIAGED` → inline `autoTriageService.triageEnquiry` promotes a *complete non-urgent* request to **PLANNING_POOL** (not just TRIAGED). Route proposal (`routeProposalService.generateProposals(targetDate?, originOverride?)` — it's a service method, NOT a bare `generateProposals()`) moves the VR to **CLUSTERED** (the prompt said "PROPOSED" — wrong; there is no PROPOSED planningStatus on the VR). Booking then needs the RouteRun at **APPROVED** first — there's a vet gate `routeRunRepository.update(id,{status:'APPROVED'})` (DRAFT→APPROVED) BETWEEN proposal and `bookingService.bookRouteRun`. Booking: Appointment PROPOSED + VR→BOOKED + RouteRun→BOOKED. Confirmation flips Appointment PROPOSED→CONFIRMED + writes ConfirmationDispatch. Completion: VisitOutcome + Appointment→COMPLETED + VR→COMPLETED + (followUpRequired) a new FOLLOW_UP VR in PLANNING_POOL.
- **To get a single fixture all the way to a PROPOSAL (the fiddly bit):** (a) customer must have exactly ONE yard → `yardMatcherService` returns high-confidence `sole-yard` so intake auto-fills `VisitRequest.yardId` (drives `needsMoreInfo:false` → PLANNING_POOL) regardless of message text; (b) the message must contain an explicit horse count (else `ASK_HORSE_COUNT` missing-field → stuck UNTRIAGED); (c) **horseCount must be ≥3** because `clustering.service` only keeps a single-stop cluster when `horseCount >= MIN_HORSES_FOR_SINGLE_STOP (3)` AND densityScore ≥ `minDensityScoreThreshold (20)` — a 1- or 2-horse lone yard yields NO proposal; (d) the yard needs lat/lng pre-set so the planner skips geocoding (offline-deterministic); (e) keep the e2e DB with **0 active VET staff** so `planPairing` collapses to the solo path (parallel path needs `activeVets.length>=2`).
- **Urgent path:** an urgent keyword → `urgencyLevel=URGENT`, `planningStatus=READY_FOR_REVIEW`, and `emergency.service.raiseUrgentAlert` sets `automationHold=true`; the proposal query excludes `automationHold:true` AND `urgencyLevel:'URGENT'`, so it's held out. With no `VET_ALERT_*` env + no active vet, the alert just warns + dead-letters — never throws (intake completes).
- **Real-DB test plumbing:** the prisma singleton has a soft-delete *read* extension (customer/yard/horse/enquiry) but it does NOT touch `deleteMany`/`create`, so cleanup hard-deletes fine. Pass `deletedAt: undefined` in cleanup `findMany` to also see tombstoned residue. Created rows (enquiry/VR/appt/routerun) get uuid ids — cleanup must walk relations from the known `e2e-` customer ids, not by id-prefix. Delete order: VisitOutcome (no cascade, FK line 724) before Appointment (which cascades dispatch/response/statusHistory/assignment). DEMO_MODE WhatsApp confirmation routes through the simulator → `success:true` so the CONFIRMED transition + dispatch row actually happen.
- **Vitest split:** default `vitest.config.ts` needed an explicit `exclude` (it had none → relied on vitest defaults) listing the vitest defaults + `__tests__/e2e/**`; new `vitest.e2e.config.ts` includes ONLY e2e, no react plugin, own setup that does NOT stub DATABASE_URL (fails fast if RUN_DB_E2E set but URL is the unit stub). Suite gated `describe.skipIf(!RUN_DB_E2E)`. CI: new additive `e2e` job (needs: check) with a `postgres:16` service container + `prisma migrate deploy` then `npm run test:e2e`; `check`/`docker`/`security` untouched (`docker` still `needs: check` only).
- New KI-031 (confirmation `shouldUseTemplate()` 24h-window is DEMO-only / `TODO(prod)`, Meta-gated, deferred). KI-016 O marked delivered.

Format:

```
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ on:
- 'public/**'
- 'i18n/**'
- 'scripts/**'
- '__tests__/**'
- 'vitest.config.ts'
- 'vitest.e2e.config.ts'
- 'auth.ts'
- 'middleware.ts'
- 'instrumentation.ts'
Expand Down Expand Up @@ -55,6 +58,54 @@ jobs:
- run: npm run test -- --passWithNoTests
- run: npm run build

# Phase 46 — KI-016 O: real-database end-to-end "demo spine" test.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI paths skip e2e tests

Medium Severity

The new e2e job only runs when the check job runs, but pull_request uses a paths filter that omits __tests__/**, vitest.config.ts, and vitest.e2e.config.ts. PRs that change only the real-database e2e suite (or Vitest split) skip the whole workflow, so the demo-spine test never runs in review despite the PR promising CI coverage.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b299f61. Configure here.

# Isolated, additive job — it does NOT touch the `check` gate above.
# Runs the intake → triage → proposal → booking → confirmation →
# completion pipeline against a fresh Postgres 16 service container with
# the real (non-stub) DATABASE_URL, after applying migrations.
e2e:
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
needs: check
services:
postgres:
image: postgres:16
# Throwaway credentials for an ephemeral CI service container (the
# same test:test convention the `check` job's stub DATABASE_URL uses)
# — not a secret; the container exists only for this job's lifetime.
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: equismile_e2e
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U test -d equismile_e2e"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
# Real (connecting) service-container DB — unlike the `check` job's
# non-connecting stub. Throwaway test:test creds, ephemeral container.
DATABASE_URL: "postgresql://test:test@localhost:5432/equismile_e2e"
RUN_DB_E2E: "1"
DEMO_MODE: "true"
SKIP_ENV_VALIDATION: "true"
NEXT_PUBLIC_BUILD_SHA: ${{ github.sha }}
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx prisma generate
- name: Apply migrations to e2e database
run: npx prisma migrate deploy
- name: Run intake → completion e2e
run: npm run test:e2e

docker:
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
Expand Down
Loading
Loading