Skip to content

test(lifecycle): make --mode api a real, free Golden Path suite (#1068) - #1147

Merged
frankbria merged 3 commits into
mainfrom
feat/1068-api-lifecycle-tests
Aug 11, 2026
Merged

test(lifecycle): make --mode api a real, free Golden Path suite (#1068)#1147
frankbria merged 3 commits into
mainfrom
feat/1068-api-lifecycle-tests

Conversation

@frankbria

Copy link
Copy Markdown
Owner

Closes #1068 (API half — see Scope below).

What

#948 deleted tests/lifecycle/test_api_lifecycle.py because it was green theatre: a @pytest.mark.skip class whose methods raised NotImplementedError, so scripts/lifecycle --mode api collected only skips and exited 0 while CLAUDE.md advertised it as the pre-PR gate. This is the work #948 spun off.

The new suite drives the Golden Path over HTTP against the real FastAPI app on CODEFRAME_LLM_PROVIDER=mock: workspace init → PRD upload → task generation → approval → batch execution → terminal-state polling → event trail. It costs nothing and needs no ANTHROPIC_API_KEY, so it is deliberately not marked lifecycle — the default CI gate collects it on every PR. That is the point the issue makes about the mock provider: it covers the server-layer seam no unit test reaches, for free, rather than being a paid duplicate of the CLI test.

7 tests, ~7s.

The plan's endpoints were wrong, as the issue warned

The issue preserves the deleted stub's plan and says to verify each endpoint rather than trust it. Checked against the live OpenAPI schema:

plan actual
POST /api/v2/workspace/init POST /api/v2/workspaces
POST /api/v2/tasks/generate POST /api/v2/discovery/generate-tasks
POST /api/v2/batches/run POST /api/v2/tasks/execute

use_llm=false on generation is deliberate: it makes the task list deterministic, so a failure here is a server-layer failure rather than the mock returning prose the decomposer cannot parse (#1115 turned that into a hard error, correctly).

scripts/lifecycle resolves per-mode, not one-size-fits-all

It used to pass -m lifecycle for every mode. With the api suite unmarked, that selects zero tests and exits 0 — the same silent success #948 removed the stubs for. Now each mode carries its own path, marker and key requirement:

mode path marker needs a key
cli test_cli_lifecycle.py -m lifecycle yes, costs money
api test_api_lifecycle.py -m "not lifecycle" no
all tests/lifecycle/ -m "lifecycle or not lifecycle" yes
web exit 3, unchanged

A gate test measures that selector rather than trusting it.

The gates flip, as the issue's last AC requires

tests/test_lifecycle_gates_948.py asserted that api and web are rejected. Now api is asserted to work — and to work with no ANTHROPIC_API_KEY present at all — while web keeps every must-fail-loudly assertion. Two further gates assert AC2 instead of assuming it: the CI step must not --ignore=tests/lifecycle, and the gate's own selector must actually collect the api suite while leaving the paid cli suite behind.

One of these is AST-parsed rather than grepped: the new file's docstring quotes both NotImplementedError and pytest.mark.skip while describing the stub it replaces, so a substring check would report the stub it is documenting.

Review finding, fixed and verified

Codex caught a real one. ApproveTasksRequest is exclusion-shaped (excluded_task_ids, no task_ids) and Pydantic drops unknown fields — so the driver's {"task_ids": [...]} returned 200 while approving the entire backlog. The existing assertion ("the chosen task is READY") held either way, so the suite looked like it covered scoped approval and did not. The helper now computes exclusions from the live task list, and a new test asserts the tasks it did not choose stayed BACKLOG. Verified the guard bites: restoring the old one-line body fails it.

The endpoint's own footgun — 200 + inverted semantics for the intuitive payload — is filed as #1146; no production caller hits that route today.

Known limitations

Testing

  • Full CI gate: uv run pytest tests/ --ignore=tests/e2e -m "not lifecycle"6450 passed, 49 skipped, 421s
  • uv run ruff check . — clean
  • All four scripts/lifecycle modes verified by --dry-run, including with the key unset

Review

Third-party pre-PR review: codex review (opencode skipped — it mutates the tree and returns zero bytes on this repo, per prior sessions). One P2 finding, fixed above.

)

#948 deleted `tests/lifecycle/test_api_lifecycle.py` because it was green
theatre: a `@pytest.mark.skip` class whose methods raised NotImplementedError,
so `scripts/lifecycle --mode api` collected only skips and exited 0 while
CLAUDE.md advertised it as the pre-PR gate. This is the actual work.

The new suite drives the Golden Path over HTTP against the real FastAPI app on
`CODEFRAME_LLM_PROVIDER=mock`: workspace init, PRD upload, task generation,
approval, batch execution, terminal-state polling, and the event trail. It
costs nothing and needs no ANTHROPIC_API_KEY, so it is deliberately NOT marked
`lifecycle` — the default CI gate collects it on every PR, which is the point
the issue makes about the mock provider.

The plan preserved in the issue named endpoints that do not exist as written;
the issue says to verify them, and it was right. Checked against the live
OpenAPI schema:

    plan                          actual
    POST /api/v2/workspace/init   POST /api/v2/workspaces
    POST /api/v2/tasks/generate   POST /api/v2/discovery/generate-tasks
    POST /api/v2/batches/run      POST /api/v2/tasks/execute

`scripts/lifecycle` now resolves per-mode paths, markers and key requirements
rather than passing `-m lifecycle` for everything — with the api suite unmarked,
that combination selects zero tests and exits 0, which is the same silent
success #948 removed the stubs for. A gate test measures that.

`tests/test_lifecycle_gates_948.py` flips as the issue requires: `api` is now
asserted to work (and to work with no API key present), while `web` keeps every
must-fail-loudly assertion. Two further gates assert AC2 rather than assuming
it — the CI step must not `--ignore=tests/lifecycle`, and the gate's own
selector must actually collect the api suite while leaving the paid cli suite
behind.
…1068)

Codex review, and correct. `ApproveTasksRequest` is exclusion-shaped — it has
`excluded_task_ids` and no `task_ids` — and Pydantic drops unknown fields, so
the driver's `{"task_ids": [...]}` returned 200 while approving every BACKLOG
task. The existing assertion (`the chosen task is READY`) held either way, so
the suite looked like it covered scoped approval and did not.

The helper now computes the exclusions from the live task list, and a new test
asserts the tasks it did not choose stayed BACKLOG. Verified the guard bites:
restoring the old one-line body fails it.
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 6m 12s —— View job


GLM Precision Review — PR #1147

Hunting concrete defects only (logic errors, null/error paths, security, data loss, races, contract breaks, resource leaks). Style/coverage/architecture are covered by a separate review.

  • Gather full PR diff + existing comments (no prior findings to avoid)
  • Read each changed file + callers/callees
  • Confirm each suspected defect against real context
  • (no findings to post inline)
  • Summary

Verified against source (not just the diff):

  • POST /api/v2/workspaces (201), POST /api/v2/prd (201), POST /api/v2/discovery/generate-tasks (404 when no PRD → test_generation_is_rejected_without_a_prd holds), POST /api/v2/tasks/approve, POST /api/v2/tasks/execute, GET /api/v2/batches/{id}, GET /api/v2/events — all exist with the shapes the driver assumes.
  • ApproveTasksRequest is genuinely exclusion-shaped (excluded_task_ids, no task_ids); the approve() helper's exclusion math is correct.
  • BatchStatus enum = PENDING/RUNNING/COMPLETED/PARTIAL/FAILED/CANCELLED → the test's TERMINAL set is exact (PENDING/RUNNING correctly excluded).
  • RUN_COMPLETED (runtime.py:422) and BATCH_COMPLETED (conductor.py:1421) are real emitted event types → the events-test disjunction holds.
  • CI gate step (test.yml:279-281) contains -m "not lifecycle" and ignores only tests/e2e, not tests/lifecycle → the three TestTheApiSuiteActuallyRunsInCi assertions target the right step.
  • scripts/lifecycle: per-mode MARKER_ARGS resolution, the api key-check exemption, and ${MARKER_ARGS[@]}/[*] expansions are all correct bash.

✅ GLM review: no defects found.

No concrete failure scenario could be constructed for any hunk — the test's contracts all match the live source, and the bash/AST/test logic is internally consistent.

@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review

I independently traced every endpoint and response model this PR's tests rely on against the live code (not just the PR's own claims) — POST /api/v2/workspaces (201, repo_path), POST /api/v2/prd (201), POST /api/v2/discovery/generate-tasks (task_count/tasks), POST /api/v2/tasks/approve (ApproveTasksRequest is indeed exclusion-only — excluded_task_ids, no task_ids), POST /api/v2/tasks/execute (batch_id), GET /api/v2/batches/{id} (status/task_ids/strategy, and BatchStatus enum values match the TERMINAL set exactly), and GET /api/v2/events (events/event_type). Everything lines up. The scripts/lifecycle mode-resolution logic (key bypass only for api, MARKER_ARGS per mode, dry-run output format) also checks out line-for-line against the new assertions in tests/test_lifecycle_gates_948.py, and the workflow-gate assumptions in TestTheApiSuiteActuallyRunsInCi._gate_step() match .github/workflows/test.yml (lines 279–281) as written. The exclusion-vs-inclusion approve bug described in the PR body is real and the fix (computing exclusions from the live task list) is correct.

