<!-- lifecycle-handoff-protocol:start --> Lifecycle handoff (standalone /qa): When no parent orchestrator (one-shot, work) owns the pipeline, invoke /compound then /ship after the QA report — do not end at the report. A PASS is a checkpoint, not completion. If a recorded operator ruling already authorizes shipping (a scope ruling in session-state.md, an explicit instruction), proceed under wg-verified-work-ships-without-asking rather than pausing to re-confirm — held scope that was never implemented has no files to carry along and is not a reason to halt. <!-- lifecycle-handoff-protocol:end -->
Functional QA
Verify that features actually work before merge -- not just that pages render, but that forms submit correctly, external services receive the right data, and data integrity holds across system boundaries.
Scope boundary with /test-browser: This skill verifies functional correctness (user flows + external service state). /test-browser verifies visual rendering, layout regressions, and console errors. They coexist in the pipeline.
Prerequisites
- Local development server running OR a
devscript in the project'spackage.json(auto-started if not running) - Playwright MCP available (for browser scenarios)
- Doppler CLI installed and configured (for API verification scenarios)
Usage
skill: soleur:qa, args: "<plan_file_path>"
The skill reads the plan file's ## Test Scenarios section and executes each scenario.
Workflow
Step 1: Read Plan and Extract Test Scenarios
Read the plan file passed as $ARGUMENTS. Find the ## Test Scenarios section.
If no Test Scenarios section exists: Output "No test scenarios found in plan — skipping QA" and stop. Do not block the pipeline.
If Test Scenarios section is empty: Same as above — warn and skip.
If Test Scenarios contains only Given/When/Then prose with no Browser:, API verify:, or Cleanup: prefixed steps: Output "Test Scenarios are integration-level Given/When/Then prose (no executable Browser:/API verify: steps) — covered by unit test suite + manual Phase 6 cross-check. Skipping automated QA." and stop. Do not start the dev server. This case is common for plans whose QA gate is explicitly manual (e.g., requires a real Anthropic key + live Supabase apply); a dev-server smoke would not add coverage beyond what the typecheck + unit suite already validate. Never block the pipeline on a confirmation prompt for the dev server in a test environment — auto-skip silently.
Step 1.5: Ensure Dev Server is Running
Before executing any browser scenarios, check whether the dev server is reachable. If not, attempt to start it automatically.
- Check if already running:
curl -sf --max-time 3 http://localhost:3000/ >/dev/null 2>&1. If reachable, skip to Step 2 — no action needed. Record that the server was NOT started by QA (so cleanup skips it).
- Detect the dev command: Read
apps/web-platform/package.jsonand extract thescripts.devfield. If nodevscript exists, warn: "No dev script found in package.json — cannot auto-start server. Skipping browser scenarios." Continue to API verification steps (do not block the pipeline).
- Start the server: Change to the
apps/web-platform/directory first (the dev command must run from the app root). Check if Doppler is available (command -v doppler). If available, start viadoppler run -p soleur -c dev -- <dev-command> > "$QA_LOG" 2>&1 &(afterQA_LOG=$(mktemp -t qa-dev-server.XXXXXXXX.log)). If Doppler is unavailable, start via<dev-command> > "$QA_LOG" 2>&1 &. Record the background PID and echoQA_LOG=$QA_LOG— a later Bash call does not inherit the variable, so the path must be recoverable from the transcript. A fixed name would collide with any concurrent QA session.
- Poll for readiness (30s timeout): Poll
http://localhost:3000/until it responds or 30 seconds have elapsed, whichever comes first. If the server responds, proceed to Step 2. If the timeout elapses: - Kill the background process by PID
- Include the last 20 lines of
"$QA_LOG"in the failure report - Report: "Dev server failed to start within 30s. See server output above."
- Continue to API verification steps (do not block the pipeline)
- When
doppler runstarts the dev server but Supabase env vars (NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY) are missing from the Doppler config, the server starts but crashes on first request. Check the server log for "Your project's URL and Key are required" before declaring the server ready.
Step 2: Detect Environment
Determine the Doppler config to use:
# Check if DEPLOY_URL is set (indicates production context)
echo "${DEPLOY_URL:-not_set}"
- If
DEPLOY_URLis set: use Doppler configprd - If
DEPLOY_URLis not set: use Doppler configdev
Store the config name for use in subsequent doppler commands.
Step 2.6: Structural-UI Visual-Regression Gate (#4834 / ADR-049)
This is the gate's semantic home. Run it when the diff (git diff --name-only origin/main...HEAD — the branch-vs-main merge-base diff; do NOT use origin/<branch>...HEAD, which only sees unpushed commits and returns 0 files once the branch is pushed, silently skipping the gate) touches apps/web-platform/app/(dashboard)/, apps/web-platform/components/dashboard/, or any layout.tsx. Skip silently otherwise.
Why this exists: jsdom (vitest) renders no CSS, so md:w-14 / hidden md:block / flex-wrap / display:none regressions ship green through the unit suite (the #4810 class — top-level chrome leaking into drilled routes; a collapsed rail with no icon-only form). The gate renders real CSS in real headless Chromium.
Deterministic layer (BLOCKING). Run the committed nav-states-*.e2e.ts spec in the existing authenticated Playwright project — real headless Chromium + real Next.js SSR seeded by the offline mock-Supabase storageState (e2e/global-setup.ts + e2e/helpers/supabase-mocks.ts). Zero credentials; NO dev-signin; never point at a live origin (CLO: synthetic fixtures only).
Browser-readiness preflight (unsupported-host guard — do this FIRST). On a host newer than the pinned Playwright officially supports (e.g. Ubuntu 26.04 vs the apps/web-platform @playwright/test pin), a plain playwright install fails with does not support chromium ... on <os> and every test then dies at browserType.launch: Executable doesn't exist before any navigation — the false-fail that repeatedly costs a QA session its whole run. Recover automatically by retrying the install with the host-platform override, which pulls the nearest supported fallback build (verified ubuntu26.04-x64 → ubuntu24.04-x64; once the fallback build is in cache the launcher resolves it at runtime with NO override, so the override stays out of the run command and can never leak onto a supported host like CI's Jammy container):
cd apps/web-platform
npx playwright install chromium >"$(mktemp -t qa-pw-install.XXXXXXXX.log)" 2>&1 \
|| PLAYWRIGHT_HOST_PLATFORM_OVERRIDE=ubuntu24.04-x64 npx playwright install chromium
If BOTH the plain install and the override install fail (a genuinely unsupported host with no fallback build), do NOT run the gate and do NOT spend reasoning re-deriving whether it is a regression: this is INFRA-BLOCKED, not a code failure. Record in the QA report — "Step 2.6 nav-states gate INFRA-BLOCKED locally (Playwright browser uninstallable on <os>); CI's containerized e2e job (mcr.microsoft.com/playwright:v1.58.2-jammy) is the authoritative gate per #5009" — and proceed. Never block the pipeline on this.
Then run the gate (no override — the runtime resolves the installed build):
cd apps/web-platform && ./node_modules/.bin/playwright test nav-states --project=authenticated --reporter=list
A non-zero exit FAILS this QA run. The assertions read invariants j