mirror of
https://github.com/willchen96/mike.git
synced 2026-07-26 23:51:08 +02:00
ci(e2e): apply migrations, serve prod build, skip LLM specs without a key
Three fixes that take the suite from ~everything-failing to green (verified end
to end against a local stack):
1. schema.sql lags the migrations (missing e.g. workflow_open_source_submissions,
which GET /workflows/:id queries → 500). Apply every dated migration on top of
schema.sql (idempotent; 0 errors on a fresh DB), grant service_role AFTER so
new tables are covered, then reload PostgREST. Fixes the workflow specs.
2. Serve a production build (next build + next start) instead of next dev. The dev
server's on-demand compilation (slow first hit → waitForResponse timeouts) and
hydration-error overlay (injects nextjs__container_errors DOM that pollutes text
locators, e.g. getByText('All') matching 'Call Stack') made the suite flaky.
Fixes tabular-reviews list render + the timing flakes.
3. LLM-dependent specs (chat rename/delete/submit, critical-path) require a model
key to send a message. Guard them with test.skip(!hasLlmKey) (e2e/llm.ts) and
expose ANTHROPIC_API_KEY to the Playwright process, so a keyless run is green on
the ~23 other specs and the LLM specs run + enforce only when the secret is set.
Local result (no key, with MinIO+LibreOffice as on ubuntu-latest): 23 passed,
4 skipped, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28a6cb6467
commit
7039461cd3
4 changed files with 69 additions and 14 deletions
59
.github/workflows/e2e.yml
vendored
59
.github/workflows/e2e.yml
vendored
|
|
@ -38,6 +38,11 @@ jobs:
|
||||||
# Defaults match e2e/auth.setup.ts; overriding the password here would break
|
# Defaults match e2e/auth.setup.ts; overriding the password here would break
|
||||||
# the valid-login specs, so only the base URL is pinned.
|
# the valid-login specs, so only the base URL is pinned.
|
||||||
PLAYWRIGHT_BASE_URL: http://localhost:3000
|
PLAYWRIGHT_BASE_URL: http://localhost:3000
|
||||||
|
# Exposed to the Playwright process (not just backend/.env) so the LLM-gated
|
||||||
|
# specs can skip themselves when no key is present — see e2e/llm.ts. When the
|
||||||
|
# secret is set they run and must pass; when absent they skip, so a keyless
|
||||||
|
# run (e.g. a fork PR) is still green on the other ~20 specs.
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|
@ -104,28 +109,42 @@ jobs:
|
||||||
uses: supabase/setup-cli@v1
|
uses: supabase/setup-cli@v1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
- name: Init + start Supabase, load schema
|
- name: Init + start Supabase, load schema + migrations
|
||||||
run: |
|
run: |
|
||||||
supabase init --force --with-vscode-settings=false || supabase init --force
|
supabase init --force --with-vscode-settings=false || supabase init --force
|
||||||
supabase start
|
supabase start
|
||||||
DB_URL=$(supabase status -o json | jq -r '.DB_URL')
|
DB_URL=$(supabase status -o json | jq -r '.DB_URL')
|
||||||
# README: "For a new Supabase database ... run backend/schema.sql". The
|
# 1) Base schema. README: "For a new Supabase database ... run
|
||||||
# schema file already includes the latest shape, so no migrations are
|
# backend/schema.sql". It is meant to be the latest shape but in practice
|
||||||
# replayed on a fresh CI database.
|
# lags the migrations (e.g. it is missing workflow_open_source_submissions,
|
||||||
|
# which GET /workflows/:id queries — a 500 that breaks the workflow specs).
|
||||||
psql "$DB_URL" -v ON_ERROR_STOP=1 -f backend/schema.sql
|
psql "$DB_URL" -v ON_ERROR_STOP=1 -f backend/schema.sql
|
||||||
# schema.sql revokes client grants (anon/authenticated) on purpose, but
|
# 2) Apply every dated migration on top. They are written to be safe to
|
||||||
# it assumes a HOSTED Supabase project where service_role already holds
|
# re-run (IF NOT EXISTS / ADD COLUMN IF NOT EXISTS), so the ones already
|
||||||
# full table access. On a fresh CLI stack loaded via psql, service_role
|
# in schema.sql are no-ops and the newer ones fill the gaps. Verified on a
|
||||||
# gets no grants on the newly-created public tables, so the backend —
|
# fresh CLI DB: all migrations apply with zero errors. Kept fail-tolerant
|
||||||
# which queries exclusively as service_role (lib/supabase.ts) — 500s with
|
# so one non-idempotent migration can't wedge the whole gate — mismatches
|
||||||
# "permission denied for table user_profiles" on the first write. Restore
|
# surface as a spec failure downstream, not an opaque CI abort.
|
||||||
# the production grant posture so the API behaves as it does in prod.
|
for m in $(ls backend/migrations/*.sql | sort); do
|
||||||
|
psql "$DB_URL" -v ON_ERROR_STOP=1 -f "$m" >/dev/null 2>&1 \
|
||||||
|
|| echo "::warning::migration returned a non-zero status (already applied?): $m"
|
||||||
|
done
|
||||||
|
# 3) schema.sql revokes client grants (anon/authenticated) on purpose, but
|
||||||
|
# assumes a HOSTED Supabase where service_role already holds full table
|
||||||
|
# access. On a fresh CLI stack loaded via psql, service_role gets no grants
|
||||||
|
# on the new public tables, so the backend — which queries exclusively as
|
||||||
|
# service_role (lib/supabase.ts) — 500s with "permission denied for table
|
||||||
|
# user_profiles" on the first write. Grant AFTER migrations so the tables
|
||||||
|
# they add are covered too.
|
||||||
psql "$DB_URL" -v ON_ERROR_STOP=1 <<'SQL'
|
psql "$DB_URL" -v ON_ERROR_STOP=1 <<'SQL'
|
||||||
GRANT USAGE ON SCHEMA public TO service_role;
|
GRANT USAGE ON SCHEMA public TO service_role;
|
||||||
GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role;
|
GRANT ALL ON ALL TABLES IN SCHEMA public TO service_role;
|
||||||
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO service_role;
|
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO service_role;
|
||||||
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO service_role;
|
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO service_role;
|
||||||
SQL
|
SQL
|
||||||
|
# 4) PostgREST cached its schema when `supabase start` booted it (before
|
||||||
|
# the DDL above). Tell it to reload so the new tables/grants are visible.
|
||||||
|
psql "$DB_URL" -v ON_ERROR_STOP=1 -c "NOTIFY pgrst, 'reload schema';"
|
||||||
|
|
||||||
- name: Wire env from Supabase
|
- name: Wire env from Supabase
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -161,17 +180,29 @@ jobs:
|
||||||
echo "R2_BUCKET_NAME=mike"
|
echo "R2_BUCKET_NAME=mike"
|
||||||
echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}"
|
echo "ANTHROPIC_API_KEY=${{ secrets.ANTHROPIC_API_KEY }}"
|
||||||
} >> backend/.env
|
} >> backend/.env
|
||||||
# Frontend reads NEXT_PUBLIC_* at dev-server boot.
|
# next build inlines NEXT_PUBLIC_* at build time, so these must be set
|
||||||
|
# before the "Build web" step below.
|
||||||
{
|
{
|
||||||
echo "NEXT_PUBLIC_SUPABASE_URL=$API_URL"
|
echo "NEXT_PUBLIC_SUPABASE_URL=$API_URL"
|
||||||
echo "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=$ANON_KEY"
|
echo "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=$ANON_KEY"
|
||||||
echo "NEXT_PUBLIC_API_BASE_URL=http://localhost:3001"
|
echo "NEXT_PUBLIC_API_BASE_URL=http://localhost:3001"
|
||||||
} > frontend/.env.local
|
} > frontend/.env.local
|
||||||
|
|
||||||
|
# Serve a PRODUCTION build, not `next dev`. Two reasons the dev server makes
|
||||||
|
# this suite flaky: (1) its on-demand route compilation makes the first hit
|
||||||
|
# of each page arbitrarily slow (20s waitForResponse assertions time out),
|
||||||
|
# and (2) its hydration-error overlay injects DOM (nextjs__container_errors…)
|
||||||
|
# that pollutes text locators — e.g. getByText('All') matched the overlay's
|
||||||
|
# "Call Stack"/"…fallback" text and failed strict mode. A production build has
|
||||||
|
# neither: no overlay, no JIT compile. Verified locally — flipping dev→start
|
||||||
|
# took the suite from intermittent overlay/timeout failures to green.
|
||||||
|
- name: Build web (production)
|
||||||
|
run: npm run build --prefix frontend
|
||||||
|
|
||||||
- name: Start backend API
|
- name: Start backend API
|
||||||
run: npm run dev --prefix backend &
|
run: npm run dev --prefix backend &
|
||||||
- name: Start web
|
- name: Start web (production server)
|
||||||
run: npm run dev --prefix frontend &
|
run: npm run start --prefix frontend &
|
||||||
|
|
||||||
- name: Wait for servers
|
- name: Wait for servers
|
||||||
run: npx wait-on http://localhost:3001/health http://localhost:3000 --timeout 120000
|
run: npx wait-on http://localhost:3001/health http://localhost:3000 --timeout 120000
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
* Test user: e2e@mike.local / E2eTestPass1!
|
* Test user: e2e@mike.local / E2eTestPass1!
|
||||||
*/
|
*/
|
||||||
import { test, expect, type Page } from "@playwright/test";
|
import { test, expect, type Page } from "@playwright/test";
|
||||||
|
import { hasLlmKey, LLM_SKIP_REASON } from "./llm";
|
||||||
|
|
||||||
/* ─── Helpers ────────────────────────────────────────────────────────────────── */
|
/* ─── Helpers ────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
|
@ -112,6 +113,7 @@ test("cold-load: direct URL to a chat triggers the getChat history load", async
|
||||||
/* ─── Test 2: rename a chat from sidebar ────────────────────────────────────── */
|
/* ─── Test 2: rename a chat from sidebar ────────────────────────────────────── */
|
||||||
|
|
||||||
test("rename chat: sidebar rename interaction updates the title", async ({ page }) => {
|
test("rename chat: sidebar rename interaction updates the title", async ({ page }) => {
|
||||||
|
test.skip(!hasLlmKey, LLM_SKIP_REASON);
|
||||||
// REGRESSION: fails if the renameChat API call or the optimistic title update in
|
// REGRESSION: fails if the renameChat API call or the optimistic title update in
|
||||||
// ChatHistoryContext.renameChatFn / SidebarChatItem.handleRenameSave is removed.
|
// ChatHistoryContext.renameChatFn / SidebarChatItem.handleRenameSave is removed.
|
||||||
|
|
||||||
|
|
@ -203,6 +205,7 @@ test("rename chat: sidebar rename interaction updates the title", async ({ page
|
||||||
/* ─── Test 3: delete a chat from sidebar ────────────────────────────────────── */
|
/* ─── Test 3: delete a chat from sidebar ────────────────────────────────────── */
|
||||||
|
|
||||||
test("delete chat: sidebar delete action removes the chat from history", async ({ page }) => {
|
test("delete chat: sidebar delete action removes the chat from history", async ({ page }) => {
|
||||||
|
test.skip(!hasLlmKey, LLM_SKIP_REASON);
|
||||||
// REGRESSION: fails if the deleteChat API call or the optimistic list removal in
|
// REGRESSION: fails if the deleteChat API call or the optimistic list removal in
|
||||||
// ChatHistoryContext.deleteChatFn (filter by chatId) is removed.
|
// ChatHistoryContext.deleteChatFn (filter by chatId) is removed.
|
||||||
|
|
||||||
|
|
@ -315,6 +318,7 @@ test("delete chat: sidebar delete action removes the chat from history", async (
|
||||||
/* ─── Test 4: project assistant — create new chat ───────────────────────────── */
|
/* ─── Test 4: project assistant — create new chat ───────────────────────────── */
|
||||||
|
|
||||||
test("project assistant: create a new chat and submit a question", async ({ page }) => {
|
test("project assistant: create a new chat and submit a question", async ({ page }) => {
|
||||||
|
test.skip(!hasLlmKey, LLM_SKIP_REASON);
|
||||||
// REGRESSION: fails if the project chat creation route is broken — specifically if
|
// REGRESSION: fails if the project chat creation route is broken — specifically if
|
||||||
// handleNewChat() in ProjectPage.tsx (lines 515-519) fails to call saveChat() or
|
// handleNewChat() in ProjectPage.tsx (lines 515-519) fails to call saveChat() or
|
||||||
// router.push to /projects/[id]/assistant/chat/[chatId]. (Verified by temporarily
|
// router.push to /projects/[id]/assistant/chat/[chatId]. (Verified by temporarily
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
* Prerequisite: auth.setup.ts has already saved the session to e2e/.auth/user.json
|
* Prerequisite: auth.setup.ts has already saved the session to e2e/.auth/user.json
|
||||||
*/
|
*/
|
||||||
import { test, expect, type Page } from "@playwright/test";
|
import { test, expect, type Page } from "@playwright/test";
|
||||||
|
import { hasLlmKey, LLM_SKIP_REASON } from "./llm";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const PDF_FIXTURE = path.join(__dirname, "fixtures/test.pdf");
|
const PDF_FIXTURE = path.join(__dirname, "fixtures/test.pdf");
|
||||||
|
|
@ -53,6 +54,7 @@ test("authenticated user lands on the assistant page", async ({ page }) => {
|
||||||
test("create project, upload PDF, ask a question and receive a response", async ({
|
test("create project, upload PDF, ask a question and receive a response", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
test.skip(!hasLlmKey, LLM_SKIP_REASON);
|
||||||
/* This end-to-end flow (create + upload + navigate + chat) is throttled by
|
/* This end-to-end flow (create + upload + navigate + chat) is throttled by
|
||||||
the local Supabase stack and needs far more than the 30s default. The
|
the local Supabase stack and needs far more than the 30s default. The
|
||||||
per-test `{ timeout }` option that test() accepts is silently ignored by
|
per-test `{ timeout }` option that test() accepts is silently ignored by
|
||||||
|
|
|
||||||
18
e2e/llm.ts
Normal file
18
e2e/llm.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Helpers for specs that require a live LLM turn.
|
||||||
|
*
|
||||||
|
* A few specs create/populate a chat by sending a message and awaiting a
|
||||||
|
* streamed answer. That only works when a model key is configured — in CI the
|
||||||
|
* `ANTHROPIC_API_KEY` secret, which `.github/workflows/e2e.yml` exposes to the
|
||||||
|
* Playwright process. When it is absent (a plain local run, or a fork PR with no
|
||||||
|
* secret access) the app blocks message send behind the ApiKeyMissingModal, so
|
||||||
|
* these specs would hang to their timeout.
|
||||||
|
*
|
||||||
|
* Guarding them with `test.skip(!hasLlmKey, LLM_SKIP_REASON)` keeps a keyless
|
||||||
|
* run green and fast on the ~20 specs that don't need a model, while still
|
||||||
|
* running — and enforcing — the LLM specs whenever the key is present.
|
||||||
|
*/
|
||||||
|
export const hasLlmKey = Boolean(process.env.ANTHROPIC_API_KEY);
|
||||||
|
|
||||||
|
export const LLM_SKIP_REASON =
|
||||||
|
"requires a model key — set the ANTHROPIC_API_KEY secret to run LLM-dependent specs";
|
||||||
Loading…
Add table
Add a link
Reference in a new issue