fix(e2e): harden card selectors for session resume
All checks were successful
Mirror to GitHub / mirror (push) Successful in 12s
CI / frontend (pull_request) Successful in 5m43s
CI / backend (pull_request) Successful in 10m21s
CI / e2e (pull_request) Successful in 11m23s

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
2026-04-25 16:42:33 -04:00
parent ede6eebf9a
commit 1e3a6cfa01
13 changed files with 76 additions and 65 deletions

View File

@@ -1,14 +1,13 @@
# CURRENT_TASK.md # 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 **Status:** in-progress
**Definition of Done:** **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 #150 (`fix/ci-workflow-config`) merged. `CI / backend (pull_request)`, `CI / frontend (pull_request)`, and `CI / e2e (pull_request)` show success before merge.
- [ ] PR #151 (`fix/ci-pytest-xdist`) merged. Backend CI on the merge commit completes in <6 min (was ~22 min serial).
- [ ] `CI / backend (pull_request)` added to required status checks on `main` in Gitea branch protection (frontend is already required). - [ ] `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:** **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`. - 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`.

View File

@@ -2,54 +2,52 @@
# HANDOFF.md # 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. ```ts
2. **Workflow correctness:** `DATABASE_TEST_URL` env, `actions/upload-artifact` v3 pin (Gitea Actions doesn't support v4+). page.locator('.bg-card').filter({ hasText: tree.name }).first()
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.
## Expected wall-clock on the next CI run On the session history page this matched the tree filter `<select>` first because the select options contain the same flow name, then the test waited forever for a `Resume` button inside the select.
| Job | Duration | Starts at | This session fixed that properly by adding stable test IDs to repeated cards and moving e2e tests off `.bg-card` selectors:
|---|---|---|
| backend | ~9 min (xdist on 4-core CI runner) | minute 0 |
| frontend | ~6 min | minute 0 |
| e2e | ~7 min (now self-builds) | minute 0 |
Total wall-clock: ~9 min (whichever job runs longest), down from 1h 14m. - `flow-session-card` in `SessionHistoryPage.tsx`
- `tree-card` in `TreeGridView.tsx` and `TreeListView.tsx`
- `share-card` in `MySharesPage.tsx`
The workflow was also hardened:
- Postgres service healthchecks now run `pg_isready -U postgres` instead of checking as `root`.
- The e2e frontend build now bakes `VITE_API_URL="${PLAYWRIGHT_API_ORIGIN}"`, matching the Playwright backend origin.
## Verification completed
- `git diff --check`
- Confirmed no remaining `.bg-card` selectors in `frontend/e2e/*.ts`.
- `docker exec -w /app resolutionflow_frontend npm run build`
- Ran migrations and test-user seed in the dev backend container.
- Focused Playwright verification in an Actions-like Ubuntu container:
- First `e2e/resume.spec.ts` passed.
- Then `e2e/history.spec.ts e2e/library.spec.ts e2e/library-start.spec.ts e2e/resume.spec.ts e2e/shares.spec.ts --project=chromium --workers=1` passed: `6 passed (1.3m)`.
## Immediate next steps ## Immediate next steps
1. **Watch PR #150's CI** on its latest sha (`261814a`). All three jobs should run concurrently and finish within ~9 min total. 1. Push the WIP commit from this session to PR #150.
2. **Merge PR #150** when all three jobs are green. 2. Watch PR #150 CI on the new SHA. Expected result: backend, frontend, and e2e all green.
3. **Enable backend gate** on `main` branch protection — append `"CI / backend (pull_request)"` to `status_check_contexts`: 3. Merge PR #150 when green.
```bash 4. Enable `CI / backend (pull_request)` as a required status check on `main`.
curl -X PATCH -H "Authorization: token $GITEA_TOKEN" \ 5. After at least one reliable green PR run, consider adding `CI / e2e (pull_request)` as required too.
"https://gitea.resolutionflow.com/api/v1/repos/chihlasm/resolutionflow/branch_protections/main" \
-H "Content-Type: application/json" \
-d '{"status_check_contexts": ["CI / frontend (pull_request)", "CI / backend (pull_request)"]}'
```
4. **Then enable `CI / e2e (pull_request)`** — same PATCH, append to the list. Verify e2e is reliably green for at least one PR run before locking it in.
PRs #151 (`fix/ci-pytest-xdist`) and #152 (`fix/e2e-test-selectors`) were closed as superseded by this PR.
## Uncommitted state
Working tree clean (after this handoff commit).
## Branch protection on main (current) ## Branch protection on main (current)
@@ -58,21 +56,9 @@ Working tree clean (after this handoff commit).
- Force-push blocked - Force-push blocked
- No review required (solo) - No review required (solo)
## Recently merged on main
- `f27f671` — PR #149: fix(ci): frontend lint to zero errors + test-DB schema fix + dev-deps installable
- `06593a4` — PR #148: fix(tests): repair two pre-existing bugs blocking backend CI
- `32fae2c` — PR #147: feat: FlowPilot migration — Phase 1-9 + Phase 9 bug fixes + QA fixture harness
- `16060d2` — PR #141: feat: PSA ticket management
## Open questions
- One known concern with `--maxfail=10`: if a single bad commit produces 11+ legitimate failures, CI bails before reporting them all. Acceptable trade-off — the alternative is burning 25 min on a structural break.
- pytest-xdist load distribution is the default file-scoped balance. If one worker consistently gets the slow tests, switch to `--dist worksteal` (xdist 3.x). Not worth tuning preemptively.
## Useful breadcrumbs ## Useful breadcrumbs
- `.gitea/workflows/ci.yml` contains the parallel backend/frontend/e2e workflow.
- `backend/scripts/seed_phase9_qa_fixtures.py` pre-bakes Phase 9 QA fixtures. - `backend/scripts/seed_phase9_qa_fixtures.py` pre-bakes Phase 9 QA fixtures.
- `.gstack/qa-reports/phase9-20260424-232700/REPORT.md` — full QA report from the FlowPilot session. - `.gstack/qa-reports/phase9-20260424-232700/REPORT.md` has the FlowPilot QA report.
- gstack is in team mode for this repo. `/browse` Chromium needs `CONTAINER=1` env (see `~/.claude/skills/gstack/browse/src/browser-manager.ts:188`). - Per-worker test DBs accumulate on the Postgres service. Cheap to leave around; cleanup if needed.
- Per-worker test DBs accumulate on the postgres service. Cheap to leave around; cleanup if it ever bothers anyone.

View File

@@ -12,6 +12,15 @@
--- ---
## 2026-04-25 16:41 EDT — Codex — Stabilize PR #150 e2e selectors
- Investigated the remaining PR #150 failure after backend and frontend CI were green. The e2e resume smoke test was not failing because of product behavior; it used `.bg-card` plus text filtering and matched the tree filter `<select>` 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 ## 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. - 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.

View File

@@ -23,7 +23,7 @@ jobs:
# would race — two backend/e2e jobs both binding 0.0.0.0:5432 → the # would race — two backend/e2e jobs both binding 0.0.0.0:5432 → the
# second fails with "port is already allocated". # second fails with "port is already allocated".
options: >- options: >-
--health-cmd pg_isready --health-cmd "pg_isready -U postgres"
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
@@ -148,7 +148,7 @@ jobs:
# would race — two backend/e2e jobs both binding 0.0.0.0:5432 → the # would race — two backend/e2e jobs both binding 0.0.0.0:5432 → the
# second fails with "port is already allocated". # second fails with "port is already allocated".
options: >- options: >-
--health-cmd pg_isready --health-cmd "pg_isready -U postgres"
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
--health-retries 5 --health-retries 5
@@ -193,7 +193,7 @@ jobs:
# immediately on a free runner. Adds ~1-2 min of build time, but # immediately on a free runner. Adds ~1-2 min of build time, but
# eliminates the artifact-upload mechanism entirely (no more # eliminates the artifact-upload mechanism entirely (no more
# v3/v4 GHES headaches) and saves ~5 min of waiting. # 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 - name: Install Playwright browser
run: cd frontend && npx playwright install --with-deps chromium run: cd frontend && npx playwright install --with-deps chromium

View File

@@ -34,7 +34,11 @@ test.describe('session history smoke tests', () => {
await page.getByPlaceholder('Search by ticket number...').fill(ticketNumber) await page.getByPlaceholder('Search by ticket number...').fill(ticketNumber)
await page.getByPlaceholder('Search by client name...').fill(clientName) 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).toBeVisible()
await expect(sessionCard.getByText(tree.name)).toBeVisible() await expect(sessionCard.getByText(tree.name)).toBeVisible()

View File

@@ -24,7 +24,7 @@ test.describe('flow library start-session smoke tests', () => {
await page.getByPlaceholder('Search flows...').fill(tree.name) await page.getByPlaceholder('Search flows...').fill(tree.name)
await page.getByRole('button', { name: 'Search', exact: true }).click() 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 expect(treeCard).toBeVisible()
await treeCard.getByRole('button', { name: /^Start(?: Session)?$/ }).click() await treeCard.getByRole('button', { name: /^Start(?: Session)?$/ }).click()

View File

@@ -20,7 +20,7 @@ test.describe('flow library smoke tests', () => {
await page.getByPlaceholder('Search flows...').fill(tree.name) await page.getByPlaceholder('Search flows...').fill(tree.name)
await page.getByRole('button', { name: 'Search', exact: true }).click() 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 { } finally {
await disposeApiContext(api) await disposeApiContext(api)
} }

View File

@@ -28,7 +28,7 @@ test.describe('session resume smoke tests', () => {
await page.getByRole('button', { name: 'Flow Sessions' }).click() await page.getByRole('button', { name: 'Flow Sessions' }).click()
// Active sub-tab is the default and surfaces in-progress sessions. // 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 expect(resumeCard).toBeVisible()
await resumeCard.getByRole('button', { name: 'Resume' }).first().click() await resumeCard.getByRole('button', { name: 'Resume' }).first().click()

View File

@@ -31,7 +31,7 @@ test.describe('shared session management smoke tests', () => {
).toBeVisible() ).toBeVisible()
await expect(page.getByText(share.share_name || '')).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() await shareCard.getByRole('button', { name: 'Revoke' }).click()
const confirmDialog = page.getByRole('dialog', { name: 'Revoke Share Link' }) const confirmDialog = page.getByRole('dialog', { name: 'Revoke Share Link' })

View File

@@ -34,6 +34,8 @@ export function TreeGridView({
{trees.map((tree) => ( {trees.map((tree) => (
<div <div
key={tree.id} key={tree.id}
data-testid="tree-card"
data-tree-id={tree.id}
className="relative bg-card border border-border rounded-2xl p-4 transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6" className="relative bg-card border border-border rounded-2xl p-4 transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6"
> >
<div className="mb-2 flex items-start justify-between gap-2"> <div className="mb-2 flex items-start justify-between gap-2">

View File

@@ -33,6 +33,8 @@ export function TreeListView({
{trees.map((tree) => ( {trees.map((tree) => (
<div <div
key={tree.id} key={tree.id}
data-testid="tree-card"
data-tree-id={tree.id}
className="flex items-center gap-4 bg-card border border-border rounded-2xl p-4 transition-all hover:border-primary/30 hover:shadow-xs" className="flex items-center gap-4 bg-card border border-border rounded-2xl p-4 transition-all hover:border-primary/30 hover:shadow-xs"
> >
{/* Left: Name and Description */} {/* Left: Name and Description */}

View File

@@ -161,7 +161,12 @@ export default function MySharesPage() {
const isCopied = copiedId === share.id const isCopied = copiedId === share.id
return ( return (
<div key={share.id} className="bg-card border border-border rounded-xl p-5"> <div
key={share.id}
data-testid="share-card"
data-share-id={share.id}
className="bg-card border border-border rounded-xl p-5"
>
{/* Top row: badge + name */} {/* Top row: badge + name */}
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<span className="inline-flex items-center gap-1.5 text-xs rounded-full px-2 py-0.5 bg-accent text-muted-foreground"> <span className="inline-flex items-center gap-1.5 text-xs rounded-full px-2 py-0.5 bg-accent text-muted-foreground">

View File

@@ -533,7 +533,11 @@ export default function SessionHistoryPage() {
)} )}
style={{ '--stagger-index': i } as React.CSSProperties} style={{ '--stagger-index': i } as React.CSSProperties}
> >
<div className="bg-card border border-border rounded-xl p-4 transition-all hover:border-[var(--color-border-hover)]"> <div
data-testid="flow-session-card"
data-session-id={session.id}
className="bg-card border border-border rounded-xl p-4 transition-all hover:border-[var(--color-border-hover)]"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">