Two minor, non-blocking things:

  1. Dead code: TestSseStreamingDeliversEvents._events() (tests/lifecycle/test_api_lifecycle.py:231-239) is defined but never called anywhere in the file. Looks like a leftover from the abandoned attempt to open the SSE stream directly (the well-documented comment block right below it explains why that was dropped) — the raw-SSE-parsing helper itself wasn't cleaned up when that approach was abandoned in favor of asserting via /api/v2/events. Worth deleting since nothing references it.

  2. Loose assertion: test_generation_is_rejected_without_a_prd (tests/lifecycle/test_api_lifecycle.py:206-212) only asserts status_code != 200. That would also pass if the endpoint 500'd for an unrelated server bug, which would mask a regression rather than catch the intended "order of the pipeline is enforced" behavior. Tightening to an explicit set of expected codes (e.g. in (400, 404, 422)) would make the test's intent self-enforcing.

No prior formal review exists on this PR (only a placeholder "working" comment), so this is a fresh pass. Code quality, test structure, and self-documentation (docstrings explaining why, not just what) are strong throughout — the SSE-hang investigation and the honest "known limitations" section in both the PR body and the test file are a good example of documenting a real constraint instead of hiding it.

🤖 Generated with Claude Code

Post-PR review flagged that a hang in this suite would stall the default
backend gate rather than report anything, since it is deliberately unmarked.
The reported reproduction does not occur here — the file runs in ~7s locally
and passed inside the full 6450-test gate run — but the consequence is real
enough to bound: pytest-timeout is already a dependency, so a marker turns any
hypothetical block into a failure with a traceback. Same reasoning that kept
the SSE test out.
@frankbria

