---
name: smoke-test
description: >
  Runtime smoke-test for a freshly-generated SmartStack.app project. Starts
  the backend (`dotnet run`) and the frontend (`npm run dev`) in the
  background, waits for both to become reachable, then probes every page
  route and every API endpoint scaffolded by the pipeline. Fails the run
  if ANY endpoint returns a 4xx/5xx status code or if any page is missing
  from disk. Designed to be the final gate of `ba-develop`
  so cross-stack drift surfaces in CI rather than at the user's first
  manual `npm run dev`.
group: development
cli: cli/run-smoke
mode: execute
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# smoke-test — Runtime gate for freshly-scaffolded projects

## Why this skill exists

`/ba-develop` currently builds (`dotnet build` + `npm run
type-check`) and runs unit tests, but **never starts the application**.
Static audits (`audit-dev-frontend`, `audit-pre-dev`) grep the generated
sources for patterns ; they cannot tell you whether the running app
serves the routes the frontend believes exist.

The ba-002 (Budgets, 2026-05-10) regression exhibited four runtime
failures that every existing gate missed :

- `404 GET /api/budgets/dashboard/alerts` — frontend hook emitted, backend
  endpoint not scaffolded (cross-stack drift)
- `404 GET /api/budgets/dashboard/consolidated` — same drift
- `405 POST /api/budgets/dashboard/budgets/{id}/export` — wrong verb on
  the controller side
- `400 GET /api/user/preferences/for-tenant/{guid}` — platform endpoint
  contract drift, hidden by the absence of a runtime probe

A single end-to-end smoke probe catches all four classes.

## What this skill does

It is invoked as the **last step** of `ba-develop`, after all
scaffolders + audits have completed and before the orchestrator emits
its « done » signal :

1. Spawns the backend (`dotnet run --project <api-csproj>`) in the
   background, waits for `GET /health` (or `/swagger/index.html` as
   fallback) to return 200.
2. Spawns the frontend (`npm run dev` — Vite) in the background, waits
   for `GET /` to return 200 on the configured port (default `3000`).
3. **Page probe** : for each `pageSpec.filePath` in the PRD slice,
   derives the expected URL (`/{appCode}/{module}/{section}/...`) and
   issues a `GET`. Fail if status ≠ 200.
4. **API probe** : extracts every URL referenced inside the generated
   `src/features/**/services/*Service.ts` (where `scaffold-api-client`
   writes — NOT `src/services/api/**`, which never existed) and
   `src/features/**/hooks/use*.ts` files. Issues an anonymous `GET` and fails
   on any UNEXPECTED 4xx/5xx — 404 (missing route, e.g. the NavRoute mismatch),
   405 (wrong verb), 400 (broken contract/config), 5xx. A 401/403 means the
   route EXISTS but is `[Authorize]`-gated → treated as a pass.
5. Tears down both processes ; emits the standard `executeEnvelope`
   with a structured `report` summarising probed routes, statuses,
   and timings.

## How to invoke

```
npx --prefer-offline tsx skills/development/smoke-test/cli/run-smoke/index.ts \
  --project-path <path-to-feature-folder> \
  --prd-slice <path-to-prd-slice.frontend.json> \
  [--backend-port 5000] [--frontend-port 3000] [--timeout-ms 60000] \
  [--module-root <.smartstack/ba/APP/MODULE>] [--admin-token <admin JWT>]
```

`--module-root` enables **axis 4b** (custom-action contract probes): the CLI reads
the module's `pagespecs/*.md`, derives each custom-action endpoint and fires it with
a synthesized VALID body at the sentinel id (`00000000-…`, row) / an empty id set
(bulk) so the handler answers 404 before any commit (contract validated, no mutation).
A `415` (body/content-type rejected) / `405` / `5xx` fails the gate; `2xx`/`404` pass.
Because the endpoints are `[RequirePermission]`, pass `--admin-token` to validate them
authenticated — without it the axis surfaces `smoke.interaction-unavailable` (a medium
note, never a silent pass).

The CLI exits with a non-zero status (and emits `success=false` in the
envelope) on the FIRST endpoint that fails its probe, after best-effort
teardown of the two background processes.

## What it does NOT do (v1 scope)

- **No browser test** (Playwright / headless Chromium) — the v1 probes
  HTTP only. The v2 milestone adds a browser navigation pass that
  captures `console.error` events.
- **No load test** — single GET per endpoint, no concurrency.
- **Anonymous by default** — the HTTP + browser axes probe anonymously; an
  `[Authorize]` endpoint answers 401/403, treated as a PASS (the route exists).
  Real per-role token coverage is the `/uat` skill's job. **Exception: axis 4b**
  (custom-action contract) authenticates when `--admin-token` is supplied — that
  is the ONLY axis that sends a body and validates the request contract.
- **No full UI-journey** — the browser axis LOADS list/home/dashboard routes and
  asserts they render (no overlay/console-error/failed-request); it does not click
  into detail pages, submit forms or click action buttons. The detail page's
  completeness (tabs + edit/delete/header-action buttons) is verified STATICALLY by
  `audit-dev-frontend` DEV-UI-031/032; real per-role clicking is the `/uat` skill.
- **No data assertion** — only status codes are checked (axis 4b classifies them
  but does not read the response body). The aim is to catch missing endpoints (404),
  wrong verbs (405), broken contracts (400/415/500) — not to validate response shapes.

## Wiring as the final gate of ba-develop

The orchestrator MUST invoke this skill as its last step. If the smoke
test fails, the orchestrator **must not signal « done »** ; instead it
should surface the failing endpoints to the user and route to the
relevant fix-skill (controller for 404 on a missing backend endpoint,
frontend-component for a misnamed hook URL, etc.).

The skill's envelope `report.failures[]` carries enough metadata
(`endpoint`, `verb`, `status`, `expectedBy`) for a programmatic
dispatcher to fan out the corrections without re-asking Claude.
