From 1e3a6cfa01db51359d2e5d26cb1129f446c2ec9e Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 16:42:33 -0400 Subject: [PATCH] fix(e2e): harden card selectors for session resume Co-Authored-By: Codex --- .ai/CURRENT_TASK.md | 7 +- .ai/HANDOFF.md | 88 ++++++++----------- .ai/SESSION_LOG.md | 9 ++ .gitea/workflows/ci.yml | 6 +- frontend/e2e/history.spec.ts | 6 +- frontend/e2e/library-start.spec.ts | 2 +- frontend/e2e/library.spec.ts | 2 +- frontend/e2e/resume.spec.ts | 2 +- frontend/e2e/shares.spec.ts | 2 +- .../src/components/library/TreeGridView.tsx | 2 + .../src/components/library/TreeListView.tsx | 2 + frontend/src/pages/MySharesPage.tsx | 7 +- frontend/src/pages/SessionHistoryPage.tsx | 6 +- 13 files changed, 76 insertions(+), 65 deletions(-) diff --git a/.ai/CURRENT_TASK.md b/.ai/CURRENT_TASK.md index 0109ea2d..41635201 100644 --- a/.ai/CURRENT_TASK.md +++ b/.ai/CURRENT_TASK.md @@ -1,14 +1,13 @@ # CURRENT_TASK.md -**Task:** Land two stacked CI PRs and lock the backend gate on `main`. +**Task:** Land consolidated CI-recovery PR #150 and lock reliable CI gates on `main`. **Status:** in-progress **Definition of Done:** -- [ ] PR #150 (`fix/ci-workflow-config`) merged. Both `CI / backend (pull_request)` and `CI / frontend (pull_request)` show success on the merge commit. -- [ ] PR #151 (`fix/ci-pytest-xdist`) merged. Backend CI on the merge commit completes in <6 min (was ~22 min serial). +- [ ] PR #150 (`fix/ci-workflow-config`) merged. `CI / backend (pull_request)`, `CI / frontend (pull_request)`, and `CI / e2e (pull_request)` show success before merge. - [ ] `CI / backend (pull_request)` added to required status checks on `main` in Gitea branch protection (frontend is already required). -- [ ] Optional: `CI / e2e (pull_request)` confirmed clean and added to required checks. +- [ ] Optional: `CI / e2e (pull_request)` confirmed clean across at least one PR run and added to required checks. **Assumptions:** - The 8-core homelab Gitea Actions runner can support `-n auto` (8 xdist workers). If memory pressure shows up in CI, drop to `-n 4`. diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 51872b94..54367b03 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -2,54 +2,52 @@ # HANDOFF.md -**Last updated:** 2026-04-25 (America/New_York) +**Last updated:** 2026-04-25 16:41 EDT -**Active task:** Land PR #150 (the consolidated CI-recovery PR), then enable backend + e2e gates on `main`. See [CURRENT_TASK.md](CURRENT_TASK.md). +**Active task:** Land PR #150 (the consolidated CI-recovery PR), then enable backend and eventually e2e gates on `main`. See [CURRENT_TASK.md](CURRENT_TASK.md). -**Branch:** `fix/ci-workflow-config` → PR #150. PRs #151 and #152 were closed and consolidated into this branch — one PR is easier to land than three stacked ones, and the user got tired of waiting on serial CI runs of intermediate states. +**Branch:** `fix/ci-workflow-config` -> PR #150. PRs #151 and #152 were closed and consolidated into this branch. -**Runner setup:** Three Gitea Actions agents are registered on the homelab box, so `backend` / `frontend` / `e2e` jobs run truly in parallel. Combined with the xdist parallelization and the e2e decoupling in this PR, the previous 1h 14m wall-clock should drop to ~9 min. +## Current resume point -## What's on PR #150's branch (consolidated) +Latest PR #150 CI had backend and frontend green, but `CI / e2e (pull_request)` failed on the resume smoke test. -Seven CI-recovery commits that landed together because they were inter-dependent: +The failure was not product behavior. Playwright was using: -1. **Codex's `49f8856 wip(handoff): restore backend suite to green`** — fixes the 54 real backend test failures left after #149. -2. **Workflow correctness:** `DATABASE_TEST_URL` env, `actions/upload-artifact` v3 pin (Gitea Actions doesn't support v4+). -3. **Test-fix + cheap CI wins (`e976fb4`):** mocks `_extract_template_parameters` in `test_record_decision_persists_and_bumps_state_version` so it doesn't need an AI provider key; pip + npm caches; `--cov-report=term-missing` dropped (the custom display step parses JSON); `--maxfail=10` so structural breakage exits fast. -4. **Postgres port-collision fix (`1bd43ab`):** dropped `ports: 5432:5432` host mapping. With three Gitea runner agents now active, two parallel jobs would race on `0.0.0.0:5432`. Tests connect via the `postgres` service-DNS hostname, not the host, so the mapping wasn't actually needed. -5. **pytest-xdist with per-worker DBs (`7f71436`):** `pytest-xdist==3.6.1` added; `conftest.py` derives a per-worker DB URL from `PYTEST_XDIST_WORKER` and creates it synchronously on first import. Verified on PR #151's CI run before consolidation: 22m serial → 9m37s on the 4-core runner. -6. **Five e2e selector updates (`69f2a37`):** drift from the FlowPilot/PSA migration. `Sessions` → `Session History`, `Account Settings` → `Account Management`, `/assistant` accepts `/pilot`, "Flow Sessions" tab clicks for ticket/client filtering and Resume on `/sessions`. -7. **e2e decoupled from frontend (`261814a`):** dropped `needs: [frontend]` and the cross-job artifact handoff. e2e now builds its own frontend (npm ci + npm run build are already in the job). Adds ~1-2 min to the e2e job duration but removes the ~5 min of waiting for frontend to finish, and gets rid of the cross-job `actions/upload-artifact` mechanism entirely. e2e starts immediately on the third runner. +```ts +page.locator('.bg-card').filter({ hasText: tree.name }).first() +``` -## Expected wall-clock on the next CI run +On the session history page this matched the tree filter `` before the intended session card. +- Added stable test IDs to flow session, tree, and share cards, then updated affected e2e tests to target those cards instead of Tailwind class names. +- Hardened the CI workflow by making Postgres healthchecks authenticate as `postgres` and baking `VITE_API_URL="${PLAYWRIGHT_API_ORIGIN}"` into the e2e frontend build. +- Verified with `git diff --check`, frontend build in Docker, no remaining `.bg-card` e2e selectors, and focused Playwright runs in an Actions-like Ubuntu container: resume spec passed, then history/library/library-start/resume/shares passed (`6 passed`). +- Left for next session: push this WIP commit to PR #150, watch CI, merge when all three jobs are green, then enable backend branch protection and consider the e2e gate after a reliable green run. +- Files touched: `.gitea/workflows/ci.yml`, `frontend/e2e/history.spec.ts`, `frontend/e2e/library-start.spec.ts`, `frontend/e2e/library.spec.ts`, `frontend/e2e/resume.spec.ts`, `frontend/e2e/shares.spec.ts`, `frontend/src/components/library/TreeGridView.tsx`, `frontend/src/components/library/TreeListView.tsx`, `frontend/src/pages/MySharesPage.tsx`, `frontend/src/pages/SessionHistoryPage.tsx`, `.ai/HANDOFF.md`, `.ai/CURRENT_TASK.md`, `.ai/SESSION_LOG.md`. + ## 2026-04-25 12:00 America/New_York — Claude Code — Mock final AI-provider test, cache CI deps, parallelize backend with pytest-xdist - Diagnosed why CI was still red despite Codex's local 1076 passed: a single test (`test_record_decision_persists_and_bumps_state_version`) needed `ANTHROPIC_API_KEY` because the `decision: draft_template` path calls `TemplateExtractionService` → AI provider. Patched `_extract_template_parameters` with an `AsyncMock` so the test no longer depends on AI availability. Verified. diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 26275382..d3f2f8c9 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: # would race — two backend/e2e jobs both binding 0.0.0.0:5432 → the # second fails with "port is already allocated". options: >- - --health-cmd pg_isready + --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 @@ -148,7 +148,7 @@ jobs: # would race — two backend/e2e jobs both binding 0.0.0.0:5432 → the # second fails with "port is already allocated". options: >- - --health-cmd pg_isready + --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 @@ -193,7 +193,7 @@ jobs: # immediately on a free runner. Adds ~1-2 min of build time, but # eliminates the artifact-upload mechanism entirely (no more # v3/v4 GHES headaches) and saves ~5 min of waiting. - run: cd frontend && NODE_OPTIONS="--max-old-space-size=4096" npm run build + run: cd frontend && NODE_OPTIONS="--max-old-space-size=4096" VITE_API_URL="${PLAYWRIGHT_API_ORIGIN}" npm run build - name: Install Playwright browser run: cd frontend && npx playwright install --with-deps chromium diff --git a/frontend/e2e/history.spec.ts b/frontend/e2e/history.spec.ts index 7483ec64..7a41efe6 100644 --- a/frontend/e2e/history.spec.ts +++ b/frontend/e2e/history.spec.ts @@ -34,7 +34,11 @@ test.describe('session history smoke tests', () => { await page.getByPlaceholder('Search by ticket number...').fill(ticketNumber) await page.getByPlaceholder('Search by client name...').fill(clientName) - const sessionCard = page.locator('.bg-card').filter({ hasText: ticketNumber }).filter({ hasText: clientName }).first() + const sessionCard = page + .getByTestId('flow-session-card') + .filter({ hasText: ticketNumber }) + .filter({ hasText: clientName }) + .first() await expect(sessionCard).toBeVisible() await expect(sessionCard.getByText(tree.name)).toBeVisible() diff --git a/frontend/e2e/library-start.spec.ts b/frontend/e2e/library-start.spec.ts index 6ab716cb..61aa822e 100644 --- a/frontend/e2e/library-start.spec.ts +++ b/frontend/e2e/library-start.spec.ts @@ -24,7 +24,7 @@ test.describe('flow library start-session smoke tests', () => { await page.getByPlaceholder('Search flows...').fill(tree.name) await page.getByRole('button', { name: 'Search', exact: true }).click() - const treeCard = page.locator('.bg-card').filter({ hasText: tree.name }).first() + const treeCard = page.getByTestId('tree-card').filter({ hasText: tree.name }).first() await expect(treeCard).toBeVisible() await treeCard.getByRole('button', { name: /^Start(?: Session)?$/ }).click() diff --git a/frontend/e2e/library.spec.ts b/frontend/e2e/library.spec.ts index 04d694c0..8f544c51 100644 --- a/frontend/e2e/library.spec.ts +++ b/frontend/e2e/library.spec.ts @@ -20,7 +20,7 @@ test.describe('flow library smoke tests', () => { await page.getByPlaceholder('Search flows...').fill(tree.name) await page.getByRole('button', { name: 'Search', exact: true }).click() - await expect(page.getByText(tree.name)).toBeVisible() + await expect(page.getByTestId('tree-card').filter({ hasText: tree.name }).first()).toBeVisible() } finally { await disposeApiContext(api) } diff --git a/frontend/e2e/resume.spec.ts b/frontend/e2e/resume.spec.ts index 73211735..241aa3a3 100644 --- a/frontend/e2e/resume.spec.ts +++ b/frontend/e2e/resume.spec.ts @@ -28,7 +28,7 @@ test.describe('session resume smoke tests', () => { await page.getByRole('button', { name: 'Flow Sessions' }).click() // Active sub-tab is the default and surfaces in-progress sessions. - const resumeCard = page.locator('.bg-card').filter({ hasText: tree.name }).first() + const resumeCard = page.getByTestId('flow-session-card').filter({ hasText: tree.name }).first() await expect(resumeCard).toBeVisible() await resumeCard.getByRole('button', { name: 'Resume' }).first().click() diff --git a/frontend/e2e/shares.spec.ts b/frontend/e2e/shares.spec.ts index 78237c0f..1d288afa 100644 --- a/frontend/e2e/shares.spec.ts +++ b/frontend/e2e/shares.spec.ts @@ -31,7 +31,7 @@ test.describe('shared session management smoke tests', () => { ).toBeVisible() await expect(page.getByText(share.share_name || '')).toBeVisible() - const shareCard = page.locator('.bg-card').filter({ hasText: share.share_name || '' }).first() + const shareCard = page.getByTestId('share-card').filter({ hasText: share.share_name || '' }).first() await shareCard.getByRole('button', { name: 'Revoke' }).click() const confirmDialog = page.getByRole('dialog', { name: 'Revoke Share Link' }) diff --git a/frontend/src/components/library/TreeGridView.tsx b/frontend/src/components/library/TreeGridView.tsx index 13ca1293..1ec5ff02 100644 --- a/frontend/src/components/library/TreeGridView.tsx +++ b/frontend/src/components/library/TreeGridView.tsx @@ -34,6 +34,8 @@ export function TreeGridView({ {trees.map((tree) => (
diff --git a/frontend/src/components/library/TreeListView.tsx b/frontend/src/components/library/TreeListView.tsx index f172038c..ab3104b5 100644 --- a/frontend/src/components/library/TreeListView.tsx +++ b/frontend/src/components/library/TreeListView.tsx @@ -33,6 +33,8 @@ export function TreeListView({ {trees.map((tree) => (
{/* Left: Name and Description */} diff --git a/frontend/src/pages/MySharesPage.tsx b/frontend/src/pages/MySharesPage.tsx index 6feb4fdd..5aaab825 100644 --- a/frontend/src/pages/MySharesPage.tsx +++ b/frontend/src/pages/MySharesPage.tsx @@ -161,7 +161,12 @@ export default function MySharesPage() { const isCopied = copiedId === share.id return ( -
+
{/* Top row: badge + name */}
diff --git a/frontend/src/pages/SessionHistoryPage.tsx b/frontend/src/pages/SessionHistoryPage.tsx index 56699db7..a87c46a3 100644 --- a/frontend/src/pages/SessionHistoryPage.tsx +++ b/frontend/src/pages/SessionHistoryPage.tsx @@ -533,7 +533,11 @@ export default function SessionHistoryPage() { )} style={{ '--stagger-index': i } as React.CSSProperties} > -
+