Copy link
Copy Markdown
Owner Author

Post-PR third-party review (codex review, post-fix diff)

One P1, on the new suite's TestClient(app) fixture:

When the new unmarked API lifecycle suite runs in the default CI gate, the first request through this TestClient(app) can hang before reaching the handler; I reproduced test_a_project_is_built_through_the_api blocking in starlette.testclient on api.init_workspace(). […] that turns the normal backend gate and scripts/lifecycle --mode api into a non-terminating run.

The reproduction does not hold here, and I checked rather than assumed. The suite ran clean four separate times before this comment: standalone (7 passed in 6.87s), combined with the gates file (34 passed in 17.45s), and inside the full CI gate (6450 passed, 49 skipped, 421s) — which is the exact invocation the finding says would never terminate. The decisive arbiter is the Backend Unit Tests job on this PR, which runs that same command on a clean runner. I am not merging until it is green, and if it hangs the finding is right and this suite comes back out.

The consequence is still worth bounding, so I bounded it. A hang in an unmarked suite stalls the whole backend job instead of reporting anything — which is precisely the reasoning that kept the SSE test out of this PR in the first place. pytest-timeout is already a dependency, so pytestmark = [pytest.mark.v2, pytest.mark.timeout(120)] costs nothing and converts any hypothetical block into a failure with a traceback. The file runs in ~7s, so 120s can only fire on a genuine block.

I did not take the suggested rewrite. Moving to a router-mounted app or httpx.ASGITransport would defeat the point of the suite: it exists to exercise the assembled application — middleware, auth dependencies, rate limiting, lifespan — which is the seam no unit test covers. The router-mounted pattern is what the unit tests under tests/ui/ already do. Swapping to it would leave the same coverage hole #948 spun this issue off to close.

Earlier finding (pre-PR pass), fixed in d13dae1

/api/v2/tasks/approve only accepts excluded_task_ids […] so the call approves every BACKLOG task instead of just the requested subset.

