---
name: smoke-http
description: >
  HTTP smoke prober for generated SmartStack applications. Given a list of URLs
  (frontend routes + backend API endpoints), fetches each one, follows redirects,
  applies retry-on-startup, and returns a JSON report of 200/non-200 results.
  Invoked after Phase 4 of ba-develop to detect broken routes and
  missing API endpoints before the user ever sees a 404 in the browser.
phase: development/testing
cli: cli/smoke-http
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# smoke-http

Fetches a list of URLs and reports which ones return the expected status code.
Designed for the `frontend_smoke` gate after `ba-develop` Phase 4,
but usable standalone to probe any app.

## Contract

The CLI is a pure HTTP prober. It does NOT:
- spawn the app (the caller must ensure backend+frontend are running on the
  probed ports),
- interpret response bodies (only status codes + redirect chain),
- know anything about SmartStack conventions beyond the JSON input shape.

This keeps it composable: the Studio's `ba-prd-develop-handlers` wires
`dev-runner.ts testPorts={api:7030, frontend:9030}` then calls this CLI with
the URL list derived from the PRD.

## When to use

- **Gate after Phase 4** of ba-develop: the Studio has spawned
  the generated app on ports 7030/9030 and wants to verify every route + API
  endpoint advertised in the PRD actually responds.
- **Regression probe**: after any change to navigation or routing, re-run on
  the existing app to ensure nothing broke.
- **Standalone smoke**: any CI pipeline wanting a tiny URL reachability
  check — no Playwright/browser involved.

## Invocation

```bash
npx --prefer-offline tsx skills/development/testing/smoke-http/cli/smoke-http/index.ts \
  --probes '[
    { "url": "http://localhost:9030/", "expect": 200 },
    { "url": "http://localhost:9030/crm/prospection/", "expect": 200 },
    { "url": "http://localhost:7030/api/prospects", "expect": 200 }
  ]' \
  --timeout-ms 5000 \
  --retries 10 \
  --retry-delay-ms 500
```

Or load the probes from a file (`--probes-file probes.json`) when the JSON is large.

## Arguments

| Arg | Type | Required | Default | Description |
|-----|------|----------|---------|-------------|
| `--probes` | JSON string | one of | — | Inline array of probe objects. |
| `--probes-file` | string path | one of | — | Path to a JSON file containing an array of probes. |
| `--timeout-ms` | integer | no | `5000` | Per-probe request timeout. |
| `--retries` | integer | no | `10` | How many times to retry a failing probe. Useful for app boot. |
| `--retry-delay-ms` | integer | no | `500` | Delay between retries (with exponential back-off from attempt 3). |
| `--follow-redirects` | boolean | no | `true` | Whether to follow 3xx redirects (up to 5 hops). |
| `--concurrency` | integer | no | `4` | How many probes to run in parallel. |

## Probe shape

```typescript
{
  "url": string,            // required — full URL including scheme + port
  "expect"?: number,        // default 200
  "method"?: "GET" | "HEAD", // default GET — HEAD avoids transferring bodies for static assets
  "label"?: string           // optional human-readable label for the report
}
```

## Output envelope

```json
{
  "success": true,
  "command": "smoke-http",
  "data": {
    "totalProbes": 3,
    "passed": 2,
    "failed": 1,
    "allPassed": false
  },
  "report": {
    "results": [
      { "url": "http://localhost:9030/", "label": null, "expect": 200, "actual": 200, "attempts": 1, "durationMs": 43, "status": "pass" },
      { "url": "http://localhost:9030/crm/prospection/", "label": null, "expect": 200, "actual": 200, "attempts": 3, "durationMs": 2110, "status": "pass" },
      { "url": "http://localhost:7030/api/prospects", "label": null, "expect": 200, "actual": 404, "attempts": 10, "durationMs": 4800, "status": "fail", "error": "expected 200 got 404" }
    ]
  },
  "errors": [],
  "warnings": [],
  "nextSteps": [
    "1 probe failed. Inspect the report and run fix-bug on the failing endpoint(s)."
  ]
}
```

Exit code: `0` if all probes pass, `1` otherwise.

## Retry heuristic

- Attempts 1–2: immediate retry after `retryDelayMs` — catches race conditions during app boot.
- Attempts 3–N: exponential back-off (`delay * 2^(attempt-2)`, capped at 30 s).
- A probe is considered failed when all N retries exhaust without a matching
  status code. Errors thrown (network refused, DNS failure, timeout) count as
  retries; no special treatment.

## Design constraints

- No external HTTP library — uses `fetch` (Node ≥18 native).
- No Playwright / browser involvement — this is a pure reachability probe,
  not a UI test (ui-test covers browser scenarios).
- Stateless: one invocation = one report, no daemon mode.
