diff --git a/ai/backend-context.md b/ai/backend-context.md index 7fc2f71..c9e9609 100644 --- a/ai/backend-context.md +++ b/ai/backend-context.md @@ -131,6 +131,9 @@ intentionally unclaimed; the built-in health endpoint is `/up`. - Run the focused PHPUnit test while developing. - Use `just backend-types-check` for Larastan and `just backend-test` for the full PHPUnit suite during iteration. +- The completion gate runs `backend-test-unit` and `backend-test-feature` as + concurrent processes. This is safe because each process owns its own + in-memory SQLite database. - Fix failures caused by the change. Report unrelated baseline failures precisely rather than expanding scope silently. - The shared `just test-all` command is the required completion gate. Focused diff --git a/ai/frontend-context.md b/ai/frontend-context.md index 1d2997c..5df5d5a 100644 --- a/ai/frontend-context.md +++ b/ai/frontend-context.md @@ -95,6 +95,10 @@ backend and does not proxy the frontend. - Cypress is configured under `cypress/` and runs through `npm run test:e2e` or `just frontend-cypress-run`. +- Every Cypress spec must appear in exactly one `frontend_specs_*` group in + the root `justfile`. The completion gate runs those groups concurrently, + and its `cypress-spec-coverage` job rejects ungrouped, duplicate, or missing + specs. Keep `frontend-cypress-run` for whole-suite iteration. - Prefer the cheapest test seam that proves the behavior. Cypress covers routing, browser forms, authentication flows, request wiring, and responsive behavior. diff --git a/ai/shared.md b/ai/shared.md index ed0a14a..eb2bd6b 100644 --- a/ai/shared.md +++ b/ai/shared.md @@ -223,12 +223,16 @@ gate passes against that worktree: 3. Run the complete gate from the worktree root: ```sh - direnv exec "$(git rev-parse --show-toplevel)" just test-all + JUST_JOBS=4 direnv exec "$(git rev-parse --show-toplevel)" just test-all ``` 4. Do not hand-assemble a substitute from focused commands. `test-all` runs - frontend format and lint checks, frontend type checking, Larastan, the - production build, PHPUnit, and Cypress in fail-fast order. + one pool of 13 jobs: frontend format, lint, type, build, and Cypress + groups; Larastan; the PHPUnit Unit and Feature suites; and the Cypress + spec coverage guard. Jobs report as they finish, failed output is buffered + and printed at the end, and every job runs even after another job fails. + `JUST_JOBS` limits concurrency and defaults to 4. Use 2 when resources are + constrained or 1 for a serial debugging run. 5. Everything must pass before completion. Report exact baseline or environmental failures rather than hiding them. 6. If the stack was started only for validation, stop it when finished: diff --git a/justfile b/justfile index ecd141f..ab14801 100644 --- a/justfile +++ b/justfile @@ -1,29 +1,87 @@ set shell := ["bash", "-c"] +# How many jobs the parallel gate runs at once. Lower this when another +# worktree stack is active, or use 1 for a serial debugging run. +jobs := env('JUST_JOBS', '4') + default: @just --list -# Full completion gate. Start the worktree stack before running it because -# Cypress exercises the frontend and its backend wiring. +# Full completion gate. Every check runs in one bounded pool, with likely +# long Cypress groups scheduled first and shorter jobs filling free lanes. +# Start the worktree stack before running it because Cypress needs Vite. test-all: - @echo "==> frontend format + lint checks" - just frontend-format-check - just frontend-lint-check - @echo "==> frontend type check" - just frontend-type-check - @echo "==> backend static analysis" - just backend-types-check - @echo "==> frontend production build" - just frontend-build - @echo "==> backend tests" - just backend-test - @echo "==> frontend Cypress tests" - just frontend-cypress-run + just par "gate" {{ jobs }} \ + frontend-cypress-account frontend-cypress-session \ + frontend-cypress-sets backend-types-check \ + frontend-cypress-scheduling frontend-build \ + backend-test-feature frontend-cypress-today \ + frontend-lint-check frontend-type-check backend-test-unit \ + frontend-format-check cypress-spec-coverage + +# Run recipes concurrently, buffering each one's output to its own log. +# Report jobs as they finish, then print every failed job's complete log. +[private] +par label max +targets: + #!/usr/bin/env bash + set -uo pipefail + + targets=({{ targets }}) + logs=$(mktemp -d) + declare -A target_of started_at + failed=() + next=0 + running=0 + + echo "==> {{ label }} (${#targets[@]} jobs, up to {{ max }} at a time)" + + while (( next < ${#targets[@]} || running > 0 )); do + while (( next < ${#targets[@]} && running < {{ max }} )); do + target="${targets[next]}" + just "$target" > "$logs/$target.log" 2>&1 & + target_of[$!]="$target" + started_at[$!]=$SECONDS + next=$(( next + 1 )) + running=$(( running + 1 )) + done + + wait -n -p finished + status=$? + running=$(( running - 1 )) + target="${target_of[$finished]}" + elapsed=$(( SECONDS - started_at[$finished] )) + + if (( status == 0 )); then + printf ' ok %-28s %4ds\n' "$target" "$elapsed" + else + printf ' FAIL %-28s %4ds\n' "$target" "$elapsed" + failed+=("$target") + fi + done + + if (( ${#failed[@]} == 0 )); then + rm -rf "$logs" + exit 0 + fi + + for target in "${failed[@]}"; do + printf '\n--- %s ---\n' "$target" + cat "$logs/$target.log" + done + printf '\n{{ label }} failed: %s\n' "${failed[*]}" + printf 'logs kept in %s\n' "$logs" + exit 1 # Backend backend-test *args: - cd backend && php artisan test {{args}} + cd backend && php artisan test {{ args }} + +backend-test-unit *args: + cd backend && php artisan test --testsuite=Unit {{ args }} + +backend-test-feature *args: + cd backend && php artisan test --testsuite=Feature {{ args }} backend-types-check: cd backend && composer types:check @@ -53,3 +111,74 @@ frontend-build: frontend-cypress-run: cd frontend/website && npm run test:e2e + +# Each Cypress spec must belong to exactly one feature group. The complete +# gate runs these groups concurrently; the whole-suite recipe above remains +# available for focused iteration. +frontend_specs_account := "confirm-email login signup" +frontend_specs_session := "guest-auth session-auth" +frontend_specs_sets := "set-layout sets-dashboard" +frontend_specs_scheduling := "set-scheduling" +frontend_specs_today := "today-assignments" + +frontend-cypress-account: + just _cypress {{ frontend_specs_account }} + +frontend-cypress-session: + just _cypress {{ frontend_specs_session }} + +frontend-cypress-sets: + just _cypress {{ frontend_specs_sets }} + +frontend-cypress-scheduling: + just _cypress {{ frontend_specs_scheduling }} + +frontend-cypress-today: + just _cypress {{ frontend_specs_today }} + +[private] +_cypress +names: + cd frontend/website && specs=$(for name in {{ names }}; do \ + printf 'cypress/e2e/%s.cy.ts,' "$name"; done) && \ + npm run test:e2e -- --spec "${specs%,}" + +cypress-spec-coverage: + just _spec-coverage {{ frontend_specs_account }} \ + {{ frontend_specs_session }} {{ frontend_specs_sets }} \ + {{ frontend_specs_scheduling }} {{ frontend_specs_today }} + +[private] +_spec-coverage +names: + #!/usr/bin/env bash + set -uo pipefail + + cd frontend/website + listed=$(printf '%s\n' {{ names }} | sort) + on_disk=$(ls cypress/e2e/*.cy.ts | xargs -n1 basename \ + | sed 's/\.cy\.ts$//' | sort) + + duplicated=$(echo "$listed" | uniq -d) + unique=$(echo "$listed" | uniq) + ungrouped=$(comm -13 <(echo "$unique") <(echo "$on_disk")) + missing=$(comm -23 <(echo "$unique") <(echo "$on_disk")) + status=0 + + if [ -n "$ungrouped" ]; then + echo "in no group, so never runs in the gate:" + printf ' %s\n' $ungrouped + status=1 + fi + + if [ -n "$missing" ]; then + echo "listed in a group but not on disk:" + printf ' %s\n' $missing + status=1 + fi + + if [ -n "$duplicated" ]; then + echo "in more than one group, so runs twice:" + printf ' %s\n' $duplicated + status=1 + fi + + exit $status