Correct and material — the suite looked like it covered scoped approval and did not, because the assertion held either way. Helper now computes exclusions from the live task list; new test asserts the non-chosen tasks stayed BACKLOG; verified the guard bites by restoring the old body and watching it fail. The endpoint's underlying footgun (200 + inverted semantics for the intuitive payload) is filed as #1146.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 6m 7s —— View job


GLM Precision Review — PR #1147 (synchronize)

Hunting concrete defects only (logic errors, null/error paths, security, data loss, races, contract breaks, resource leaks). Style/coverage/architecture are covered by a separate review.

  • Gather full PR diff + existing comments (avoid repeating findings)
  • Review new commits since last pass (approve exclusion fix d13dae1, 120s timeout 74d3ced)
  • Read each changed file + callers/callees, confirm suspected defects
  • (no findings to post inline)
  • Summary

Verified against source (focused on the two new commits since the prior clean pass):

  • d13dae1 — approve exclusion fix is correct. ApproveTasksRequest (tasks_v2.py:45) is exclusion-only (excluded_task_ids, no task_ids); the endpoint calls runtime.approve_tasks(workspace, excluded_task_ids=...) (tasks_v2.py:669), and approve_tasks (runtime.py:1252) approves every BACKLOG task not in excluded. The driver's excluded = [t["id"] for t in list if t["id"] not in wanted] therefore resolves to exactly wanted. GET /api/v2/tasks defaults to limit=100 (tasks_v2.py:206); the mock decomposer emits a handful of tasks, so the listed set is the full backlog and the exclusion set is complete — no extra task can leak through.
  • 74d3ced — timeout marker is valid. pytest-timeout>=2.3.0 is a declared dep (pyproject.toml:91), and pytest.ini registers --strict-markers, so pytest.mark.timeout(120) resolves rather than erroring. With the default signal method it converts a hypothetical block into a failing traceback, which is the stated intent.
  • Re-confirmed the remaining driver contracts hold: StartExecutionRequest accepts task_ids/strategy and StartExecutionResponse returns batch_id/strategy (tasks_v2.py:90,126); GET /api/v2/events resolves the workspace from workspace_path and returns events[].event_type (events_v2.py:37-72); the bash MARKER_ARGS arrays expand correctly per element under "${MARKER_ARGS[@]}".

✅ GLM review: no defects found.

No concrete failure scenario could be constructed for any hunk — the approve-fix logic, the timeout marker, and the bash/endpoint contracts are all internally consistent and match the live source.

@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review

I read the current diff directly (not just the PR description) and re-checked the two non-blocking items from my prior pass, since the task asked me to be consistent with earlier reviews rather than repeat them wholesale.

Endpoints, contracts, and the approve-bug fix all check out. POST /api/v2/workspaces, POST /api/v2/prd, POST /api/v2/discovery/generate-tasks, POST /api/v2/tasks/approve (ApproveTasksRequest is genuinely exclusion-only — excluded_task_ids, no task_ids), POST /api/v2/tasks/execute, GET /api/v2/batches/{id} (BatchStatus enum matches the TERMINAL set exactly), and GET /api/v2/events all match what the driver in tests/lifecycle/test_api_lifecycle.py assumes. The ApiDriver.approve() fix (computing excluded_task_ids from the live task list instead of guessing at task_ids) is correct, and test_approval_is_scoped_to_the_chosen_tasks is a real regression guard for it — this was a legitimate bug (silent full-backlog approval behind a 200) and the fix + new test close it properly. Filing the endpoint's underlying inclusion/exclusion footgun as a separate issue (#1146) rather than trying to fix production code in a test PR is the right scope call.

scripts/lifecycle mode resolution is correct bash. Traced MARKER_ARGS through all three branches (cli/api/all) and the array expansions ("${MARKER_ARGS[@]}" for exec, ${MARKER_ARGS[*]} for the dry-run echo) — both are right, and I confirmed against pytest.ini's addopts = -m "not e2e_llm and not lifecycle" that a command-line -m does override the ini default (last -m wins), so api mode genuinely selects the unmarked suite instead of matching zero tests, which was the exact failure mode #948 was about.

