From 8f7df2c0ef814d22a7c225733507e3fb631f9daa Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 03:28:54 -0400 Subject: [PATCH 01/15] fix(ci): set DATABASE_TEST_URL + downgrade upload-artifact to v3 (Gitea Actions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two CI-config issues blocking the gate from going green: 1. **Backend tests connect to localhost instead of postgres service.** conftest.py reads DATABASE_TEST_URL only — DATABASE_URL is intentionally not consulted (per dab740d's test-DB-isolation hardening — running pytest with DATABASE_URL set previously dropped the dev DB schema). The CI workflow only sets DATABASE_URL, so conftest falls back to its localhost default and every fixture-setup fails with `OSError: Connect call failed ('127.0.0.1', 5432)` — observed as 638 errors on the latest main run. Add DATABASE_TEST_URL pointing at the postgres service container. Same connection string as DATABASE_URL — the test DB and the app DB are the same physical postgres in CI; conftest's safety assertion is satisfied by the URL containing "test". 2. **Frontend artifact upload fails on Gitea Actions runner.** actions/upload-artifact@v4 (and v5) are not supported on Gitea Actions / GHES — the runner returns `GHESNotSupportedError: ... not currently supported on GHES`. Lint itself is now passing (0 errors after PR #149); the job exits 1 only because the upload step then fails. Pin upload-artifact + download-artifact to v3, the latest version compatible with Gitea Actions until they ship v4 support. After this lands, both backend and frontend CI gates should turn green — at which point we can also add backend to the required status checks on main. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/ci.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3fe3e2f8..cc471ba3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -28,6 +28,12 @@ jobs: env: DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/resolutionflow_test DATABASE_URL_SYNC: postgresql://postgres:postgres@postgres:5432/resolutionflow_test + # conftest.py reads DATABASE_TEST_URL only (DATABASE_URL is intentionally + # not consulted after the dab740d test-isolation hardening). The CI test + # DB is the same postgres service, so point DATABASE_TEST_URL at it + # explicitly — without this, conftest falls back to localhost:5432 and + # all tests fail at fixture setup with "connection refused". + DATABASE_TEST_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/resolutionflow_test SECRET_KEY: ci-test-secret-key-not-for-production DEBUG: "true" APP_NAME: ResolutionFlow @@ -88,7 +94,7 @@ jobs: run: cd frontend && NODE_OPTIONS="--max-old-space-size=4096" npm run build - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: frontend-dist path: frontend/dist @@ -132,7 +138,7 @@ jobs: run: cd frontend && npm ci - name: Download frontend build - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 with: name: frontend-dist path: frontend/dist @@ -145,7 +151,7 @@ jobs: - name: Upload Playwright report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: playwright-report path: | -- 2.49.1 From 208ec996d504b5f6a172fe9ffe5494709d3709b2 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 03:36:54 -0400 Subject: [PATCH 02/15] =?UTF-8?q?docs(ai):=20handoff=20for=20Codex=20?= =?UTF-8?q?=E2=80=94=20CI=20recovery=20+=2054=20real=20backend=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates HANDOFF.md, CURRENT_TASK.md, and SESSION_LOG.md so the next session has accurate resume state. Summary of where things are: - PR #141 (PSA tickets), PR #147 (FlowPilot Phase 1-9), PR #148 (CI fixes part 1), PR #149 (CI fixes part 2) all merged to main in this session. - Branch protection enabled on main: PR-only, CI / frontend required. - PR #150 (this branch) is the last CI-config PR — adds DATABASE_TEST_URL to the workflow and pins upload-artifact to v3. - Next session: watch #150's CI, merge if green, add CI / backend to required checks, then start on the 54 real backend test failures. Co-Authored-By: Claude Opus 4.7 --- .ai/CURRENT_TASK.md | 37 +++++++++--------------- .ai/HANDOFF.md | 68 +++++++++++++++++++++++++++++++-------------- .ai/SESSION_LOG.md | 14 ++++++++++ 3 files changed, 74 insertions(+), 45 deletions(-) diff --git a/.ai/CURRENT_TASK.md b/.ai/CURRENT_TASK.md index d9e5ac68..383fff86 100644 --- a/.ai/CURRENT_TASK.md +++ b/.ai/CURRENT_TASK.md @@ -1,33 +1,22 @@ # CURRENT_TASK.md -**Task:** none — replace this file when starting the next real task. +**Task:** Restore a fully green CI gate on `main` and lock it via branch protection so future merges can't introduce silent rot. -**Status:** not-started - -**Definition of Done:** n/a - -**Assumptions:** n/a - -**Out of scope:** n/a - ---- - - +- New feature work on FlowPilot (Phase 10+) or PSA — keep this branch focused on CI debt. +- Frontend lint warnings (23 remain after #149; they're missing-deps in useEffect, opt-in cleanup later). +- RLS test suite (`test_rls_isolation.py`) — gated behind `RUN_RLS_TESTS=1` and not in the default CI run. diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 7fc03fbd..81147067 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -2,34 +2,60 @@ # HANDOFF.md -**Last updated:** 2026-04-24 (America/New_York) +**Last updated:** 2026-04-25 (America/New_York) -**Active task:** None — see [CURRENT_TASK.md](CURRENT_TASK.md). Replace it when picking up the next real task. +**Active task:** Restore green CI gate on `main` and address the 54 real backend test failures. See [CURRENT_TASK.md](CURRENT_TASK.md). -**Branch:** `feat/flowpilot-migration` — a long-running FlowPilot Phase 9 feature branch. The recent AI-handoff migration commits ride on this branch (not on their own branch); they'll merge to `main` whenever Phase 9 does. +**Branch:** `fix/ci-workflow-config` — a small workflow-only PR (#150) that should turn CI green on the next run. After #150 merges, follow-up work is on a fresh branch off main. -**Branch state:** 3 commits ahead of `origin/feat/flowpilot-migration`: +## Where the previous session left off -- `b3be1e0 chore: ignore .remember/ skill runtime state` -- `b3506b5 docs(pilot): phase 9 review issues` -- `b14a16a chore(tests): gate RLS tests behind RUN_RLS_TESTS flag` +FlowPilot Phase 1-9 (PR #147) and PSA ticket management (PR #141) are both on main. Two CI-recovery PRs (#148 and #149) followed, fixing 4 real bugs and bringing the backend test suite from `482 passed / 488 errors / 44 failed` → `1018 passed / 4 errors / 54 failed`. -Earlier in this session (already pushed to origin): +CI on main still showed red after #149 because of two workflow-config issues unmasked once the code-side rot was cleared. PR #150 fixes both. -- `9c8ba29 fix(ai): correct stale role-hierarchy and file-listing claims` -- `bee8690 chore(ai): migrate to dual-agent handoff system` -- `e110fed chore: snapshot CLAUDE.md before ai-handoff migration` (tag: `pre-ai-handoff`) +## Currently open -**Where I left off:** -- File: n/a — nothing mid-edit. -- Next intended action: push the 3 unpushed commits when ready (`git push`), then start the next real task (replace `CURRENT_TASK.md`, update this file). +- **PR #150** — `fix/ci-workflow-config` → main. Mergeable. Two changes: + 1. Add `DATABASE_TEST_URL` env to the backend job. `conftest.py` reads `DATABASE_TEST_URL` only — `DATABASE_URL` is intentionally not consulted (see `dab740d`'s safety hardening). Without this env, conftest falls back to its `localhost:5432` default and every fixture-setup fails with `Connect call failed ('127.0.0.1', 5432)` — observed as 638 errors on the `f27f671` run. + 2. Pin `actions/upload-artifact` + `actions/download-artifact` to `v3`. Gitea Actions doesn't support v4+ (`GHESNotSupportedError`). Lint itself passes already after #149; the job exited 1 only on the upload step. -**Uncommitted state:** -- Working tree is clean. +## Immediate next steps -**Immediate next steps:** -1. `git push` to publish the 3 local commits (cleanup batch). -2. When starting the next real feature task: replace `CURRENT_TASK.md` with actual goal/DoD, rewrite this file's resume section. +1. Watch PR #150's CI run on its head sha. Both `CI / backend (pull_request)` and `CI / frontend (pull_request)` should now succeed. If they do, merge it. +2. After #150 merges, add `CI / backend (pull_request)` to required status checks on main: + ``` + PATCH /repos/chihlasm/resolutionflow/branch_protections/main + { "status_check_contexts": ["CI / frontend (pull_request)", "CI / backend (pull_request)"] } + ``` + ($GITEA_TOKEN is in `.claude/settings.local.json`.) +3. Start on the 54 real backend test failures. Suggested approach: read 3-5 failing tests, find the dominant root cause (likely fixture-scoping or DB-cleanup leak — one sample seen this session was `test_psa_writeback_phase4` failing with `duplicate key value violates unique constraint "ix_users_email"`, which points at the `test_user` fixture being called twice in a test path). Fix categories in atomic commits. -**Open questions / blockers:** -- None. The dual-agent handoff system is live and has survived one Codex review round (see DECISIONS.md 2026-04-24 entry; corrections in `9c8ba29`). +## Uncommitted state + +Working tree clean (after this handoff commit). + +## Branch protection on main + +Enabled in this session: +- PR-only merges +- `CI / frontend (pull_request)` required +- Force-push blocked +- No review required (solo repo) + +## Recently merged on main (in order) + +- `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 — /tickets page, detail panel, AI ticket creation + +## Open questions + +- The 54 real backend failures haven't been root-caused yet. One sample (the unique-email violation) suggests a test-fixture leak. Need to read more failing tests before writing the cleanup PR — don't assume one root cause. + +## Useful breadcrumbs + +- `backend/scripts/seed_phase9_qa_fixtures.py` (new) pre-bakes 4 ai_sessions × 4 suggested_fixes covering the four backend states the AI orchestrator must produce. Use it for any future Phase 9 QA pass instead of hoping the AI emits a `SUGGEST_FIX`. +- `.gstack/qa-reports/phase9-20260424-232700/REPORT.md` — full QA report from this session, with screenshots showing the four Phase 9 layout/state bugs that were fixed. +- `gstack` is now in team mode for this repo (`.claude/settings.json` + `.claude/hooks/check-gstack.sh`); the `/browse` Chromium needs `CONTAINER=1` env var to add `--no-sandbox` (see `~/.claude/skills/gstack/browse/src/browser-manager.ts:188`). diff --git a/.ai/SESSION_LOG.md b/.ai/SESSION_LOG.md index 2f30f2f3..d73b4c7f 100644 --- a/.ai/SESSION_LOG.md +++ b/.ai/SESSION_LOG.md @@ -12,6 +12,20 @@ --- +## 2026-04-25 02:00 America/New_York — Claude Code — Land FlowPilot + PSA, recover CI from 488 errors to ~4 + +- Started session by completing pending FlowPilot Phase 9 QA: ran `/qa` against the seeded fixtures, found and fixed four latent layout/state bugs (`ResolutionNotePreview` off-screen, `TemplateMatchPanel` deadlock when TaskLane closed, `EscalateInterceptDialog` clipped above viewport, `seed_test_users.py` `cancel_at_period_end` NOT NULL crash). Added a new fixture seeder `backend/scripts/seed_phase9_qa_fixtures.py` that pre-bakes the four backend states the AI orchestrator needs to emit, so future QA can exercise all 7 conditional Phase 9 components without depending on stochastic AI behavior. +- Discovered PR #141 (PSA ticket management) and `feat/flowpilot-migration` had 5 overlapping files but only 2 real conflicts (`CLAUDE.md`, `AssistantChatPage.tsx`). Conflicts were both additive — concatenated rather than chose-a-side. +- Merged PSA first (PR #141), then merged FlowPilot (PR #147), each through Gitea API. `tsc -b` clean and visual smoke-test confirmed PSA's Tickets sidebar coexists with Phase 9 ProposalBanner. +- Discovered main had been merging through a broken CI gate for several merges. Initially recommended "stop the line, fix CI before shipping." After scoping the actual rot (~50% of tests red, ~600 errors on a clean run), reversed the recommendation: ship the queue first because FlowPilot itself carried significant test-infra repairs that would be duplicated work on a fresh recovery branch. +- PR #148: two surgical fixes to main (network_diagrams JSONB `server_default` triple-quote bug, deprecated session-scoped `event_loop` fixture in conftest). +78 passing / -114 errors. +- PR #149: frontend lint `20 errors → 0`, `requirements-dev.txt` pytest pin bumped to satisfy `pytest-asyncio==0.24.0`'s `pytest>=8.2`, and a one-line `from app import models as _models` in conftest that registers all ~60 models with `Base.metadata` before `create_all`. The conftest fix collapsed 484 of the remaining 488 backend errors. `1018 passed / 4 errors / 54 failed` after. +- Enabled Gitea branch protection on `main`: PR-only merges, `CI / frontend (pull_request)` required, force-push blocked, no review required. +- Discovered CI on the merge commit STILL showed red despite local pytest being mostly green. Root cause: workflow only set `DATABASE_URL`, but conftest reads only `DATABASE_TEST_URL` (per `dab740d`'s safety hardening). 638 connection-refused errors on every fixture setup. Plus `actions/upload-artifact@v4` not supported by Gitea Actions. PR #150 fixes both. +- Left for next session: merge PR #150 once CI confirms green, add `CI / backend (pull_request)` to required status checks, then root-cause and fix the 54 real backend test failures (one sample seen — `test_user` fixture leaking across calls causing duplicate-email violations). +- Files touched (committed): `backend/scripts/seed_test_users.py`, `backend/scripts/seed_phase9_qa_fixtures.py` (new), `backend/app/models/network_diagram.py`, `backend/tests/conftest.py`, `backend/requirements-dev.txt`, `frontend/src/components/pilot/ResolutionNotePreview.tsx`, `frontend/src/components/pilot/EscalateInterceptDialog.tsx`, `frontend/src/components/pilot/ScriptBuilderTab.tsx`, `frontend/src/pages/AssistantChatPage.tsx`, `frontend/src/pages/FlowPilotSessionPage.tsx`, `frontend/src/pages/TicketsPage.tsx`, `frontend/src/hooks/useFlowPilotSession.ts`, `frontend/src/hooks/useMediaQuery.ts`, `frontend/src/components/dashboard/TicketQueue.tsx`, `frontend/src/components/network/nodes/DeviceNode.tsx`, `frontend/src/components/network/nodes/GroupNode.tsx`, `frontend/src/components/routing/AssistantSessionRedirect.tsx` (new), `frontend/src/router.tsx`, `.gitea/workflows/ci.yml`, `.claude/settings.json` (new), `.claude/hooks/check-gstack.sh` (new), `.gitignore`, `CLAUDE.md`, `.gstack/qa-reports/phase9-*/` (QA artifacts). +- Net merges to main: PR #141 (PSA), PR #147 (FlowPilot), PR #148 (CI fixes part 1), PR #149 (CI fixes part 2). PR #150 still open at session end. + ## 2026-04-24 — Claude Code — Migrate to dual-agent handoff system - Split CLAUDE.md into `.ai/PROJECT_CONTEXT.md` + shared-protocol root files (`CLAUDE.md`, `AGENTS.md`). -- 2.49.1 From 49f88569da79e536799d36f6bade22dd904de40b Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 06:13:23 -0400 Subject: [PATCH 03/15] wip(handoff): restore backend suite to green Co-Authored-By: Codex --- .ai/HANDOFF.md | 84 ++++++++++--------- .ai/SESSION_LOG.md | 9 ++ .gitea/workflows/ci.yml | 5 ++ backend/Dockerfile.dev | 8 +- backend/app/api/endpoints/folders.py | 1 + backend/app/api/endpoints/script_builder.py | 1 + backend/app/api/endpoints/shares.py | 3 +- backend/app/api/router.py | 3 +- backend/app/models/ai_session.py | 15 +++- backend/app/schemas/user.py | 4 +- .../app/services/assistant_chat_service.py | 3 +- .../services/resolution_output_generator.py | 5 +- .../app/services/script_builder_service.py | 2 + backend/pytest.ini | 3 + backend/tests/conftest.py | 17 ++++ backend/tests/test_ai_endpoints.py | 14 +++- backend/tests/test_branch_manager.py | 5 +- backend/tests/test_psa_writeback_phase4.py | 1 + backend/tests/test_script_builder.py | 33 ++++---- backend/tests/test_session_branches_api.py | 4 +- backend/tests/test_session_resolutions_api.py | 1 + backend/tests/test_session_sharing.py | 2 +- .../tests/test_session_suggested_fixes_api.py | 1 + backend/tests/test_tenant_isolation_p0.py | 9 +- backend/tests/test_tree_sharing.py | 7 +- backend/tests/test_uploads.py | 2 +- 26 files changed, 165 insertions(+), 77 deletions(-) diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 81147067..0b896b7e 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -2,60 +2,62 @@ # HANDOFF.md -**Last updated:** 2026-04-25 (America/New_York) +**Last updated:** 2026-04-25 06:12 EDT -**Active task:** Restore green CI gate on `main` and address the 54 real backend test failures. See [CURRENT_TASK.md](CURRENT_TASK.md). +**Active task:** Restore green CI gate on `main` and lock it via branch protection. See [CURRENT_TASK.md](CURRENT_TASK.md). -**Branch:** `fix/ci-workflow-config` — a small workflow-only PR (#150) that should turn CI green on the next run. After #150 merges, follow-up work is on a fresh branch off main. +**Branch:** `fix/ci-workflow-config` -## Where the previous session left off +## Current state -FlowPilot Phase 1-9 (PR #147) and PSA ticket management (PR #141) are both on main. Two CI-recovery PRs (#148 and #149) followed, fixing 4 real bugs and bringing the backend test suite from `482 passed / 488 errors / 44 failed` → `1018 passed / 4 errors / 54 failed`. +Previous session fixed the 54 real backend failures left after #149. The default backend suite is now green locally: -CI on main still showed red after #149 because of two workflow-config issues unmasked once the code-side rot was cleared. PR #150 fixes both. +```bash +docker exec resolutionflow_backend bash -lc 'pytest --override-ini="addopts=" -q > /tmp/full-backend.log 2>&1; code=$?; tail -n 160 /tmp/full-backend.log; exit $code' +# 1076 passed, 35 deselected in 1347.41s (0:22:27) +``` -## Currently open +Targeted validation also passed: -- **PR #150** — `fix/ci-workflow-config` → main. Mergeable. Two changes: - 1. Add `DATABASE_TEST_URL` env to the backend job. `conftest.py` reads `DATABASE_TEST_URL` only — `DATABASE_URL` is intentionally not consulted (see `dab740d`'s safety hardening). Without this env, conftest falls back to its `localhost:5432` default and every fixture-setup fails with `Connect call failed ('127.0.0.1', 5432)` — observed as 638 errors on the `f27f671` run. - 2. Pin `actions/upload-artifact` + `actions/download-artifact` to `v3`. Gitea Actions doesn't support v4+ (`GHESNotSupportedError`). Lint itself passes already after #149; the job exited 1 only on the upload step. +- `tests/test_session_resolutions_api.py tests/test_session_sharing.py tests/test_session_suggested_fixes_api.py tests/test_survey.py tests/test_tenant_isolation_p0.py tests/test_tree_sharing.py tests/test_trees.py::TestTrees::test_delete_tree_cleans_up_folder_and_tag_assignments tests/test_uploads.py::test_delete_upload_forbidden_for_non_owner` → `73 passed` +- PDF export tests → `3 passed` +- Prompt/PSA/resolution/script-builder subset → `14 passed` +- Admin/AI/branch subsets → `11 passed` + +## What changed + +Production fixes: + +- CI/backend dev image now installs WeasyPrint system libraries. +- Public share-token and survey routes are mounted outside tenant auth; protected share management remains tenant-protected. +- Folder creation now persists `UserFolder.account_id`. +- Script Builder save-to-library now persists `ScriptTemplate.account_id`. +- Resolution output generation eager-loads `AISession.steps` to avoid async lazy-load `MissingGreenlet`. +- AI session model now declares the generated `search_vector` column already present in Alembic, so `create_all` test schemas match runtime migrations. +- Direct account-role update now rejects `"owner"`; ownership changes must use the transfer path. +- Assistant prompt marker examples no longer include a literal executable `create_spin_off_ticket` payload. + +Test/harness fixes: + +- Test seeds updated for tenant-scoped `account_id` columns on sessions, branches, resolution outputs, script templates, PSA connections, folders, schedules, and categories. +- Tests aligned with 404-not-403 resource-hiding policy. +- Disabled-AI tests now restore both Anthropic and Google key settings. +- Pytest harness closes pytest-asyncio's leftover clean loop and ignores known unclosed asyncio/asyncpg teardown ResourceWarnings that otherwise appear at arbitrary later setup points under `filterwarnings = error`. ## Immediate next steps -1. Watch PR #150's CI run on its head sha. Both `CI / backend (pull_request)` and `CI / frontend (pull_request)` should now succeed. If they do, merge it. -2. After #150 merges, add `CI / backend (pull_request)` to required status checks on main: - ``` +1. Commit current working tree if not already committed with trailer: + `Co-Authored-By: Codex `. +2. Check PR #150 status on Gitea. If both `CI / backend (pull_request)` and `CI / frontend (pull_request)` are green, merge it. +3. After #150 merges, add `CI / backend (pull_request)` to required status checks on main: + ```bash PATCH /repos/chihlasm/resolutionflow/branch_protections/main { "status_check_contexts": ["CI / frontend (pull_request)", "CI / backend (pull_request)"] } ``` - ($GITEA_TOKEN is in `.claude/settings.local.json`.) -3. Start on the 54 real backend test failures. Suggested approach: read 3-5 failing tests, find the dominant root cause (likely fixture-scoping or DB-cleanup leak — one sample seen this session was `test_psa_writeback_phase4` failing with `duplicate key value violates unique constraint "ix_users_email"`, which points at the `test_user` fixture being called twice in a test path). Fix categories in atomic commits. - -## Uncommitted state - -Working tree clean (after this handoff commit). - -## Branch protection on main - -Enabled in this session: -- PR-only merges -- `CI / frontend (pull_request)` required -- Force-push blocked -- No review required (solo repo) - -## Recently merged on main (in order) - -- `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 — /tickets page, detail panel, AI ticket creation + `$GITEA_TOKEN` is in `.claude/settings.local.json`. +4. Run/confirm frontend lint if needed for the final DoD item (`npm run lint` was already green after #149, but this session did not rerun it). ## Open questions -- The 54 real backend failures haven't been root-caused yet. One sample (the unique-email violation) suggests a test-fixture leak. Need to read more failing tests before writing the cleanup PR — don't assume one root cause. - -## Useful breadcrumbs - -- `backend/scripts/seed_phase9_qa_fixtures.py` (new) pre-bakes 4 ai_sessions × 4 suggested_fixes covering the four backend states the AI orchestrator must produce. Use it for any future Phase 9 QA pass instead of hoping the AI emits a `SUGGEST_FIX`. -- `.gstack/qa-reports/phase9-20260424-232700/REPORT.md` — full QA report from this session, with screenshots showing the four Phase 9 layout/state bugs that were fixed. -- `gstack` is now in team mode for this repo (`.claude/settings.json` + `.claude/hooks/check-gstack.sh`); the `/browse` Chromium needs `CONTAINER=1` env var to add `--no-sandbox` (see `~/.claude/skills/gstack/browse/src/browser-manager.ts:188`). +- PR #150 was not rechecked or merged in this session. +- Branch protection was not updated in this session. diff --git a/.ai/SESSION_LOG.md b/.ai/SESSION_LOG.md index d73b4c7f..0da1f81d 100644 --- a/.ai/SESSION_LOG.md +++ b/.ai/SESSION_LOG.md @@ -12,6 +12,15 @@ --- +## 2026-04-25 06:12 EDT — Codex — Fix backend suite to green + +- Fixed the real backend failures left after the CI-infra cleanup: tenant-scoped seed drift, missing production `account_id` writes, public route mounting for survey/share links, Script Builder library saves, resolution output async loading, AI search schema metadata, disabled-AI fixture leakage, and prompt marker guardrails. +- Added backend CI/dev system packages required by WeasyPrint PDF export. +- Stabilized the pytest harness for pytest-asyncio/asyncpg teardown ResourceWarnings under `filterwarnings = error`. +- Verified `pytest --override-ini="addopts=" -q` inside `resolutionflow_backend`: `1076 passed, 35 deselected in 1347.41s`. +- Left for next session: commit/push if needed, check and merge PR #150 when Gitea CI is green, add backend CI as a required branch-protection check, and rerun frontend lint if final DoD requires it. +- Files touched: `.gitea/workflows/ci.yml`, `backend/Dockerfile.dev`, `backend/app/api/endpoints/folders.py`, `backend/app/api/endpoints/script_builder.py`, `backend/app/api/endpoints/shares.py`, `backend/app/api/router.py`, `backend/app/models/ai_session.py`, `backend/app/schemas/user.py`, `backend/app/services/assistant_chat_service.py`, `backend/app/services/resolution_output_generator.py`, `backend/app/services/script_builder_service.py`, `backend/pytest.ini`, `backend/tests/conftest.py`, and focused backend tests. + ## 2026-04-25 02:00 America/New_York — Claude Code — Land FlowPilot + PSA, recover CI from 488 errors to ~4 - Started session by completing pending FlowPilot Phase 9 QA: ran `/qa` against the seeded fixtures, found and fixed four latent layout/state bugs (`ResolutionNotePreview` off-screen, `TemplateMatchPanel` deadlock when TaskLane closed, `EscalateInterceptDialog` clipped above viewport, `seed_test_users.py` `cancel_at_period_end` NOT NULL crash). Added a new fixture seeder `backend/scripts/seed_phase9_qa_fixtures.py` that pre-bakes the four backend states the AI orchestrator needs to emit, so future QA can exercise all 7 conditional Phase 9 components without depending on stochastic AI behavior. diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index cc471ba3..0ea10f73 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -43,6 +43,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install system dependencies + run: | + apt-get update + apt-get install -y libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev libffi-dev libjpeg-dev zlib1g-dev + - name: Install dependencies run: pip install --break-system-packages -r backend/requirements.txt -r backend/requirements-dev.txt diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index d8df5f59..04e1b0a5 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -5,6 +5,12 @@ WORKDIR /app RUN apt-get update && apt-get install -y \ gcc \ libpq-dev \ + libpango1.0-dev \ + libcairo2-dev \ + libgdk-pixbuf-2.0-dev \ + libffi-dev \ + libjpeg-dev \ + zlib1g-dev \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt requirements-dev.txt ./ @@ -12,4 +18,4 @@ RUN pip install --no-cache-dir -r requirements-dev.txt EXPOSE 8000 -CMD [ "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" ] \ No newline at end of file +CMD [ "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload" ] diff --git a/backend/app/api/endpoints/folders.py b/backend/app/api/endpoints/folders.py index 7cb3b17e..c39ac8ee 100644 --- a/backend/app/api/endpoints/folders.py +++ b/backend/app/api/endpoints/folders.py @@ -194,6 +194,7 @@ async def create_folder( new_folder = UserFolder( user_id=current_user.id, + account_id=current_user.account_id, name=folder_data.name, color=folder_data.color, icon=folder_data.icon, diff --git a/backend/app/api/endpoints/script_builder.py b/backend/app/api/endpoints/script_builder.py index f028ae28..55eab8fa 100644 --- a/backend/app/api/endpoints/script_builder.py +++ b/backend/app/api/endpoints/script_builder.py @@ -260,6 +260,7 @@ async def save_to_library( category_id=data.category_id, share_with_team=data.share_with_team, user_id=current_user.id, + account_id=current_user.account_id, team_id=current_user.team_id, script_body=data.script_body, parameters_schema=data.parameters_schema, diff --git a/backend/app/api/endpoints/shares.py b/backend/app/api/endpoints/shares.py index ca04dadf..7529bd3b 100644 --- a/backend/app/api/endpoints/shares.py +++ b/backend/app/api/endpoints/shares.py @@ -20,6 +20,7 @@ from app.core.audit import log_audit from app.core.rate_limit import limiter router = APIRouter(tags=["shares"]) +public_router = APIRouter(tags=["shares"]) def build_share_response(share: SessionShare) -> ShareResponse: @@ -206,7 +207,7 @@ async def _get_optional_user(request: Request, db: AsyncSession) -> Optional[Use return None -@router.get("/share/{share_token}", response_model=SharePublicView) +@public_router.get("/share/{share_token}", response_model=SharePublicView) @limiter.limit("30/minute") async def access_share( share_token: str, diff --git a/backend/app/api/router.py b/backend/app/api/router.py index c831a5d0..5a51bdd9 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -78,9 +78,11 @@ api_router = APIRouter() # --------------------------------------------------------------------------- api_router.include_router(auth.router) api_router.include_router(shared.router) # Public share links (no auth) +api_router.include_router(shares.public_router) # Public session share links (optional auth) api_router.include_router(beta_signup.router) api_router.include_router(webhooks.router) # Stripe webhook receiver api_router.include_router(public_templates.router) # Public gallery (no auth, rate-limited) +api_router.include_router(survey.router) # Public survey flow (no auth, rate-limited) # --------------------------------------------------------------------------- # Admin endpoints — super_admin only @@ -125,7 +127,6 @@ api_router.include_router(ai_fix.router, dependencies=_tenant_deps) api_router.include_router(ai_chat.router, dependencies=_tenant_deps) api_router.include_router(copilot.router, dependencies=_tenant_deps) api_router.include_router(assistant_chat.router, dependencies=_tenant_deps) -api_router.include_router(survey.router, dependencies=_tenant_deps) api_router.include_router(tree_transfer.router, dependencies=_tenant_deps) api_router.include_router(ai_suggestions.router, dependencies=_tenant_deps) api_router.include_router(kb_accelerator.router, dependencies=_tenant_deps) diff --git a/backend/app/models/ai_session.py b/backend/app/models/ai_session.py index 8d69c916..04c29a95 100644 --- a/backend/app/models/ai_session.py +++ b/backend/app/models/ai_session.py @@ -10,7 +10,7 @@ from typing import Optional, Any, TYPE_CHECKING from sqlalchemy import String, Text, DateTime, ForeignKey, Boolean, Integer, Float, CheckConstraint import sqlalchemy as sa from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.dialects.postgresql import UUID, JSONB +from sqlalchemy.dialects.postgresql import UUID, JSONB, TSVECTOR from app.core.database import Base @@ -46,6 +46,7 @@ class AISession(Base): "confidence_tier IN ('guided', 'exploring', 'discovery')", name="ck_ai_sessions_confidence_tier", ), + sa.Index("idx_ai_sessions_search", "search_vector", postgresql_using="gin"), ) id: Mapped[uuid.UUID] = mapped_column( @@ -150,6 +151,18 @@ class AISession(Base): Text, nullable=True, comment="Why escalated (set on escalation)", ) + search_vector: Mapped[Optional[str]] = mapped_column( + TSVECTOR, + sa.Computed( + "to_tsvector('english', " + "coalesce(problem_summary, '') || ' ' || " + "coalesce(resolution_summary, '') || ' ' || " + "coalesce(escalation_reason, '') || ' ' || " + "coalesce(problem_domain, ''))", + persisted=True, + ), + nullable=True, + ) escalation_package: Mapped[Optional[dict[str, Any]]] = mapped_column( JSONB, nullable=True, comment="Context package for receiving engineer: steps_tried, hypotheses, suggestions", diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index ea554b0a..0c3162fc 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -68,4 +68,6 @@ class RoleUpdate(BaseModel): class AccountRoleUpdate(BaseModel): - account_role: str = Field(..., pattern="^(owner|admin|engineer|viewer)$") + # Ownership changes must go through the explicit transfer-ownership flow so + # account.owner_id stays consistent with user.account_role. + account_role: str = Field(..., pattern="^(admin|engineer|viewer)$") diff --git a/backend/app/services/assistant_chat_service.py b/backend/app/services/assistant_chat_service.py index c961b2d1..be0c458c 100644 --- a/backend/app/services/assistant_chat_service.py +++ b/backend/app/services/assistant_chat_service.py @@ -300,13 +300,14 @@ To create a fork, append this marker AFTER your [QUESTIONS]/[ACTIONS] markers: When you identify a second distinct issue that is clearly separate from the primary topic \ of this session, suggest creating a spin-off ticket using the [ACTIONS] marker below. \ Use this sparingly — only when the issue is genuinely independent, not for every tangential mention. +Use `create_spin_off_ticket` as the command value for this action. Format: [ACTIONS] [ { "label": "Create ticket: ", - "command": "create_spin_off_ticket", + "command": "", "description": "" } ] diff --git a/backend/app/services/resolution_output_generator.py b/backend/app/services/resolution_output_generator.py index 022f658e..7340d3e3 100644 --- a/backend/app/services/resolution_output_generator.py +++ b/backend/app/services/resolution_output_generator.py @@ -5,6 +5,7 @@ from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload from app.models.ai_session import AISession from app.models.session_resolution_output import SessionResolutionOutput @@ -21,7 +22,9 @@ class ResolutionOutputGenerator: async def generate_all(self, session_id: UUID) -> list[SessionResolutionOutput]: result = await self.db.execute( - select(AISession).where(AISession.id == session_id) + select(AISession) + .options(selectinload(AISession.steps)) + .where(AISession.id == session_id) ) session = result.scalar_one_or_none() if not session: diff --git a/backend/app/services/script_builder_service.py b/backend/app/services/script_builder_service.py index 2422c437..866f2685 100644 --- a/backend/app/services/script_builder_service.py +++ b/backend/app/services/script_builder_service.py @@ -360,6 +360,7 @@ async def save_to_library( category_id: UUID | None, share_with_team: bool, user_id: UUID, + account_id: UUID, team_id: UUID | None, script_body: str | None = None, parameters_schema: dict | None = None, @@ -401,6 +402,7 @@ async def save_to_library( id=uuid_mod.uuid4(), category_id=resolved_category_id, created_by=user_id, + account_id=account_id, team_id=team_id if share_with_team else None, name=name, slug=slug, diff --git a/backend/pytest.ini b/backend/pytest.ini index e8d63210..0f527590 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -35,6 +35,9 @@ testpaths = tests # Warnings filterwarnings = error + ignore:unclosed AsyncGenerator[AsyncSession, None]: """ @@ -137,6 +152,7 @@ async def test_db() -> AsyncGenerator[AsyncSession, None]: # Dispose engine first so all pooled connections are released, # then reconnect to perform the schema teardown cleanly. await engine.dispose() + await asyncio.sleep(0.01) # Drop all tables after test (CASCADE for circular FKs) teardown_engine = create_async_engine( @@ -150,6 +166,7 @@ async def test_db() -> AsyncGenerator[AsyncSession, None]: await conn.execute(sa.text("CREATE SCHEMA public")) finally: await teardown_engine.dispose() + await asyncio.sleep(0.01) @pytest.fixture diff --git a/backend/tests/test_ai_endpoints.py b/backend/tests/test_ai_endpoints.py index 1f91514e..697dbf18 100644 --- a/backend/tests/test_ai_endpoints.py +++ b/backend/tests/test_ai_endpoints.py @@ -74,19 +74,25 @@ def _mock_ai_provider(text: str, input_tokens: int = 100, output_tokens: int = 2 @pytest.fixture def enable_ai(): """Temporarily enable AI by setting a fake API key.""" - original = settings.ANTHROPIC_API_KEY + original_anthropic = settings.ANTHROPIC_API_KEY + original_google = settings.GOOGLE_AI_API_KEY settings.ANTHROPIC_API_KEY = "test-key-fake" + settings.GOOGLE_AI_API_KEY = None yield - settings.ANTHROPIC_API_KEY = original + settings.ANTHROPIC_API_KEY = original_anthropic + settings.GOOGLE_AI_API_KEY = original_google @pytest.fixture def disable_ai(): """Ensure AI is disabled.""" - original = settings.ANTHROPIC_API_KEY + original_anthropic = settings.ANTHROPIC_API_KEY + original_google = settings.GOOGLE_AI_API_KEY settings.ANTHROPIC_API_KEY = None + settings.GOOGLE_AI_API_KEY = None yield - settings.ANTHROPIC_API_KEY = original + settings.ANTHROPIC_API_KEY = original_anthropic + settings.GOOGLE_AI_API_KEY = original_google # ── Quota endpoint ── diff --git a/backend/tests/test_branch_manager.py b/backend/tests/test_branch_manager.py index 923ee6b1..c7ed0549 100644 --- a/backend/tests/test_branch_manager.py +++ b/backend/tests/test_branch_manager.py @@ -66,6 +66,7 @@ async def test_create_fork(client: AsyncClient, test_user, auth_headers, test_db step = AISessionStep( session_id=session.id, + account_id=session.account_id, step_order=0, step_type="question", content={"text": "What's the issue?"}, @@ -119,7 +120,7 @@ async def test_switch_branch(client: AsyncClient, test_user, auth_headers, test_ root = await manager.create_root_branch(session.id) step = AISessionStep( - session_id=session.id, step_order=0, step_type="question", + session_id=session.id, account_id=session.account_id, step_order=0, step_type="question", content={"text": "test"}, confidence_at_step=0.5, ) test_db.add(step) @@ -197,7 +198,7 @@ async def test_get_branch_tree(client: AsyncClient, test_user, auth_headers, tes root = await manager.create_root_branch(session.id) step = AISessionStep( - session_id=session.id, step_order=0, step_type="question", + session_id=session.id, account_id=session.account_id, step_order=0, step_type="question", content={"text": "test"}, confidence_at_step=0.5, ) test_db.add(step) diff --git a/backend/tests/test_psa_writeback_phase4.py b/backend/tests/test_psa_writeback_phase4.py index 131f1bec..eef47aa4 100644 --- a/backend/tests/test_psa_writeback_phase4.py +++ b/backend/tests/test_psa_writeback_phase4.py @@ -50,6 +50,7 @@ async def _make_session(test_db, user, *, with_psa: bool = False) -> AISession: conn = PsaConnection( account_id=user["user_data"]["account_id"], provider="connectwise", + display_name="Test ConnectWise", site_url="https://fake.cw.local", company_id="TEST", credentials_encrypted=encrypt_credentials({"public_key": "x", "private_key": "y"}), diff --git a/backend/tests/test_script_builder.py b/backend/tests/test_script_builder.py index 4cbe183a..c9a7d88f 100644 --- a/backend/tests/test_script_builder.py +++ b/backend/tests/test_script_builder.py @@ -472,19 +472,20 @@ class TestScriptBuilderSlugCollision: # Pre-create a template with slug "test-script" to cause collision user_resp = await client.get("/api/v1/auth/me", headers=auth_headers) user_id = user_resp.json()["id"] + account_id = user_resp.json()["account_id"] await test_db.execute( sa.text(""" INSERT INTO script_templates - (id, category_id, created_by, name, slug, script_body, + (id, category_id, created_by, account_id, name, slug, script_body, parameters_schema, default_values, validation_rules, tags, complexity, is_active, version, usage_count, created_at, updated_at) VALUES - (:id, 'a0000000-0000-0000-0000-000000000001'::uuid, :uid, + (:id, 'a0000000-0000-0000-0000-000000000001'::uuid, :uid, :account_id, 'Test Script', 'test-script', 'echo hello', '{"parameters": []}', '{}', '{}', '["powershell"]', 'beginner', true, 1, 0, NOW(), NOW()) """), - {"id": str(uuid_mod.uuid4()), "uid": user_id}, + {"id": str(uuid_mod.uuid4()), "uid": user_id, "account_id": account_id}, ) await test_db.commit() @@ -561,6 +562,7 @@ class TestScriptTemplateFilters: """mine=true returns only templates created by the current user.""" user_resp = await client.get("/api/v1/auth/me", headers=auth_headers) user_id = user_resp.json()["id"] + account_id = user_resp.json()["account_id"] second_resp = await client.get("/api/v1/auth/me", headers=second_user_headers) second_user_id = second_resp.json()["id"] @@ -571,32 +573,32 @@ class TestScriptTemplateFilters: await test_db.execute( sa.text(""" INSERT INTO script_templates - (id, category_id, created_by, team_id, name, slug, script_body, + (id, category_id, created_by, account_id, team_id, name, slug, script_body, parameters_schema, default_values, validation_rules, tags, complexity, is_active, version, usage_count, created_at, updated_at) VALUES - (:id, :cat, :uid, NULL, + (:id, :cat, :uid, :account_id, NULL, 'My Script', 'my-script', 'echo mine', '{"parameters": []}', '{}', '{}', '[]', 'beginner', true, 1, 0, NOW(), NOW()) """), - {"id": str(uuid_mod.uuid4()), "cat": cat_id, "uid": user_id}, + {"id": str(uuid_mod.uuid4()), "cat": cat_id, "uid": user_id, "account_id": account_id}, ) # Create template owned by second user (no team_id, so visible to all) await test_db.execute( sa.text(""" INSERT INTO script_templates - (id, category_id, created_by, team_id, name, slug, script_body, + (id, category_id, created_by, account_id, team_id, name, slug, script_body, parameters_schema, default_values, validation_rules, tags, complexity, is_active, version, usage_count, created_at, updated_at) VALUES - (:id, :cat, :uid, NULL, + (:id, :cat, :uid, :account_id, NULL, 'Other Script', 'other-script', 'echo other', '{"parameters": []}', '{}', '{}', '[]', 'beginner', true, 1, 0, NOW(), NOW()) """), - {"id": str(uuid_mod.uuid4()), "cat": cat_id, "uid": second_user_id}, + {"id": str(uuid_mod.uuid4()), "cat": cat_id, "uid": second_user_id, "account_id": account_id}, ) await test_db.commit() @@ -617,6 +619,7 @@ class TestScriptTemplateFilters: """shared=true returns only templates shared with the user's team.""" user_resp = await client.get("/api/v1/auth/me", headers=auth_headers) user_id = user_resp.json()["id"] + account_id = user_resp.json()["account_id"] cat_id = "b0000000-0000-0000-0000-000000000001" @@ -639,32 +642,32 @@ class TestScriptTemplateFilters: await test_db.execute( sa.text(""" INSERT INTO script_templates - (id, category_id, created_by, team_id, name, slug, script_body, + (id, category_id, created_by, account_id, team_id, name, slug, script_body, parameters_schema, default_values, validation_rules, tags, complexity, is_active, version, usage_count, created_at, updated_at) VALUES - (:id, :cat, :uid, :tid, + (:id, :cat, :uid, :account_id, :tid, 'Team Script', 'team-script', 'echo team', '{"parameters": []}', '{}', '{}', '[]', 'beginner', true, 1, 0, NOW(), NOW()) """), - {"id": str(uuid_mod.uuid4()), "cat": cat_id, "uid": user_id, "tid": team_id}, + {"id": str(uuid_mod.uuid4()), "cat": cat_id, "uid": user_id, "account_id": account_id, "tid": team_id}, ) # Template NOT shared (no team_id) await test_db.execute( sa.text(""" INSERT INTO script_templates - (id, category_id, created_by, team_id, name, slug, script_body, + (id, category_id, created_by, account_id, team_id, name, slug, script_body, parameters_schema, default_values, validation_rules, tags, complexity, is_active, version, usage_count, created_at, updated_at) VALUES - (:id, :cat, :uid, NULL, + (:id, :cat, :uid, :account_id, NULL, 'Personal Script', 'personal-script', 'echo personal', '{"parameters": []}', '{}', '{}', '[]', 'beginner', true, 1, 0, NOW(), NOW()) """), - {"id": str(uuid_mod.uuid4()), "cat": cat_id, "uid": user_id}, + {"id": str(uuid_mod.uuid4()), "cat": cat_id, "uid": user_id, "account_id": account_id}, ) await test_db.commit() diff --git a/backend/tests/test_session_branches_api.py b/backend/tests/test_session_branches_api.py index 104a1968..2d35af62 100644 --- a/backend/tests/test_session_branches_api.py +++ b/backend/tests/test_session_branches_api.py @@ -49,7 +49,7 @@ async def test_create_fork(client: AsyncClient, test_user, auth_headers, test_db await test_db.flush() step = AISessionStep( - session_id=session.id, step_order=0, step_type="question", + session_id=session.id, account_id=session.account_id, step_order=0, step_type="question", content={"text": "test"}, confidence_at_step=0.5, ) test_db.add(step) @@ -88,7 +88,7 @@ async def test_switch_branch(client: AsyncClient, test_user, auth_headers, test_ await test_db.flush() step = AISessionStep( - session_id=session.id, step_order=0, step_type="question", + session_id=session.id, account_id=session.account_id, step_order=0, step_type="question", content={"text": "test"}, confidence_at_step=0.5, ) test_db.add(step) diff --git a/backend/tests/test_session_resolutions_api.py b/backend/tests/test_session_resolutions_api.py index deac8c70..a8394fd3 100644 --- a/backend/tests/test_session_resolutions_api.py +++ b/backend/tests/test_session_resolutions_api.py @@ -45,6 +45,7 @@ async def test_edit_output_api(client: AsyncClient, test_user, auth_headers, tes output = SessionResolutionOutput( session_id=session.id, + account_id=session.account_id, output_type="psa_ticket_notes", generated_content="Original", status="draft", diff --git a/backend/tests/test_session_sharing.py b/backend/tests/test_session_sharing.py index 1f097b19..f85a8096 100644 --- a/backend/tests/test_session_sharing.py +++ b/backend/tests/test_session_sharing.py @@ -219,7 +219,7 @@ class TestSessionSharing: json={"visibility": "public"}, headers=other_headers ) - assert response.status_code == 403 + assert response.status_code == 404 async def test_share_nonexistent_session(self, client: AsyncClient, auth_headers): """Creating a share for nonexistent session returns 404.""" diff --git a/backend/tests/test_session_suggested_fixes_api.py b/backend/tests/test_session_suggested_fixes_api.py index a6a52486..427e4d32 100644 --- a/backend/tests/test_session_suggested_fixes_api.py +++ b/backend/tests/test_session_suggested_fixes_api.py @@ -213,6 +213,7 @@ async def test_record_decision_persists_and_bumps_state_version( title="x", description="y", confidence_pct=50, + ai_drafted_script="Write-Output 'ok'", ) test_db.add(fix) await test_db.commit() diff --git a/backend/tests/test_tenant_isolation_p0.py b/backend/tests/test_tenant_isolation_p0.py index c7519f59..4ef9729f 100644 --- a/backend/tests/test_tenant_isolation_p0.py +++ b/backend/tests/test_tenant_isolation_p0.py @@ -43,7 +43,7 @@ async def _create_account_and_user(db: AsyncSession, prefix: str): async def _login(client: AsyncClient, email: str, password: str) -> dict: """Log in and return Authorization headers.""" resp = await client.post( - "/api/v1/auth/login", + "/api/v1/auth/login/json", json={"email": email, "password": password}, ) assert resp.status_code == 200, f"Login failed: {resp.text}" @@ -101,11 +101,11 @@ async def test_category_tree_count_scoped_to_account( acct_a, user_a, pass_a = await _create_account_and_user(test_db, "cat-a") acct_b, user_b, pass_b = await _create_account_and_user(test_db, "cat-b") - # Shared category (account_id=None means global) + # Categories are tenant-scoped; the endpoint must only count account A's trees. category = TreeCategory( name="Shared Category", slug=f"shared-cat-{uuid.uuid4().hex[:6]}", - account_id=None, + account_id=acct_a.id, is_active=True, ) test_db.add(category) @@ -270,6 +270,7 @@ async def test_get_session_returns_404_not_403_for_other_user( session_b = Session( tree_id=tree_b.id, user_id=user_b.id, + account_id=acct_b.id, tree_snapshot={"id": "root", "type": "start", "children": []}, path_taken=[], decisions=[], @@ -384,6 +385,7 @@ async def test_share_revoke_returns_404_not_403_for_other_user( session_b = Session( tree_id=tree_b.id, user_id=user_b.id, + account_id=acct_b.id, tree_snapshot={"id": "root", "type": "start", "children": []}, path_taken=[], decisions=[], @@ -534,6 +536,7 @@ async def test_maintenance_schedule_returns_404_for_other_team( # Create a schedule for that tree schedule_b = MaintenanceSchedule( tree_id=tree_b.id, + account_id=acct_b.id, created_by=user_b.id, cron_expression="0 2 * * 0", timezone="UTC", diff --git a/backend/tests/test_tree_sharing.py b/backend/tests/test_tree_sharing.py index a9adee54..c7350ece 100644 --- a/backend/tests/test_tree_sharing.py +++ b/backend/tests/test_tree_sharing.py @@ -4,6 +4,7 @@ from datetime import datetime, timezone, timedelta from httpx import AsyncClient from uuid import uuid4 +from app.models.account import Account from app.models.tree import Tree from app.models.tree_share import TreeShare from app.models.user import User @@ -287,13 +288,17 @@ class TestTreeSharing: @pytest.mark.asyncio async def test_migration_defaults_visibility_to_team(test_db): """Test that existing trees default to 'team' visibility after migration.""" + account = Account(name="Migration Default Test", display_code=uuid4().hex[:8]) + test_db.add(account) + await test_db.flush() + # Create a tree without specifying visibility tree = Tree( name="Old Tree", description="Created before migration", tree_structure={"id": "root", "type": "decision", "question": "Test?", "children": []}, author_id=None, - account_id=None + account_id=account.id ) test_db.add(tree) await test_db.commit() diff --git a/backend/tests/test_uploads.py b/backend/tests/test_uploads.py index fa4f54cb..27ca0cca 100644 --- a/backend/tests/test_uploads.py +++ b/backend/tests/test_uploads.py @@ -359,7 +359,7 @@ async def test_delete_upload_forbidden_for_non_owner(client, auth_headers, test_ f"/api/v1/uploads/{upload.id}", headers=other_headers ) - assert response.status_code == 403 + assert response.status_code == 404 # --------------------------------------------------------------------------- -- 2.49.1 From 0aefaa78eb371e4bc10258cfd43cc9399c90dfbf Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 11:35:38 -0400 Subject: [PATCH 04/15] docs(ai): queue pytest-xdist parallelization in TODO.md Capture the backend pytest parallelization work so it survives session end. Backend suite is currently ~22 min wall-clock for 1076 tests; xdist with one-DB-per-worker should land in the 3-6 min range on the homelab Gitea Actions runner. Also queues two backlog items: - Frontend lint warnings (23 react-hooks/exhaustive-deps after PR #149) - Periodic audit of the ResourceWarning filterwarnings added by Codex Co-Authored-By: Claude Opus 4.7 --- .ai/TODO.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.ai/TODO.md b/.ai/TODO.md index 38730ced..bf7513e8 100644 --- a/.ai/TODO.md +++ b/.ai/TODO.md @@ -5,8 +5,9 @@ ## Up next -- [ ] No queued backlog yet. +- [ ] **Parallelize backend pytest with pytest-xdist.** Currently the backend suite takes ~22 min wall-clock for `1076 passed, 35 deselected` (verified locally 2026-04-25). With `-n auto` on the homelab Gitea Actions runner, this should land in the 3–6 min range depending on core count. Blocker: `test_db` fixture in `backend/tests/conftest.py` does `DROP SCHEMA public CASCADE` per test, which two workers would race on. Standard fix: one database per worker, derived from `PYTEST_XDIST_WORKER` env var inside conftest. The runner has spare CPU, so prioritize once main is green and the 54-failure cleanup has landed. ## Backlog -- [ ] No queued backlog yet. +- [ ] **Frontend lint warnings cleanup.** 23 `react-hooks/exhaustive-deps` warnings remain after PR #149 (mostly missing-deps in useEffect). Either fix them or audit them for known-safe ones and add eslint-disable comments. Not blocking CI today. +- [ ] **Audit `filterwarnings` ignores added in `wip(handoff): restore backend suite to green`.** Codex added narrow `ResourceWarning` filters for unclosed socket/transport/event-loop noise from pytest-asyncio teardown. Worth periodically reviewing whether those are still needed (e.g. when bumping pytest-asyncio) — if a real warning appears in those forms it would be silenced. -- 2.49.1 From e976fb4e87e5cbe9ae46afee5e018d3ccd2dafa1 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 12:01:05 -0400 Subject: [PATCH 05/15] fix(ci): mock AI provider in record_decision test + cache pip/npm + drop term-missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes that get PR #150 to a green CI gate: 1. **test_record_decision_persists_and_bumps_state_version** — the `decision: draft_template` path calls `_extract_template_parameters` (TemplateExtractionService → AI provider). CI doesn't set ANTHROPIC_API_KEY/GOOGLE_AI_API_KEY, so the endpoint raised `RuntimeError: No AI provider configured` and returned 500. The test isn't exercising the AI integration — patched the extractor with an AsyncMock returning a minimal valid `{templated_body, parameters}` dict. Verified locally: the test now passes. 2. **pip + npm caches** in backend, frontend, and e2e jobs. Keyed on the hash of requirements*.txt / package-lock.json with a runner-os restore-key fallback. Saves ~30-60s per run on cache hit. 3. **Pytest invocation tightened**: - Dropped `--cov-report=term-missing` — the custom "Display coverage summary" step below parses coverage.json and prints the same module list more concisely. Term-missing dumps every uncovered line which adds ~5-10s of stdout. - Added `--maxfail=10` so a structural breakage (fixture explosion, DB unreachable) bails after 10 errors instead of running the full 25-min suite. Tunable. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/ci.yml | 38 ++++++++++++++++++- .../tests/test_session_suggested_fixes_api.py | 22 ++++++++--- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 0ea10f73..a801f984 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -43,6 +43,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: pip-${{ runner.os }}-${{ hashFiles('backend/requirements.txt', 'backend/requirements-dev.txt') }} + restore-keys: | + pip-${{ runner.os }}- + - name: Install system dependencies run: | apt-get update @@ -58,7 +66,11 @@ jobs: run: cd backend && python scripts/check_tenant_filters.py - name: Run tests with coverage - run: cd backend && python -m pytest --override-ini="addopts=" --cov=app --cov-report=term-missing --cov-report=json:coverage.json --cov-fail-under=50 + # term-missing dropped — the custom "Display coverage summary" step + # below parses coverage.json and prints the same info more concisely. + # --maxfail=10 short-circuits on structural breakage so we don't burn + # 25 minutes when a fixture explodes. + run: cd backend && python -m pytest --override-ini="addopts=" --maxfail=10 --cov=app --cov-report=json:coverage.json --cov-fail-under=50 - name: Display coverage summary if: always() @@ -86,6 +98,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Cache npm + uses: actions/cache@v3 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }} + restore-keys: | + npm-${{ runner.os }}- + - name: Install dependencies run: cd frontend && npm ci @@ -136,6 +156,22 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: pip-${{ runner.os }}-${{ hashFiles('backend/requirements.txt', 'backend/requirements-dev.txt') }} + restore-keys: | + pip-${{ runner.os }}- + + - name: Cache npm + uses: actions/cache@v3 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }} + restore-keys: | + npm-${{ runner.os }}- + - name: Install backend dependencies run: pip install --break-system-packages -r backend/requirements.txt -r backend/requirements-dev.txt diff --git a/backend/tests/test_session_suggested_fixes_api.py b/backend/tests/test_session_suggested_fixes_api.py index 427e4d32..dfb78243 100644 --- a/backend/tests/test_session_suggested_fixes_api.py +++ b/backend/tests/test_session_suggested_fixes_api.py @@ -218,11 +218,23 @@ async def test_record_decision_persists_and_bumps_state_version( test_db.add(fix) await test_db.commit() - r = await client.post( - f"/api/v1/ai-sessions/{session.id}/suggested-fixes/{fix.id}/decision", - headers=auth_headers, - json={"decision": "draft_template"}, - ) + # The draft_template path calls TemplateExtractionService, which needs an + # AI provider configured. CI doesn't set ANTHROPIC_API_KEY/GOOGLE_AI_API_KEY, + # and this test isn't exercising the AI integration — patch the extractor + # with a minimal valid response so the rest of the decision flow runs. + extractor_stub = AsyncMock(return_value={ + "templated_body": "Write-Output 'ok'", + "parameters": [], + }) + with patch( + "app.api.endpoints.session_suggested_fixes._extract_template_parameters", + extractor_stub, + ): + r = await client.post( + f"/api/v1/ai-sessions/{session.id}/suggested-fixes/{fix.id}/decision", + headers=auth_headers, + json={"decision": "draft_template"}, + ) assert r.status_code == 200 assert r.json()["user_decision"] == "draft_template" -- 2.49.1 From fe632c919435b31a5a0b18f1218bd7d244de6d5b Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 12:15:07 -0400 Subject: [PATCH 06/15] docs(ai): handoff after CI parallelization + final test fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates HANDOFF.md, CURRENT_TASK.md, and SESSION_LOG.md to reflect: - PR #150 now contains the AI-provider test mock + caching + maxfail. Backend CI should be fully green for the first time in months. - PR #151 stacked on #150: pytest-xdist with per-worker DBs. Local verification: 22m 27s → 4m 28s (5× speedup), 1076 passed both runs. - DoD is now: merge #150, then #151, then add CI / backend (pull_request) to required status checks on main. Co-Authored-By: Claude Opus 4.7 --- .ai/CURRENT_TASK.md | 20 +++++----- .ai/HANDOFF.md | 97 +++++++++++++++++++++++++-------------------- 2 files changed, 65 insertions(+), 52 deletions(-) diff --git a/.ai/CURRENT_TASK.md b/.ai/CURRENT_TASK.md index 383fff86..0109ea2d 100644 --- a/.ai/CURRENT_TASK.md +++ b/.ai/CURRENT_TASK.md @@ -1,22 +1,22 @@ # CURRENT_TASK.md -**Task:** Restore a fully green CI gate on `main` and lock it via branch protection so future merges can't introduce silent rot. +**Task:** Land two stacked CI PRs and lock the backend gate 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). - [ ] `CI / backend (pull_request)` added to required status checks on `main` in Gitea branch protection (frontend is already required). -- [ ] The 54 real backend test failures (left after #149's infra cleanup) categorized and fixed in a follow-up PR. Target: 0 failures, 0 errors on a `pytest` run inside `resolutionflow_backend`. -- [ ] `npm run lint` stays at 0 errors after the cleanup PR (already at 0 on main). -- [ ] Append a SESSION_LOG.md entry summarizing what shipped. +- [ ] Optional: `CI / e2e (pull_request)` confirmed clean and added to required checks. **Assumptions:** -- The 54 failures fall into a small number of root-cause categories (likely 3–5: fixture-scoping leaks, DB cleanup ordering, account_id propagation in test seed paths). Verify before assuming. -- The pytest-asyncio 0.24 + pytest 8.4 toolchain bumped in #149 is the right baseline; do not revert. -- `DATABASE_TEST_URL` is the only DB URL conftest will honor; do not weaken the safety guard added in `dab740d`. +- 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`. +- pytest-cov's xdist support continues to handle the coverage merge and `--cov-fail-under=50` check correctly. +- The per-worker DB creation in `conftest.py` is idempotent and racing workers on first import won't all try to CREATE DATABASE simultaneously — postgres serializes that, but if it surfaces issues, wrap with an advisory lock. **Out of scope:** -- New feature work on FlowPilot (Phase 10+) or PSA — keep this branch focused on CI debt. -- Frontend lint warnings (23 remain after #149; they're missing-deps in useEffect, opt-in cleanup later). -- RLS test suite (`test_rls_isolation.py`) — gated behind `RUN_RLS_TESTS=1` and not in the default CI run. +- Frontend lint warnings (23 remain after #149). +- The 23 react-hooks/exhaustive-deps warnings. +- RLS test suite (gated behind `RUN_RLS_TESTS=1`; not in default CI). +- Per-test transactional rollback (would shave another 30-40% off backend time but is a much bigger refactor — capture in TODO if interested). diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 0b896b7e..771c40b2 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -2,62 +2,75 @@ # HANDOFF.md -**Last updated:** 2026-04-25 06:12 EDT +**Last updated:** 2026-04-25 (America/New_York) -**Active task:** Restore green CI gate on `main` and lock it via branch protection. See [CURRENT_TASK.md](CURRENT_TASK.md). +**Active task:** Land two stacked CI PRs (#150 + #151), then enable backend gate on `main`. See [CURRENT_TASK.md](CURRENT_TASK.md). -**Branch:** `fix/ci-workflow-config` +**Branch:** Currently on `fix/ci-workflow-config` (PR #150). The xdist work lives on `fix/ci-pytest-xdist` (PR #151), branched from #150. -## Current state +## Two open PRs to land in order -Previous session fixed the 54 real backend failures left after #149. The default backend suite is now green locally: +### PR #150 — `fix/ci-workflow-config` → main -```bash -docker exec resolutionflow_backend bash -lc 'pytest --override-ini="addopts=" -q > /tmp/full-backend.log 2>&1; code=$?; tail -n 160 /tmp/full-backend.log; exit $code' -# 1076 passed, 35 deselected in 1347.41s (0:22:27) -``` +Carries: +- The Codex commit (`49f8856 wip(handoff): restore backend suite to green`) — fixes 54 backend test failures. +- Workflow fixes: `DATABASE_TEST_URL` env, `actions/upload-artifact` v3 pin. +- Most-recent commit (`e976fb4`): + - Mocks `_extract_template_parameters` in `test_record_decision_persists_and_bumps_state_version` (last test failing on CI; needed an AI provider key the runner doesn't have). Verified locally — passes. + - pip + npm caches in all three jobs. + - Drops `--cov-report=term-missing` (the custom "Display coverage summary" step prints the same info from JSON). + - Adds `--maxfail=10` so structural breakage fails fast. -Targeted validation also passed: +**Expected CI on this PR:** all three jobs green for the first time in months. -- `tests/test_session_resolutions_api.py tests/test_session_sharing.py tests/test_session_suggested_fixes_api.py tests/test_survey.py tests/test_tenant_isolation_p0.py tests/test_tree_sharing.py tests/test_trees.py::TestTrees::test_delete_tree_cleans_up_folder_and_tag_assignments tests/test_uploads.py::test_delete_upload_forbidden_for_non_owner` → `73 passed` -- PDF export tests → `3 passed` -- Prompt/PSA/resolution/script-builder subset → `14 passed` -- Admin/AI/branch subsets → `11 passed` +### PR #151 — `fix/ci-pytest-xdist` → main (stacked on #150) -## What changed +Carries (on top of #150): +- `pytest-xdist==3.6.1` in `requirements-dev.txt`. +- `conftest.py` adds `_worker_db_url` + `_ensure_worker_db_exists`. Each xdist worker gets its own DB (`resolutionflow_test_gw0`, `gw1`, …) so the per-test `DROP SCHEMA public CASCADE` doesn't race across workers. +- Workflow's pytest invocation gains `-n auto`. -Production fixes: - -- CI/backend dev image now installs WeasyPrint system libraries. -- Public share-token and survey routes are mounted outside tenant auth; protected share management remains tenant-protected. -- Folder creation now persists `UserFolder.account_id`. -- Script Builder save-to-library now persists `ScriptTemplate.account_id`. -- Resolution output generation eager-loads `AISession.steps` to avoid async lazy-load `MissingGreenlet`. -- AI session model now declares the generated `search_vector` column already present in Alembic, so `create_all` test schemas match runtime migrations. -- Direct account-role update now rejects `"owner"`; ownership changes must use the transfer path. -- Assistant prompt marker examples no longer include a literal executable `create_spin_off_ticket` payload. - -Test/harness fixes: - -- Test seeds updated for tenant-scoped `account_id` columns on sessions, branches, resolution outputs, script templates, PSA connections, folders, schedules, and categories. -- Tests aligned with 404-not-403 resource-hiding policy. -- Disabled-AI tests now restore both Anthropic and Google key settings. -- Pytest harness closes pytest-asyncio's leftover clean loop and ignores known unclosed asyncio/asyncpg teardown ResourceWarnings that otherwise appear at arbitrary later setup points under `filterwarnings = error`. +**Measured locally:** backend suite goes from `22m 27s` (serial, 1076 passed) → `4m 28s` (8 workers, 1076 passed). Same exit code, same test count. ## Immediate next steps -1. Commit current working tree if not already committed with trailer: - `Co-Authored-By: Codex `. -2. Check PR #150 status on Gitea. If both `CI / backend (pull_request)` and `CI / frontend (pull_request)` are green, merge it. -3. After #150 merges, add `CI / backend (pull_request)` to required status checks on main: +1. **Watch PR #150 CI** on its latest sha (`e976fb4`). Both `CI / backend (pull_request)` and `CI / frontend (pull_request)` should be green. Merge if so. +2. **Watch PR #151 CI** after #150 merges. Once #151 is rebased / merged automatically, backend job time on subsequent runs should drop to the 4–6 min range. +3. **Enable backend gate** on `main` branch protection — append `"CI / backend (pull_request)"` to `status_check_contexts`: ```bash - PATCH /repos/chihlasm/resolutionflow/branch_protections/main - { "status_check_contexts": ["CI / frontend (pull_request)", "CI / backend (pull_request)"] } + curl -X PATCH -H "Authorization: token $GITEA_TOKEN" \ + "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)"]}' ``` - `$GITEA_TOKEN` is in `.claude/settings.local.json`. -4. Run/confirm frontend lint if needed for the final DoD item (`npm run lint` was already green after #149, but this session did not rerun it). +4. **Optional: also gate `CI / e2e (pull_request)`** once that job has run cleanly a few times. The artifact-v3 fix means it can finally run; we haven't verified its actual outcome yet. + +## Uncommitted state + +Working tree clean (after this handoff commit). + +## Branch protection on main (current) + +- PR-only merges +- `CI / frontend (pull_request)` required +- Force-push blocked +- 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 -- PR #150 was not rechecked or merged in this session. -- Branch protection was not updated in this session. +- 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 + +- `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 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 it ever bothers anyone. -- 2.49.1 From f27e3b44b07b34e3f8bf102ee395ed659e092172 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 12:15:41 -0400 Subject: [PATCH 07/15] docs(ai): SESSION_LOG entry for the parallelization session (Was meant to land in fe632c9; the multi-line edit failed silently because Codex's earlier entry shifted the surrounding context.) Co-Authored-By: Claude Opus 4.7 --- .ai/SESSION_LOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.ai/SESSION_LOG.md b/.ai/SESSION_LOG.md index 0da1f81d..1df15197 100644 --- a/.ai/SESSION_LOG.md +++ b/.ai/SESSION_LOG.md @@ -12,6 +12,16 @@ --- +## 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. +- Pushed Codex's WIP commit `49f8856` to PR #150 (had been local-only per handoff protocol). +- PR #150 (`fix/ci-workflow-config`) extended with cheap CI wins: `actions/cache@v3` for pip + npm in all three jobs; dropped `--cov-report=term-missing` (the custom display step parses JSON); added `--maxfail=10` so structural breakage exits fast. +- PR #151 (`fix/ci-pytest-xdist`) opened, stacked on #150: pytest-xdist with per-worker DB isolation. `conftest.py` reads `PYTEST_XDIST_WORKER`, computes a per-worker DB URL like `…_gw0`, and synchronously CREATEs the DB on first import. The per-test `DROP SCHEMA public CASCADE` then operates on the worker's isolated DB. Verified locally: backend suite went from 22m 27s serial → 4m 28s parallel (8 workers), 1076 passed in both cases. ~5× speedup. +- Decided NOT to do per-test transactional rollback (bigger refactor); captured for future TODO consideration. +- Left for next session: watch CI on both PRs, merge in order (#150 first, #151 second), then enable `CI / backend (pull_request)` as a required status check on main. +- Files touched: `backend/tests/test_session_suggested_fixes_api.py`, `backend/tests/conftest.py`, `backend/requirements-dev.txt`, `.gitea/workflows/ci.yml`, `.ai/HANDOFF.md`, `.ai/CURRENT_TASK.md`, `.ai/TODO.md`. + ## 2026-04-25 06:12 EDT — Codex — Fix backend suite to green - Fixed the real backend failures left after the CI-infra cleanup: tenant-scoped seed drift, missing production `account_id` writes, public route mounting for survey/share links, Script Builder library saves, resolution output async loading, AI search schema metadata, disabled-AI fixture leakage, and prompt marker guardrails. -- 2.49.1 From c203b70ef95ce2097ff139f0ef8255cdc1fd7f6b Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 15:26:21 -0400 Subject: [PATCH 08/15] docs(ai): queue data-testid hardening + reflect PR #152 + 3-runner setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TODO.md: Promote pytest-xdist to ✅ (PR #151 carries it). Adds three new backlog items: - data-testid hardening for e2e-critical interactive elements (sparked by PR #152's selector drift work) - per-test transactional rollback (next big speedup if needed) - pytest-testmon for PR-time test selection HANDOFF.md: Three open PRs now (#150, #151, #152), all independent. Three Gitea runner agents now registered, so jobs run in parallel. Combined with #151's xdist, the prior 1h 14m wall-clock should drop to ~6-10 min. Updated merge order: #152 first (smallest), #150 next, #151 last. After all three land, enable CI / backend then CI / e2e as required status checks. Co-Authored-By: Claude Opus 4.7 --- .ai/HANDOFF.md | 32 +++++++++++++++++++++++++------- .ai/TODO.md | 5 ++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 771c40b2..26edc7a1 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -4,11 +4,16 @@ **Last updated:** 2026-04-25 (America/New_York) -**Active task:** Land two stacked CI PRs (#150 + #151), then enable backend gate on `main`. See [CURRENT_TASK.md](CURRENT_TASK.md). +**Active task:** Land three open CI PRs (#150 + #151 + #152), then enable backend + e2e gates on `main`. See [CURRENT_TASK.md](CURRENT_TASK.md). -**Branch:** Currently on `fix/ci-workflow-config` (PR #150). The xdist work lives on `fix/ci-pytest-xdist` (PR #151), branched from #150. +**Branches:** Three open PRs, all independent of each other for correctness: +- `fix/ci-workflow-config` → PR #150 +- `fix/ci-pytest-xdist` → PR #151 (stacked on #150 for context but mergeable on its own) +- `fix/e2e-test-selectors` → PR #152 -## Two open PRs to land in order +**Runner setup:** Three Gitea Actions agents are now registered on the homelab box, so `backend` / `frontend` / `e2e` jobs run truly in parallel instead of serializing on a single agent. Combined with PR #151's xdist parallelization, the previous 1h 14m wall-clock should drop to ~6–10 min. + +## Three open PRs ### PR #150 — `fix/ci-workflow-config` → main @@ -32,18 +37,31 @@ Carries (on top of #150): **Measured locally:** backend suite goes from `22m 27s` (serial, 1076 passed) → `4m 28s` (8 workers, 1076 passed). Same exit code, same test count. +### PR #152 — `fix/e2e-test-selectors` → main + +Carries: five Playwright e2e selector updates against the current UI. The drift was inherited from the FlowPilot/PSA migration: + +- `Sessions` → `Session History` (page heading) +- `Account Settings` → `Account Management` (page heading) +- `/assistant` → `/pilot` (Phase 1 route rename; redirect still works) +- Flow-session filtering and the Resume button moved behind the "Flow Sessions" tab on `/sessions` (default tab is "AI Sessions") +- `resume.spec.ts` no longer starts at `/trees` — Resume button rendering moved to the session card on `/sessions` + +No product-code changes. Pure test updates. + ## Immediate next steps -1. **Watch PR #150 CI** on its latest sha (`e976fb4`). Both `CI / backend (pull_request)` and `CI / frontend (pull_request)` should be green. Merge if so. -2. **Watch PR #151 CI** after #150 merges. Once #151 is rebased / merged automatically, backend job time on subsequent runs should drop to the 4–6 min range. -3. **Enable backend gate** on `main` branch protection — append `"CI / backend (pull_request)"` to `status_check_contexts`: +1. **Merge PR #152 first.** Smallest, lowest risk, no shared file with the other two PRs. +2. **Merge PR #150 next.** Backend test suite should be fully green (1076 passed, 0 failed, 0 errors). +3. **Merge PR #151 last.** Backend job time drops to ~4–6 min on the runner. +4. **Enable backend gate** on `main` branch protection — append `"CI / backend (pull_request)"` to `status_check_contexts`: ```bash curl -X PATCH -H "Authorization: token $GITEA_TOKEN" \ "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. **Optional: also gate `CI / e2e (pull_request)`** once that job has run cleanly a few times. The artifact-v3 fix means it can finally run; we haven't verified its actual outcome yet. +5. **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. ## Uncommitted state diff --git a/.ai/TODO.md b/.ai/TODO.md index bf7513e8..eb13b867 100644 --- a/.ai/TODO.md +++ b/.ai/TODO.md @@ -5,9 +5,12 @@ ## Up next -- [ ] **Parallelize backend pytest with pytest-xdist.** Currently the backend suite takes ~22 min wall-clock for `1076 passed, 35 deselected` (verified locally 2026-04-25). With `-n auto` on the homelab Gitea Actions runner, this should land in the 3–6 min range depending on core count. Blocker: `test_db` fixture in `backend/tests/conftest.py` does `DROP SCHEMA public CASCADE` per test, which two workers would race on. Standard fix: one database per worker, derived from `PYTEST_XDIST_WORKER` env var inside conftest. The runner has spare CPU, so prioritize once main is green and the 54-failure cleanup has landed. +- [ ] **Parallelize backend pytest with pytest-xdist.** ✅ landing as PR #151. Verified locally: backend suite 22 min → 4m 28s with `-n auto` on the 8-core homelab runner. Per-worker DB isolation via `PYTEST_XDIST_WORKER` in conftest.py. ## Backlog - [ ] **Frontend lint warnings cleanup.** 23 `react-hooks/exhaustive-deps` warnings remain after PR #149 (mostly missing-deps in useEffect). Either fix them or audit them for known-safe ones and add eslint-disable comments. Not blocking CI today. - [ ] **Audit `filterwarnings` ignores added in `wip(handoff): restore backend suite to green`.** Codex added narrow `ResourceWarning` filters for unclosed socket/transport/event-loop noise from pytest-asyncio teardown. Worth periodically reviewing whether those are still needed (e.g. when bumping pytest-asyncio) — if a real warning appears in those forms it would be silenced. +- [ ] **Add `data-testid` attributes to e2e-critical interactive elements.** PR #152 fixed five Playwright tests by chasing UI-text changes (`Sessions` → `Session History`, `Account Settings` → `Account Management`, `/assistant` → `/pilot`, "Flow Sessions" tab, Resume button on session cards). Each was a one-line selector update, but every UI churn re-breaks them. Adding stable `data-testid` attributes on the targeted elements (page heading wrappers, tab nav, primary action buttons) and switching tests to `getByTestId` would make these immune to copy/route renames. Scope it small — start with `SessionHistoryPage` heading, the AI/Flow Sessions tab buttons, the per-session `Resume` button, and the command-palette FlowPilot option. +- [ ] **Per-test transactional rollback in `test_db` fixture.** Bigger engineering than xdist (which we already shipped). Instead of `DROP SCHEMA public CASCADE` per test, wrap each test in a savepoint and rollback at teardown. ~30-40% additional speedup on top of xdist for test-DB-heavy tests. Real refactor; only worth it if the suite gets significantly larger or runs more frequently. +- [ ] **Consider `pytest-testmon` for PR-time test selection.** Tracks which tests touched which source files and only re-runs affected ones. Best for small PRs touching ~few files. Adds cache-invalidation complexity; only worth it if the suite stays painfully long even after xdist. -- 2.49.1 From 1bd43abb8feddf0a9eaf1e808f3a3d2da91ddd9f Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 15:28:17 -0400 Subject: [PATCH 09/15] fix(ci): drop postgres host port mapping (multi-runner port collision) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With 3 Gitea Actions runners on the same homelab box, two simultaneous backend (or backend + e2e) jobs both try to bind 0.0.0.0:5432 for their postgres service containers. The second fails with: failed to set up container networking: ... Bind for 0.0.0.0:5432 failed: port is already allocated The host-port mapping isn't actually needed — the workflow uses \`DATABASE_URL: postgresql+asyncpg://...@postgres:5432/...\` (hostname \`postgres\` is the service container's docker-network DNS name). The tests run inside the act container which is on the same docker network, so they reach postgres without going through the host. Removing \`ports: 5432:5432\` from both backend and e2e job service definitions lets multiple postgres services run in parallel on different docker networks without colliding on the host. Surfaced when PR #150 ran in parallel with another job after the multi-runner setup. Backend instant-failed in 2s on the docker run. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/ci.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a801f984..9ca994d4 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,8 +17,11 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: resolutionflow_test - ports: - - 5432:5432 + # No host port mapping. Tests connect to `postgres:5432` (the service + # container's docker-network DNS name), not `localhost:5432`. With + # multiple Gitea runners on the same homelab box, host-port mapping + # 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-interval 10s @@ -136,8 +139,11 @@ jobs: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: resolutionflow_test - ports: - - 5432:5432 + # No host port mapping. Tests connect to `postgres:5432` (the service + # container's docker-network DNS name), not `localhost:5432`. With + # multiple Gitea runners on the same homelab box, host-port mapping + # 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-interval 10s -- 2.49.1 From 7f714363ddf0575872403841018b8bcd70f08dce Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 12:07:57 -0400 Subject: [PATCH 10/15] =?UTF-8?q?perf(ci):=20pytest-xdist=20with=20per-wor?= =?UTF-8?q?ker=20DBs=20=E2=80=94=2022m=20=E2=86=92=20~4m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend suite is the slow gate (1076 passed locally in 22m27s on fix/ci-workflow-config). Adding pytest-xdist with per-worker DB isolation drops it to ~4m20s on the 8-core homelab runner. Verified locally: `pytest -n auto --no-cov` finished in 4m28s real time (15m19s user — confirms ~5× parallelism). How it works: - conftest.py reads `PYTEST_XDIST_WORKER` (set per worker by xdist — 'gw0', 'gw1', …). When set, derives a per-worker DB URL like `…/resolutionflow_test_gw0`. The base DB stays for serial / master runs. - `_ensure_worker_db_exists` runs synchronously at conftest import, connects to the postgres maintenance DB, and `CREATE DATABASE`s the worker-suffixed DB if it doesn't exist. Idempotent across runs. - The "test" safety guard still applies — every worker DB name contains "test" so the assertion holds. - The per-test `DROP SCHEMA public CASCADE` now operates on the worker's isolated DB, no cross-worker race. CI workflow: backend job switches to `pytest -n auto`. Coverage still collected (pytest-cov has built-in xdist support). Adds `pytest-xdist==3.6.1` to requirements-dev.txt. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/ci.yml | 6 +++- backend/requirements-dev.txt | 1 + backend/tests/conftest.py | 55 +++++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9ca994d4..4399db68 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -69,11 +69,15 @@ jobs: run: cd backend && python scripts/check_tenant_filters.py - name: Run tests with coverage + # `-n auto` parallelizes across all runner cores via pytest-xdist. + # conftest.py creates a per-worker DB (resolutionflow_test_gw0, + # resolutionflow_test_gw1, …) so the per-test DROP SCHEMA doesn't + # race across workers. Master/serial runs keep the base DB. # term-missing dropped — the custom "Display coverage summary" step # below parses coverage.json and prints the same info more concisely. # --maxfail=10 short-circuits on structural breakage so we don't burn # 25 minutes when a fixture explodes. - run: cd backend && python -m pytest --override-ini="addopts=" --maxfail=10 --cov=app --cov-report=json:coverage.json --cov-fail-under=50 + run: cd backend && python -m pytest --override-ini="addopts=" -n auto --maxfail=10 --cov=app --cov-report=json:coverage.json --cov-fail-under=50 - name: Display coverage summary if: always() diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index 7b44660c..5c5d2e00 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -4,6 +4,7 @@ # Testing — pytest-asyncio 0.24+ requires pytest>=8.2 pytest==8.4.2 pytest-asyncio==0.24.0 +pytest-xdist==3.6.1 httpx>=0.27.0 pytest-cov==5.0.0 diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index bcbdb7ea..606609df 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -35,11 +35,64 @@ settings.REQUIRE_INVITE_CODE = False # would silently nuke the dev database. Only DATABASE_TEST_URL is honored, # and the safety assertion below refuses to run against a DB whose name # doesn't contain "test". -TEST_DATABASE_URL = os.environ.get( +_BASE_TEST_DATABASE_URL = os.environ.get( "DATABASE_TEST_URL", "postgresql+asyncpg://postgres:postgres@localhost:5432/resolutionflow_test", ) + +def _worker_db_url(base_url: str) -> str: + """Per-worker DB URL for pytest-xdist parallelization. + + pytest-xdist sets PYTEST_XDIST_WORKER to 'gw0', 'gw1', ... per worker + process. Each worker needs its own database so the per-test + `DROP SCHEMA public CASCADE` doesn't race across workers. Master/serial + runs (no xdist) keep the base DB. The base DB is created by the postgres + service container; per-worker DBs are CREATE DATABASE-d on first import + by `_ensure_worker_db_exists` below. + """ + worker = os.environ.get("PYTEST_XDIST_WORKER") + if not worker or worker == "master": + return base_url + head, tail = base_url.rsplit("/", 1) + db_name, _, query = tail.partition("?") + suffix = f"?{query}" if query else "" + return f"{head}/{db_name}_{worker}{suffix}" + + +def _ensure_worker_db_exists(worker_url: str, base_url: str) -> None: + """Create the per-worker DB if it doesn't exist. Runs synchronously at + conftest import time (before any async test machinery), using psycopg2 + against the postgres maintenance DB. No-op when not running under xdist. + """ + if worker_url == base_url: + return + head, tail = worker_url.rsplit("/", 1) + worker_db = tail.partition("?")[0] + # Strip the +asyncpg dialect for sync psycopg2 + connect to 'postgres'. + sync_head = head.replace("+asyncpg", "") + admin_url = f"{sync_head}/postgres" + # Lazy import — psycopg2 is a transitive backend dep; not imported at + # module top to keep the conftest light when xdist isn't in use. + from sqlalchemy import create_engine + engine = create_engine(admin_url, isolation_level="AUTOCOMMIT") + try: + with engine.begin() as conn: + exists = conn.execute( + sa.text("SELECT 1 FROM pg_database WHERE datname = :n"), + {"n": worker_db}, + ).scalar() + if not exists: + # Identifier interpolation is safe — worker_db is built from + # the trusted base URL + 'gw\d+' worker suffix. + conn.execute(sa.text(f'CREATE DATABASE "{worker_db}"')) + finally: + engine.dispose() + + +TEST_DATABASE_URL = _worker_db_url(_BASE_TEST_DATABASE_URL) +_ensure_worker_db_exists(TEST_DATABASE_URL, _BASE_TEST_DATABASE_URL) + # Belt-and-suspenders: refuse to run tests against a DB whose name doesn't # contain "test". Parses the last path segment of the URL (everything after # the final '/', with query string stripped) so credentials / hosts that -- 2.49.1 From 69f2a37591ef8449767d322e30af2f998cbe7547 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 15:21:25 -0400 Subject: [PATCH 11/15] fix(e2e): update 5 selectors that drifted with FlowPilot/PSA UI changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanical drift between the e2e selectors and the current UI surfaced on the first CI run after PR #149 unblocked the artifact upload step. Five tests, three categories of drift: 1. **Page heading renames** (navigation.spec.ts) - `Sessions` → `Session History` on /sessions - `Account Settings` → `Account Management` on /account 2. **Route rename** (command-palette.spec.ts:74) - The "Troubleshoot with FlowPilot" command palette option now lands on /pilot (Phase 1 of the FlowPilot migration renamed /assistant). /assistant still 301-redirects, so the assertion accepts either. 3. **Feature moved to /sessions** (history.spec.ts, resume.spec.ts) - Default tab on /sessions is "AI Sessions"; flow-session filtering and the Resume button moved behind the "Flow Sessions" tab. Both tests now click that tab before asserting. - resume.spec.ts no longer starts at /trees (Resume buttons aren't rendered there anymore — the flow lives on /sessions). Destination URL (/trees/:id/navigate) is unchanged. No product-code changes — these are pure test updates against the shipped UI. Run the suite locally with `cd frontend && npm run test:e2e` once a fresh build is available. Co-Authored-By: Claude Opus 4.7 --- frontend/e2e/command-palette.spec.ts | 4 +++- frontend/e2e/history.spec.ts | 6 +++++- frontend/e2e/navigation.spec.ts | 4 ++-- frontend/e2e/resume.spec.ts | 12 ++++++++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/frontend/e2e/command-palette.spec.ts b/frontend/e2e/command-palette.spec.ts index c011a1a9..419e679c 100644 --- a/frontend/e2e/command-palette.spec.ts +++ b/frontend/e2e/command-palette.spec.ts @@ -88,6 +88,8 @@ test.describe('command palette smoke tests', () => { await flowpilotOption.click() - await expect(page).toHaveURL(/\/assistant/) + // Phase 1 of the FlowPilot migration renamed /assistant to /pilot. + // /assistant still 301-redirects to /pilot, so accept either landing URL. + await expect(page).toHaveURL(/\/(pilot|assistant)/) }) }) diff --git a/frontend/e2e/history.spec.ts b/frontend/e2e/history.spec.ts index feb14379..7483ec64 100644 --- a/frontend/e2e/history.spec.ts +++ b/frontend/e2e/history.spec.ts @@ -24,9 +24,13 @@ test.describe('session history smoke tests', () => { await page.goto('/sessions') await expect( - page.getByRole('heading', { name: 'Sessions', exact: true }), + page.getByRole('heading', { name: 'Session History', exact: true }), ).toBeVisible() + // Default tab on /sessions is "AI Sessions"; flow sessions live behind + // the "Flow Sessions" tab and only that tab exposes ticket/client filters. + await page.getByRole('button', { name: 'Flow Sessions' }).click() + await page.getByPlaceholder('Search by ticket number...').fill(ticketNumber) await page.getByPlaceholder('Search by client name...').fill(clientName) diff --git a/frontend/e2e/navigation.spec.ts b/frontend/e2e/navigation.spec.ts index 2cfa3141..c2858ee8 100644 --- a/frontend/e2e/navigation.spec.ts +++ b/frontend/e2e/navigation.spec.ts @@ -14,7 +14,7 @@ test.describe('authenticated navigation smoke tests', () => { await page.goto('/sessions') await expect( - page.getByRole('heading', { name: 'Sessions', exact: true }), + page.getByRole('heading', { name: 'Session History', exact: true }), ).toBeVisible() }) @@ -30,7 +30,7 @@ test.describe('authenticated navigation smoke tests', () => { await page.goto('/account') await expect( - page.getByRole('heading', { name: 'Account Settings' }), + page.getByRole('heading', { name: 'Account Management' }), ).toBeVisible() }) }) diff --git a/frontend/e2e/resume.spec.ts b/frontend/e2e/resume.spec.ts index 8d51a9d1..73211735 100644 --- a/frontend/e2e/resume.spec.ts +++ b/frontend/e2e/resume.spec.ts @@ -18,9 +18,17 @@ test.describe('session resume smoke tests', () => { }) try { - await page.goto('/trees') + // Resume flow moved off /trees onto the Flow Sessions tab of /sessions + // during the FlowPilot migration. The destination (/trees/:id/navigate) + // is unchanged — only the entry point shifted. + await page.goto('/sessions') + await expect( + page.getByRole('heading', { name: 'Session History', exact: true }), + ).toBeVisible() + 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 }).filter({ hasText: 'Resume' }).first() + const resumeCard = page.locator('.bg-card').filter({ hasText: tree.name }).first() await expect(resumeCard).toBeVisible() await resumeCard.getByRole('button', { name: 'Resume' }).first().click() -- 2.49.1 From 6656ebdead02d784d4032abf932a838ef5f62365 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 15:55:08 -0400 Subject: [PATCH 12/15] =?UTF-8?q?docs(ai):=20reflect=20PR=20consolidation?= =?UTF-8?q?=20=E2=80=94=20#151/#152=20merged=20into=20#150?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .ai/HANDOFF.md | 61 ++++++++++++++------------------------------------ 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 26edc7a1..67716245 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -4,64 +4,37 @@ **Last updated:** 2026-04-25 (America/New_York) -**Active task:** Land three open CI PRs (#150 + #151 + #152), 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 + e2e gates on `main`. See [CURRENT_TASK.md](CURRENT_TASK.md). -**Branches:** Three open PRs, all independent of each other for correctness: -- `fix/ci-workflow-config` → PR #150 -- `fix/ci-pytest-xdist` → PR #151 (stacked on #150 for context but mergeable on its own) -- `fix/e2e-test-selectors` → PR #152 +**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. -**Runner setup:** Three Gitea Actions agents are now registered on the homelab box, so `backend` / `frontend` / `e2e` jobs run truly in parallel instead of serializing on a single agent. Combined with PR #151's xdist parallelization, the previous 1h 14m wall-clock should drop to ~6–10 min. +**Runner setup:** Three Gitea Actions agents are registered on the homelab box, so `backend` / `frontend` / `e2e` jobs run in parallel instead of serializing on a single agent. Combined with the xdist parallelization in this PR, the previous 1h 14m wall-clock should drop to ~9–12 min. -## Three open PRs +## What's on PR #150's branch (consolidated) -### PR #150 — `fix/ci-workflow-config` → main +Eight commits, all CI-recovery work that landed together because they were inter-dependent: -Carries: -- The Codex commit (`49f8856 wip(handoff): restore backend suite to green`) — fixes 54 backend test failures. -- Workflow fixes: `DATABASE_TEST_URL` env, `actions/upload-artifact` v3 pin. -- Most-recent commit (`e976fb4`): - - Mocks `_extract_template_parameters` in `test_record_decision_persists_and_bumps_state_version` (last test failing on CI; needed an AI provider key the runner doesn't have). Verified locally — passes. - - pip + npm caches in all three jobs. - - Drops `--cov-report=term-missing` (the custom "Display coverage summary" step prints the same info from JSON). - - Adds `--maxfail=10` so structural breakage fails fast. - -**Expected CI on this PR:** all three jobs green for the first time in months. - -### PR #151 — `fix/ci-pytest-xdist` → main (stacked on #150) - -Carries (on top of #150): -- `pytest-xdist==3.6.1` in `requirements-dev.txt`. -- `conftest.py` adds `_worker_db_url` + `_ensure_worker_db_exists`. Each xdist worker gets its own DB (`resolutionflow_test_gw0`, `gw1`, …) so the per-test `DROP SCHEMA public CASCADE` doesn't race across workers. -- Workflow's pytest invocation gains `-n auto`. - -**Measured locally:** backend suite goes from `22m 27s` (serial, 1076 passed) → `4m 28s` (8 workers, 1076 passed). Same exit code, same test count. - -### PR #152 — `fix/e2e-test-selectors` → main - -Carries: five Playwright e2e selector updates against the current UI. The drift was inherited from the FlowPilot/PSA migration: - -- `Sessions` → `Session History` (page heading) -- `Account Settings` → `Account Management` (page heading) -- `/assistant` → `/pilot` (Phase 1 route rename; redirect still works) -- Flow-session filtering and the Resume button moved behind the "Flow Sessions" tab on `/sessions` (default tab is "AI Sessions") -- `resume.spec.ts` no longer starts at `/trees` — Resume button rendering moved to the session card on `/sessions` - -No product-code changes. Pure test updates. +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`. ## Immediate next steps -1. **Merge PR #152 first.** Smallest, lowest risk, no shared file with the other two PRs. -2. **Merge PR #150 next.** Backend test suite should be fully green (1076 passed, 0 failed, 0 errors). -3. **Merge PR #151 last.** Backend job time drops to ~4–6 min on the runner. -4. **Enable backend gate** on `main` branch protection — append `"CI / backend (pull_request)"` to `status_check_contexts`: +1. **Watch PR #150's CI** on its latest sha (`69f2a37`). Backend should run ~9 min via xdist; frontend ~6 min; e2e ~5 min with the selector fixes. +2. **Merge PR #150** when all three jobs are green. +3. **Enable backend gate** on `main` branch protection — append `"CI / backend (pull_request)"` to `status_check_contexts`: ```bash curl -X PATCH -H "Authorization: token $GITEA_TOKEN" \ "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)"]}' ``` -5. **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. +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 -- 2.49.1 From 261814ae65c008e23a5340bd9922d7cabc3f8996 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 15:59:00 -0400 Subject: [PATCH 13/15] =?UTF-8?q?perf(ci):=20decouple=20e2e=20from=20front?= =?UTF-8?q?end=20=E2=80=94=20build=20frontend=20inline=20in=20e2e=20job?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: e2e \`needs: [frontend]\` waited for the frontend job to upload a build artifact, then downloaded it. With multiple runners this means the third runner sat idle for ~6 min while frontend ran, then started e2e — total wall-clock max(backend, frontend+e2e) ≈ 11 min. After: e2e builds its own frontend (npm ci + npm run build are already in the job; just dropped the artifact download step and added the build). e2e starts immediately on a free runner. Adds ~1-2 min to the e2e job duration but removes ~5 min of waiting and eliminates the cross-job artifact mechanism entirely. Side benefit: no more \`actions/upload-artifact\` v3/v4 GHES headaches on the cross-job handoff. The \`if: always()\` upload of the playwright-report at the end of e2e is kept (failure report retrieval is still useful), but it's a leaf-output, not a dependency. Net wall-clock: max(backend=9m, frontend=6m, e2e=7m) ≈ 9 min on the 3-runner setup, down from ~11 min. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/ci.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4399db68..26275382 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -125,15 +125,14 @@ jobs: - name: Build run: cd frontend && NODE_OPTIONS="--max-old-space-size=4096" npm run build - - name: Upload build artifact - uses: actions/upload-artifact@v3 - with: - name: frontend-dist - path: frontend/dist - retention-days: 1 + # Build artifact intentionally NOT uploaded. The e2e job below builds + # its own frontend rather than downloading one from this job, so there + # is no need for the cross-job artifact handoff (which previously broke + # on actions/upload-artifact@v4 GHES support and forced a v3 pin). + # Decoupling also lets e2e start immediately rather than waiting for + # this job to finish — important on a multi-runner setup. e2e: - needs: [frontend] runs-on: ubuntu-latest services: @@ -188,11 +187,13 @@ jobs: - name: Install frontend dependencies run: cd frontend && npm ci - - name: Download frontend build - uses: actions/download-artifact@v3 - with: - name: frontend-dist - path: frontend/dist + - name: Build frontend + # Building inline (instead of downloading an artifact from the + # frontend job) drops the cross-job dependency, so e2e can start + # 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 - name: Install Playwright browser run: cd frontend && npx playwright install --with-deps chromium -- 2.49.1 From ede6eebf9a0127d9c0df87be2e1da35aef05d9e4 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 16:12:19 -0400 Subject: [PATCH 14/15] docs(ai): note e2e decoupling commit (261814a) in HANDOFF Co-Authored-By: Claude Opus 4.7 --- .ai/HANDOFF.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.ai/HANDOFF.md b/.ai/HANDOFF.md index 67716245..51872b94 100644 --- a/.ai/HANDOFF.md +++ b/.ai/HANDOFF.md @@ -8,11 +8,11 @@ **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. -**Runner setup:** Three Gitea Actions agents are registered on the homelab box, so `backend` / `frontend` / `e2e` jobs run in parallel instead of serializing on a single agent. Combined with the xdist parallelization in this PR, the previous 1h 14m wall-clock should drop to ~9–12 min. +**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. ## What's on PR #150's branch (consolidated) -Eight commits, all CI-recovery work that landed together because they were inter-dependent: +Seven CI-recovery commits that landed together because they were inter-dependent: 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+). @@ -20,10 +20,21 @@ Eight commits, all CI-recovery work that landed together because they were inter 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 + +| Job | Duration | Starts at | +|---|---|---| +| 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. ## Immediate next steps -1. **Watch PR #150's CI** on its latest sha (`69f2a37`). Backend should run ~9 min via xdist; frontend ~6 min; e2e ~5 min with the selector fixes. +1. **Watch PR #150's CI** on its latest sha (`261814a`). All three jobs should run concurrently and finish within ~9 min total. 2. **Merge PR #150** when all three jobs are green. 3. **Enable backend gate** on `main` branch protection — append `"CI / backend (pull_request)"` to `status_check_contexts`: ```bash -- 2.49.1 From 1e3a6cfa01db51359d2e5d26cb1129f446c2ec9e Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 25 Apr 2026 16:42:33 -0400 Subject: [PATCH 15/15] 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} > -
+
-- 2.49.1