From ded70a5404657863e89a2021543f1f0b97c24d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Mon, 25 May 2026 23:27:44 +0200 Subject: [PATCH] ci: wire integration tests into the CI E2E job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five \`*.integration.test.ts\` files in apps/journal (explore, follow, demo-bot, notifications, notifications-fanout — 31 tests total) were gated behind \`EXPLORE_INTEGRATION=1\` / \`FOLLOW_INTEGRATION=1\` / \`DEMO_BOT_INTEGRATION=1\` / \`NOTIFICATIONS_INTEGRATION=1\`. The unit-test job doesn't have Postgres, so they correctly skipped there — but no CI job set the env vars, so they were effectively dead code. Added a step in the E2E job (which already has Postgres + schema pushed) that flips all four gates and runs the integration files serially (\`--no-file-parallelism\` — they share the schema and trip FK constraints if run in parallel; ~2.5s sequential anyway). Wiring them up surfaced two real issues, both fixed here: 1. **\`createRoute\` silently dropped \`input.visibility\`** — every caller passing \`visibility: \"public\"\` (including the demo-bot) was getting the column's \`private\` default. Spread now mirrors \`createActivity\`'s pattern. Demo-bot routes have actually been private in production all this time — they were rendering on the home feed only via the \`activity_published\` fan-out from the *activity*, not as visible *routes*. 2. **The demo-bot test's fetch stub was incomplete** — it stubbed \`.text()\` but the planner-session preflight calls \`.json()\`. The stub now branches on URL: \`/api/sessions\` returns \`{ sessionId: 'test-session' }\` JSON, the BRouter call returns the stub GPX text. Full repo: pnpm typecheck, pnpm lint, pnpm test all green (177 unit-test pass + 31 integration pass = 208 total, no skips). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 18 ++++++++++++++++++ .../app/lib/demo-bot.integration.test.ts | 19 ++++++++++++++++++- apps/journal/app/lib/routes.server.ts | 1 + 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c5ae58..b84f4b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -231,6 +231,24 @@ jobs: - name: Seed database run: pnpm db:seed + - name: Run integration tests + # These talk to real Postgres. The unit-test job has no DB so + # the `*.integration.test.ts` files skip there; this job has + # the DB up + schema pushed, so flip the gate env vars to "1" + # and let them run. Each gate is read by one file — see + # `runIntegration` in each test. + # + # --no-file-parallelism: integration tests share the journal + # schema and clean up by `DELETE FROM ... WHERE email LIKE + # '%@example.test'`. Parallel files step on each other's rows + # and trip FK constraints. Running sequentially is still <3s. + run: pnpm --filter @trails-cool/journal exec vitest run --no-file-parallelism --reporter=default app/lib/explore.integration.test.ts app/lib/follow.integration.test.ts app/lib/demo-bot.integration.test.ts app/lib/notifications.integration.test.ts app/jobs/notifications-fanout.integration.test.ts + env: + EXPLORE_INTEGRATION: "1" + FOLLOW_INTEGRATION: "1" + DEMO_BOT_INTEGRATION: "1" + NOTIFICATIONS_INTEGRATION: "1" + - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v5 diff --git a/apps/journal/app/lib/demo-bot.integration.test.ts b/apps/journal/app/lib/demo-bot.integration.test.ts index 7ed763a..9556fb3 100644 --- a/apps/journal/app/lib/demo-bot.integration.test.ts +++ b/apps/journal/app/lib/demo-bot.integration.test.ts @@ -82,9 +82,26 @@ describe.skipIf(!runIntegration)("demo-bot integration", () => { }); it("generateOneWalk inserts a public synthetic route + activity", async () => { + // createPlannerSession() calls resp.json() (returns { sessionId }); + // requestBrouterGpx() then calls resp.text() (returns the GPX body). + // Stub both so the chain completes without a real Planner running. vi.stubGlobal( "fetch", - vi.fn().mockResolvedValue({ ok: true, status: 200, text: async () => STUB_GPX }), + vi.fn().mockImplementation((input: string | URL) => { + const url = typeof input === "string" ? input : input.toString(); + if (url.endsWith("/api/sessions")) { + return Promise.resolve({ + ok: true, + status: 200, + json: async () => ({ sessionId: "test-session" }), + }); + } + return Promise.resolve({ + ok: true, + status: 200, + text: async () => STUB_GPX, + }); + }), ); const ownerId = await ensureDemoUser(); diff --git a/apps/journal/app/lib/routes.server.ts b/apps/journal/app/lib/routes.server.ts index cf9ef9d..d50e54e 100644 --- a/apps/journal/app/lib/routes.server.ts +++ b/apps/journal/app/lib/routes.server.ts @@ -55,6 +55,7 @@ export async function createRoute(ownerId: string, input: RouteInput) { elevationGain, elevationLoss, dayBreaks, + ...(input.visibility ? { visibility: input.visibility } : {}), ...(input.synthetic ? { synthetic: true } : {}), });