The TestClient(app) hang concern (raised by codex review post-fix) is reasonably handled. No rewrite to ASGITransport/router-mounted app (which would have quietly reintroduced the coverage gap #948 spun this issue off to close), instead a pytest.mark.timeout(120) bound plus verification the suite is clean across 4 separate runs including inside the full CI gate. That's a defensible tradeoff for an unmarked, default-gate-collected suite — a hang degrades to a failed test with a traceback rather than a stalled job.

Two minor items from my earlier pass are still present, unchanged, still non-blocking:

  1. TestSseStreamingDeliversEvents._events() (tests/lifecycle/test_api_lifecycle.py:236-244) remains dead code — defined, never called. Worth deleting since nothing references it (the real assertion goes through /api/v2/events instead, per the comment block explaining why the direct-stream approach was abandoned).
  2. test_generation_is_rejected_without_a_prd still only asserts status_code != 200, which would also pass on an unrelated 500. Tightening to an explicit in (400, 404, 422) would make the "pipeline order is enforced" intent self-checking rather than coincidentally correct.

Neither is a regression from this update and neither blocks merge.

New in this update (the 74d3ced commit) — the AST-based gate test. test_the_api_file_is_a_real_suite_not_a_stub correctly parses rather than greps, and the reasoning given (the file's own docstring quotes NotImplementedError and pytest.mark.skip while describing the stub it replaces) is accurate — a substring check would false-negative on this exact file. Good catch turning that into an AST walk instead.

Nothing else stood out — test coverage for the stated scope (Golden Path over HTTP, mode-resolution gates, AC1/AC2 assertions) is thorough, the "known limitations" sections (SSE, web mode) are honest about what's not covered rather than papering over it, and the CLAUDE.md/script doc updates match the actual behavior.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

GLM Precision Review — PR #1147 (re-review on synchronize)

Re-reviewed the two commits pushed since the last clean pass: d13dae1 (approve exclusion fix) and 74d3ced (120s timeout marker).

Verified against source, not just the diff:

  • d13dae1 — approve exclusion math is correct. ApproveTasksRequest (codeframe/ui/routers/tasks_v2.py:45) is exclusion-only (excluded_task_ids, no task_ids); the endpoint passes it straight to runtime.approve_tasks(workspace, excluded_task_ids=...) (tasks_v2.py:669), which approves every BACKLOG task not in the set (core/runtime.py:1252-1255). The driver builds excluded = [t["id"] for t in list if t["id"] not in wanted], so it resolves to exactly wanted. GET /api/v2/tasks defaults to limit=100 (tasks_v2.py:206) and the mock decomposer emits only a few tasks, so the listed set is the full backlog and the exclusion set is complete — nothing extra can leak through.
  • 74d3ced — timeout marker is valid. pytest-timeout>=2.3.0 is a declared dependency (pyproject.toml:91), and pytest.ini runs --strict-markers, so pytest.mark.timeout(120) resolves rather than erroring at collection.
  • Re-confirmed: StartExecutionRequest accepts task_ids/strategy and StartExecutionResponse returns batch_id/strategy (tasks_v2.py:90,126); GET /api/v2/events resolves the workspace from workspace_path and returns events[].event_type (events_v2.py:37-72); the bash MARKER_ARGS arrays expand element-wise under "${MARKER_ARGS[@]}".

✅ GLM review: no defects found.

No concrete failure scenario could be constructed for any hunk — the new commits and the surrounding endpoint/bash contracts are internally consistent and match the live source.

severity file:line finding
none

@frankbria

Copy link
Copy Markdown
Owner Author

Demo — every acceptance criterion, with outcome evidence

AC Status Evidence
test_api_lifecycle.py exists with real assertions and passes under CODEFRAME_LLM_PROVIDER=mock 7 passed in 6.87s locally; the file's tests assert on task status transitions (BACKLOGREADYDONE), the batch's terminal state, and the recorded event trail — not on 200s
It runs in CI (free, no ANTHROPIC_API_KEY), not only under scripts/lifecycle See the CI log excerpt below — the suite executed inside Backend Unit Tests, a job that does not declare environment: staging and therefore has no ANTHROPIC_API_KEY at all
test_web_lifecycle.py likewise not in this PR See Scope; --mode web still exits 3 with its pointer, and every gate asserting that still passes
scripts/lifecycle accepts --mode api again; the exit 3 guard is removed for it All four modes verified by --dry-run, including with the key unset
tests/test_lifecycle_gates_948.py flips 27 passedapi now asserted to work; web keeps every must-fail-loudly assertion

The CI run is the evidence, not the green tick

The post-PR review's P1 said this suite would make the backend gate non-terminating. Here is that job's own log:

collected 6505 items / 2 deselected / 6503 selected
tests/lifecycle/test_api_lifecycle.py .......                            [ 79%]
1.62s call  tests/lifecycle/test_api_lifecycle.py::TestTheGoldenPathOverHttp::test_a_project_is_built_through_the_api
=== 6437 passed, 66 skipped, 2 deselected, 62 warnings in 364.36s (0:06:04) ====

Seven dots, 1.62s for the full Golden Path, on a clean runner with no API key. That is simultaneously the disproof of the P1 and the proof of AC2 — the suite is genuinely in the default gate, not merely eligible for it.

Full check status

All 13 checks pass (Backend Unit Tests 6m24s, Test Summary pass). No inline review threads open. claude-review and review / review both pass.

What I am explicitly not claiming

@frankbria
frankbria merged commit 85f187f into main Aug 11, 2026
14 checks passed
@frankbria
frankbria deleted the feat/1068-api-lifecycle-tests branch August 11, 2026 03:52
frankbria added a commit that referenced this pull request Aug 12, 2026
…1153)

* test(lifecycle): drive the Golden Path through the browser (#1068)

The web half of #1068. #948 deleted `tests/lifecycle/test_web_lifecycle.py`
because it was green theatre — a `@pytest.mark.skip` class raising
NotImplementedError, so `--mode web` collected only skips and exited 0 while
CLAUDE.md advertised it as the pre-PR gate. #1147 replaced the API half; this
replaces the web one, and every mode the script offers is now real.

`tests/e2e/lifecycle.spec.ts` starts from an EMPTY directory and drives the
product through the UI: PRD upload, task generation, approval, execution. Every
other spec in the harness asserts how a pre-seeded workspace renders, which
cannot catch a broken write path. 3 tests, ~13s including server startup, free.

Three things had to change for it to be possible:

1. MockProvider's default is "Mock response", which no parser can read. #1115
   correctly made an unparseable decomposition a hard error, so
   CODEFRAME_LLM_PROVIDER=mock could not drive the THINK step at all — the API
   lifecycle test had to pass use_llm=false to avoid the LLM branch entirely.
   The mock now answers exactly one prompt with a valid shape: the task
   decomposition, matched on its full closing sentence rather than a keyword
   (the classifier mistake #1113/#1116/#1064 each made). The canned answer has
   a real dependency edge, because the prompt itself says an empty graph means
   the decomposition is wrong. Everything else is untouched, and a queued
   response or handler still wins.

2. WORKSPACE_ROOT now points at the PARENT of the seeded workspace, so the spec
   can create its own sibling and still sit inside a real allowlist (#896). Not
   unrestricted — the guard stays meaningful, and a third test asserts the
   seeded workspace was not touched.

3. scripts/lifecycle grew a Playwright leg. `web` runs no pytest at all rather
   than running one against a path with no tests, and `all` runs both engines.

Assertions are outcome-based. The run's result is polled from the API as a task
status, not matched as page text: `/done|completed|failed|blocked/` would have
matched the sidebar's "Blockers" link and passed before the run finished. The
approved task reaches DONE and the unapproved one stays BACKLOG — exact, so it
would fail if approval leaked to the whole backlog (the #1146 bug).

The gates in tests/test_lifecycle_gates_948.py invert as that issue requires:
they asserted `api` and `web` FAIL, and now assert every advertised mode
resolves to a suite while a typo is still rejected. Plus: the spec exists and
carries its tag, and a CI step actually selects it — the tag is how both the
script and CI find it, so an untagged spec would silently run in neither.

CI runs it on every PR (`--grep "@smoke|@lifecycle"` in e2e-browser-smoke).
Full backend suite 6502 passed; all 42 chromium specs pass.

* fix(lifecycle): only build pytest args when there is a pytest leg

Bot review. `web` sets MARKER_ARGS=(), and the PYTEST_ARGS array was built
unconditionally — under `set -u`, bash < 4.4 (macOS still ships 3.2) errors on
"${empty[@]}". So the one mode that never uses those args was the one that
would have failed to start, on the shell a contributor is most likely to have.

Bash 5.2 here, which is why it passed locally and in CI. All four modes
re-verified by --dry-run; shellcheck clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P2.23] Implement the web lifecycle test (Playwright; the API half shipped in #1147)

1 participant