# Stress Test

> Orchestrate a destructive demo-workspace QA stress test for React on Rails leakage, memory, performance, security, and framework edge cases. Use only when the user explicitly asks to run stress testing.

- Skill: `shakacode/stress-test` (Agent Skill)
- Install (CLI): `npx skillmds@latest add shakacode/stress-test`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shakacode/stress-test/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: shakacode (https://skillmd.com/u/shakacode)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/shakacode/stress-test

---


# React on Rails Stress Test

You are the orchestrator of a **no-mercy QA stress test** of the React on Rails framework. Your sub-agents are senior software engineers, offensive-security researchers, and pentesters. They do not write polite code reviews — they actually build demos, exercise the framework in extreme, stupid, and adversarial ways, and break it until it leaks.

This stress test is destructive **inside the demo workspace only**. The framework source must never be modified.

Use the skill invocation arguments as the stress-test scope and options. If no arguments are supplied, follow the empty-argument behavior in the table below.

---

## Cross-cutting test concerns (every phase, every persona must explicitly cover these)

These three concerns are **first-class** and must be measured in every demo and every vector, not as an afterthought. Findings in these categories are reported in dedicated report sections.

### 1. Data leakage

What to look for:

- Cross-request leakage in the SSR JS context (module-level state, memoized maps, Apollo / styled-components / MobX caches, console replay history, `globalThis` mutations).
- Cross-tenant leakage via `fragment_cache`, `cached_react_component`, `prerender_caching` keys missing user/locale/role fields.
- Server-only data ending up in client bundle (env vars, secrets, internal hostnames, DB rows) via accidental import, server bundle shipped to browser, render-function returning sensitive payload.
- Sensitive content in error messages, stack traces, `replay_console`, RSC payload error frames, log lines, doctor output.
- Side-channel signal: timing differences, response sizes, cache-hit vs miss observable to user.

How to measure:

- Run two demo requests as different "users" with different IDs/locales/roles. Diff the rendered HTML, the JSON props block, the cached fragments, the RSC payload. Any field that bleeds → finding.
- Inspect every error path under `raise_on_prerender_error` true and false. Trigger a render-function exception that includes a fake secret in scope; check whether it appears in the user-visible response or logs.
- Grep the client bundle for strings that should be server-only (DB connection strings, API keys planted via env in the demo, `process.env.SECRET_*`).
- Snapshot the SSR JS context state (`Object.keys(globalThis)`) before and after N renders; diff.

### 2. Memory leakage

What to look for:

- Heap growth in the Pro node renderer over N renders without restart.
- ExecJS context retaining state across requests (`console.history`, registries, side-effect imports).
- `Component`/`Store` registries growing on HMR / repeated boot.
- Listener double-bind on Turbo navigation, hot reload, or repeated `clientStartup`.
- Render functions returning new closures that never release.
- Node renderer file watcher / Bree worker leaks (FDs, child processes).
- HTTP keep-alive pool leaks under bundle reset.
- Cache layers (prerender_caching, cached_react_component) growing unbounded without TTL.
- Streaming chunks held in memory after client disconnect (no AbortController propagation).

How to measure:

- For each demo, run N=200 (quick) / 2000 (standard) / 20000 (deep) requests in a loop with constant input; record `ps -o rss,vsz`, FD count (`lsof -p`), and renderer worker `process.memoryUsage()` at 0%, 25%, 50%, 75%, 100% of the run. Plot or table-summarize. Steady-state growth above a small threshold per N requests is a leak finding.
- Snapshot the heap programmatically at start and end (see "Heap snapshots — programmatic only" below for the supported mechanism). Diff retained sets; flag suspicious retainers.
- Run the same loop with rolling worker restarts on/off and compare.
- For client-side: open the demo, navigate via Turbo 100 times, take browser heap snapshot, look for detached DOM nodes / retained React fibers.

### 3. Performance degradation

What to look for:

- TTFB regression under load.
- SSR latency p50/p95/p99 for plain, streaming, RSC, RSC payload routes.
- CPU saturation under concurrent requests with `pool_size: 1` (MRI default).
- Cache miss vs hit ratios for `cached_react_component` and `prerender_caching`.
- Streaming first-byte vs total latency vs blocking SSR equivalent — does streaming actually win, and where does it lose?
- Hydration time on the client (Performance API) for various component sizes.
- Build/precompile time for various Shakapacker/auto-bundling configs; large `ror_components/` trees.
- Renderer connection pool saturation, retry storm amplification under fault.
- Prop serialization cost as props grow; JSON.parse cost on the client.

How to measure:

- Use `oha` (preferred) or `ab` to issue concurrent requests at multiple concurrency levels (1, 4, 16, 64 — capped by tier). Record p50/p95/p99/throughput for each demo's hot routes.
- Compare: same component with `prerender: true` vs `prerender: false`. Same RSC route with cache hit vs cold. Same streaming response with Suspense boundaries vs flat.
- Watch CPU and renderer worker saturation during the load (`top -pid <pid>`).
- Treat any case where a documented optimization (caching, streaming, immediate hydration, async loading) does _not_ improve over its alternative as a finding.

A vector or persona that does not produce explicit measurements in these three concerns has not done its job.

---

## Argument parsing

| Form                                                | Meaning                                                                                                                                                                     |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| _(empty)_                                           | Stress test the **whole framework** at the current `HEAD` of `main`                                                                                                         |
| `<commit-sha>`                                      | Focus only on features/code paths touched by that commit                                                                                                                    |
| `<PR#>` or PR URL (`https://github.com/.../pull/N`) | Focus only on changes in that PR                                                                                                                                            |
| `--from <sha>`                                      | All commits from `<sha>`..default-branch (resolved via `git symbolic-ref --short refs/remotes/origin/HEAD`; falls back to `main`/`master` lookup; aborts if neither exists) |
| `--from <sha> --to <sha-or-branch>`                 | All commits from `--from` to `--to` (branch or commit, not necessarily the default branch)                                                                                  |
| `--features <list>`                                 | Comma-separated feature scope filter (see "Feature scopes" below)                                                                                                           |
| `--tier quick\|standard\|deep\|exhaustive`          | Time/coverage tier. Default: `standard` (or `quick` if scope is a single small commit/PR)                                                                                   |
| `--max-hours N`                                     | Override tier's wallclock ceiling. Hard cap; agents stop when reached                                                                                                       |
| `--no-network-fault`                                | Skip toxiproxy / network simulation phase                                                                                                                                   |
| `--skip-pro`                                        | Skip Pro tier (RSC / streaming / node renderer) phases                                                                                                                      |
| `--repo <path>`                                     | Override framework repo location (default: autodetect from `git rev-parse --show-toplevel`)                                                                                 |

Multiple flags can combine: `<PR#> --tier deep --features rsc,streaming --no-network-fault`.

### Tier defaults

| Tier       | Wallclock                          | Demos               | Personas                                         | Pentest                     | Leak/perf load                               | Max parallel agents |
| ---------- | ---------------------------------- | ------------------- | ------------------------------------------------ | --------------------------- | -------------------------------------------- | ------------------- |
| quick      | 30–60 min                          | 1–2                 | 2 (extreme user, novice)                         | smoke                       | N=200 reqs                                   | 4                   |
| standard   | 2–4 hr                             | 5                   | 4 (extreme, novice, distracted senior, attacker) | 1 pass                      | N=2000 reqs, conc 1/4/16                     | 8                   |
| deep       | 8–16 hr                            | 5 + variants        | 6 (+ ops engineer, malicious)                    | full pass with prop-fuzzing | N=20000 reqs, conc 1/4/16/64, heap snapshots | 12                  |
| exhaustive | 24–48 hr ⚠️ **very high API cost** | full feature matrix | all + multiple seeds                             | + regression replays        | + 24h soak per demo                          | 12                  |

The `Max parallel agents` value is the **hard ceiling on concurrent sub-agents at any moment, across all phases** — when the cap is hit, queue further spawns and wait for an in-flight agent to finish. Without this cap, an exhaustive run could reasonably want 6 personas × 7 demos × 13 feature areas of agents simultaneously, which would exhaust the host machine and the API quota.

If no `--tier` and no scope given → `standard`. If a single small commit/PR (≤30 lines diff, ≤3 files) → auto-`quick` unless overridden. When the resolved tier is `exhaustive`, the Phase 0 plan printout must include an explicit cost warning line (see Phase 0 step 8).

### Feature scopes (`--features`)

Comma-separated list. Unknown values abort with the list of valid values. If both `--features` and a commit/PR/range scope are given, the intersection wins (only features touched by the diff AND in the list are stressed).

| Value                 | Stress focus                                                                                                              |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `ssr`                 | All server rendering (covers `ssr-execjs` + `ssr-node` + streaming + non-streaming)                                       |
| `ssr-execjs`          | ExecJS-backed SSR only — context reuse, console polyfill, async/await unsupported paths                                   |
| `ssr-node`            | Pro Node renderer SSR — HTTP transport, fallback, retries, keep-alive                                                     |
| `ssr-no-streaming`    | Traditional non-streaming SSR — `prerender: true`, `react_component_hash`                                                 |
| `streaming`           | Pro streaming SSR — `stream_react_component`, Suspense boundaries, abort propagation, `Rack::Deflater` interaction        |
| `rsc`                 | React Server Components — `'use client'`, server/client boundary, three-bundle build                                      |
| `rsc-payload`         | RSC payload generation specifically — `rsc_payload_react_component`, NDJSON framing, `injectRSCPayload`, manifest mapping |
| `hydration`           | Client hydration — `ClientRenderer`, hydrate vs render decision, mismatch warnings, immediate_hydration                   |
| `immediate-hydration` | Pro immediate hydration only                                                                                              |
| `auto-bundling`       | `auto_load_bundle`, `nested_entries`, `ror_components/`, `generate_packs`, file-system-based registration                 |
| `registration`        | `ReactOnRails.register`, `registerStore`, registry races, double-register, HMR re-register                                |
| `redux`               | Redux store registration, `redux_store`, store generators, hydration of stores                                            |
| `router`              | React Router SSR (`StaticRouter`), wildcard route, location from railsContext                                             |
| `turbo`               | Turbo / Turbolinks integration — `setOptions({ turbo: true })`, page lifecycle, frame swap, stream targets                |
| `hotwire`             | Hotwire-broader (Turbo + Stimulus + morphing)                                                                             |
| `i18n`                | I18n — locale generator, server/client divergence, fallback chain, cache key inclusion                                    |
| `caching`             | All caching: `cached_react_component`, `prerender_caching`, fragment_cache wrapping, dependency_globs                     |
| `prerender-cache`     | `prerender_caching` only — key derivation, locale, bundle digest, collisions                                              |
| `props`               | Props serialization — JSON-unsafe values, U+2028/2029, escaping, large payloads, circular refs                            |
| `rails-context`       | `railsContext` content, mutation, RSC boundary crossing, mailer mode                                                      |
| `config`              | Configuration loading, idempotency, Shakapacker auto-detect, env-var divergence                                           |
| `generators`          | `react_on_rails:install`, `generate_packs`, locale generator, partial-state recovery                                      |
| `doctor`              | `react_on_rails:doctor` task, `FIX=true` mode, false positives/negatives                                                  |
| `node-renderer`       | Pro node renderer process — workers, restart intervals, OOM, file watcher, sandbox                                        |
| `assets`              | Manifest, Shakapacker integration, `private_output_path`, `assets_to_copy`, CDN/asset_host                                |
| `csp`                 | CSP nonce threading, inline `<script>` blocks                                                                             |
| `error-handling`      | `raise_on_prerender_error`, error boundaries during streaming, message leaks                                              |
| `replay-console`      | `replay_console`, console hijack across requests, log injection                                                           |
| `helpers`             | View helpers — `react_component`, `react_component_hash`, `stream_react_component`, `redux_store`                         |
| `licensing`           | Pro license enforcement / graceful degradation when missing                                                               |
| `all`                 | Same as omitting `--features`. Useful to be explicit                                                                      |

Examples:

- `/stress-test --features rsc,streaming` → focus only on RSC and streaming.
- `/stress-test --features ssr-no-streaming,hydration --tier deep` → traditional SSR + hydration deep run.
- `/stress-test 1234 --features streaming` → only streaming-related changes in PR #1234.

---

## Safety rules (STRICT)

- **Never modify framework source.** No edits to `react_on_rails/`, `react_on_rails_pro/`, `packages/`, `internal/`, `docs/`, `lib/`, `spec/`, or any tracked file outside the demo workspace.
- **Never push, commit, or merge.** Pull requests are never opened automatically.
- **GitHub issue creation is disabled by default.** Issues may be opened only after the user explicitly approves a subset at the end of Phase 8 (see Phase 8 for the exact gating).
- **Demo workspace is the only writable area.** Resolved as `WORKSPACE_ROOT=$(git -C "$REPO" rev-parse --show-toplevel)/tmp/stress-test-<timestamp>/`, where `$REPO` is the resolved repo path from Phase 0 (autodetected or supplied via `--repo`). Use this absolute path everywhere; never assume the orchestrator's current working directory. Pre-existing `tmp/.gitignore` already excludes it.
- **Build artifacts also belong in the workspace.** `gem build` and `pnpm -r pack` must write their outputs (`*.gem`, `*.tgz`) under `$WORKSPACE_ROOT/payloads/` so the framework checkout stays clean. Specify `--output` for `gem build` and `--pack-destination` for `pnpm pack`. See Phase 1 step 4.
- **Destructive ops allowed inside the workspace only.** Killing node processes you spawned, OOM-bombing demo apps, corrupting demo manifests, fuzzing demo props with hostile payloads — fine. Touching the user's other processes or system files — never.
- **Process control safety.** When using `kill`, `kill -STOP`, or `kill -CONT`: keep an explicit set of PIDs the orchestrator spawned (capture each via `cmd & echo $!` or equivalent). Before sending any signal, assert (a) the PID is in that set, **and** (b) `ps -p <pid> -o comm=` (Linux) / `ps -p <pid> -o comm=` (macOS) matches the expected process name (`node`, `ruby`, `rails`, `puma`, `webpack`, `bin/shakapacker-dev-server`, etc.). If either check fails, log the mismatch and skip the signal. PID reuse after a process exits is the failure mode this prevents.
- **No Pro license needed.** RoR Pro logs license warnings but does not fail; treat the warnings as expected. Capture them in the report.
- **Network-fault simulation:** if `toxiproxy` is installed, use it. If not, fall back to chaos via `kill -STOP/-CONT` on demo node processes (gated by the process control rule above). Never use `iptables` or anything requiring `sudo`.
- **No skipping hooks** (`--no-verify`, `--no-gpg-sign`, etc.).
- **Synthetic data only for leakage tests.** Plant fake "secrets" with obvious markers (e.g., `LEAK_CANARY_<uuid>`) in env / DB / context to test for leakage. Never use real credentials.

### Command-execution safety

- Treat all argument-derived values as untrusted. Validate before use.
- Commit SHAs: match `^[0-9a-f]{7,40}$`. PR numbers: match `^[1-9][0-9]{0,9}$`. Branch names: reject anything containing whitespace, `..`, leading `-`, or shell metacharacters (`` ` $ ; & | < > ( ) { } * ? [ ] ' " \ ``).
- Repo paths: resolve with the host's path resolution (`realpath`, `python -c 'import os,sys;print(os.path.realpath(sys.argv[1]))'`, etc.); confirm the resolved path contains `react_on_rails.gemspec`; reject paths that traverse outside the resolved repo or contain shell metacharacters.
- Always quote shell variable expansions (`"$repo"`, `"$sha"`). Where a tool supports it, use `--` to terminate options before positional args (`git show -- "$sha"`, `git diff -- "$path"`).
- Where possible, prefer argv-array invocations (programmatic `gh`/`git` callers) over interpolating into a shell command string.
- Feature-flag values: validate strictly against the table; abort with the valid list on unknown.
- `--max-hours N`: must match `^[0-9]+(\.[0-9]+)?$`, parse as a positive number, and satisfy `0.25 <= N <= 96`. Reject negative, zero, non-numeric, or out-of-range values with the allowed range.
- `--from`/`--to`/PR/SHA values must pass the regex above before any `git`/`gh` invocation.
- **Workspace timestamp format.** Use `TS=$(date -u +%Y%m%dT%H%M%SZ)` (UTC, ISO-8601 basic, no separators) for the workspace dir name (`tmp/stress-test-$TS`). This sorts lexically, avoids timezone ambiguity across machines, and matches across logs.
- **GitHub repo slug.** Capture once at Phase 0: `GH_REPO_SLUG=$(gh -R "$REPO" repo view --json nameWithOwner -q .nameWithOwner)`. Pass `--repo "$GH_REPO_SLUG"` (or `-R "$GH_REPO_SLUG"`) to **every** `gh` invocation in any phase, especially Phase 8's `gh label create`, `gh issue create`, and `gh pr view/diff` calls. Without this, a fork's `gh` default remote can silently target the wrong repository.

### Sensitive-data handling for persisted artifacts

- Before writing the environment snapshot (`00-env.md`) or any other artifact that captures shell or Bundler output, run a redaction pass over the captured text. Replace matches of these patterns with `[REDACTED]`:
  - `https?://[^/\s:@]+:[^/\s@]+@` (URL credentials)
  - `Bearer\s+[A-Za-z0-9\._\-]+`
  - `(api[_-]?key|token|secret|password)\s*[:=]\s*\S+` (case-insensitive)
  - `AKIA[0-9A-Z]{16}`, `gh[ps]_[A-Za-z0-9]{20,}`, `xox[baprs]-[A-Za-z0-9-]{10,}`, etc. (well-known token shapes)
  - `ssh://[^@\s]+@[^/\s]+`
- Apply the same redaction to logs and to finding-card excerpts that quote environment values.

---

## Phase 0 — Scope resolution

1. Resolve framework repo path: autodetect via `git -C "$PWD" rev-parse --show-toplevel`, or use `--repo <path>` if supplied. Confirm the resolved path contains `react_on_rails.gemspec` (the file may live at the repo root or in a top-level subdirectory; record the gemspec's directory as `$GEM_ROOT`).
2. Resolve `WORKSPACE_ROOT="$REPO/tmp/stress-test-<timestamp>"`. **Create it now**: `mkdir -p "$WORKSPACE_ROOT"`. All subsequent file writes in Phase 0 use this absolute path.
3. Resolve the **default branch** for use in `--from` (without `--to`) and as the comparison base when scope is "current PR/branch":

   ```bash
   DEFAULT_BRANCH=$(git -C "$REPO" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null \
     | sed 's@^origin/@@')
   if [ -z "$DEFAULT_BRANCH" ]; then
     for cand in main master; do
       git -C "$REPO" show-ref --verify --quiet "refs/remotes/origin/$cand" \
         && DEFAULT_BRANCH=$cand && break
     done
   fi
   [ -z "$DEFAULT_BRANCH" ] && abort "Unable to resolve default branch (no origin/HEAD, main, or master)."
   ```

4. Parse arguments. Validate `--features` values against the table; abort with the valid list on unknown. Validate SHAs/PR numbers per the regexes in "Command-execution safety". Always quote shell expansions.
5. Resolve commit/PR/range scope:
   - Empty → whole framework in scope.
   - `<sha>` → `git -C "$REPO" show "$sha" --stat` to enumerate changed files.
   - `<PR#>` → `gh pr view "$pr" --json files,title,body,baseRefName,headRefName` then `gh pr diff "$pr"` (use the GitHub repo of `$REPO`).
   - `--from <sha>` (no `--to`) → `git -C "$REPO" log "$sha".."$DEFAULT_BRANCH" --stat`, `git -C "$REPO" diff "$sha".."$DEFAULT_BRANCH"`.
   - `--from <sha> --to <sha-or-branch>` → `git -C "$REPO" log "$from".."$to" --stat`, `git -C "$REPO" diff "$from".."$to"`.
6. Build a **feature inventory** from the diff: which Ruby/JS modules changed, which docs pages changed, which behaviors are likely affected. Map changed files to feature-scope tags from the `--features` table.
7. Compute the **effective feature set**:
   - If `--features` not given: use the inventory tags (or all features if no scope).
   - If `--features` given and no commit scope: use the listed features.
   - If both given: **intersection**. Print the intersection back to the user; if empty, abort with a message ("PR #X does not touch any of the requested features: …").
8. Decide tier. The auto-quick rule fires when scope is a single commit/PR with `≤ 30 lines diff` AND `≤ 3 files changed`; this is a heuristic and may downgrade a small-but-high-impact change. Compute the hard wallclock ceiling (tier default, or `--max-hours N` if supplied).
9. Print a one-screen plan to the user before launching agents:

   ```text
   Scope:        <whole framework | commit abc1234 | PR #42 | from a..b>
   Features:     <effective list>
   Tier:         <tier> (max <N> hours)
   Demos:        <N> — <names>
   Personas:     <list>
   Pentest:      <depth>
   Leak/perf:    N=<reqs>, concurrency <list>
   Network-fault:<on|off>
   Pro features: <on|off>
   Max parallel agents: <cap>
   Workspace:    <WORKSPACE_ROOT>
   ```

   When the resolved tier was selected by the auto-quick rule, append:

   ```text
   Auto-tier:    quick (commit is <X> lines / <Y> files; threshold ≤30 lines / ≤3 files).
                 Override with --tier standard|deep|exhaustive.
   ```

   When the resolved tier is `exhaustive`, append:

   ```text
   WARNING: exhaustive tier — expect significant API token usage and extended
   wall-clock time. Consider --tier deep first.
   ```

   Wait for user `go` / `cancel`. (Do not auto-proceed.)

10. **Only after the user types `go`**, save the scope summary to `$WORKSPACE_ROOT/00-scope.md`. (Cancellation leaves the workspace empty; the orchestrator removes the empty timestamped dir on cancel.)

---

## Phase 1 — Workspace setup

1. `mkdir -p "$WORKSPACE_ROOT"/{demos,reports,logs,payloads,metrics}`. (`$WORKSPACE_ROOT` was created in Phase 0; this expands the subdirectory tree.)
   Record `START_TS=$(date -u +%s)` and the resolved tier's wallclock budget (`MAX_SECS=<tier-or-override-in-seconds>`) immediately. Persist both to `$WORKSPACE_ROOT/00-env.md`. Every later wave checks elapsed seconds against this anchor (see "Wallclock enforcement").
2. Verify `tmp/` is in `$REPO/.gitignore`. If not, **abort and tell user** rather than auto-edit `.gitignore`.
3. Snapshot environment to `$WORKSPACE_ROOT/00-env.md`: Ruby version, Node version, pnpm/yarn/npm versions, OS (`uname -srm`), free RAM, disk free, git HEAD of framework, and a **redacted** copy of `bundle env` (apply the redaction patterns in "Sensitive-data handling for persisted artifacts" before writing). Never persist raw `bundle env` output.
4. Build the gem and pack the npm packages **into the workspace** (do not litter the framework checkout). Run under `set -e` (or check `$?` after each command) and abort the entire run on any non-zero exit before scaffolding starts; downstream demos consuming a stale or missing artifact would only fail in confusing, non-obvious ways:

   ```bash
   set -euo pipefail
   gem build "$GEM_ROOT/react_on_rails.gemspec" --output "$WORKSPACE_ROOT/payloads/react_on_rails.gem"
   # Enumerate user-facing packages explicitly so we don't pick up internal/dev
   # packages from `pnpm -r`. Add to this list if a demo needs another package.
   PNPM_PACK_PACKAGES=(
     react-on-rails
     react-on-rails-pro
     react-on-rails-pro-node-renderer
     create-react-on-rails-app
   )
   for pkg in "${PNPM_PACK_PACKAGES[@]}"; do
     ( cd "$REPO" && pnpm --filter "$pkg" pack --pack-destination "$WORKSPACE_ROOT/payloads" )
   done
   ```

   If `--skip-pro` is set, drop the `react-on-rails-pro*` entries from the list before packing. Save the resulting `*.gem` and `*.tgz` paths for each demo's `Gemfile`/`package.json` to consume via `path:` / `file:`.

5. Plant **leak canaries** for data-leakage testing: generate `LEAK_CANARY_<uuid>` strings, set them as demo-only env vars, demo DB rows, and synthetic "user" fields. Record canaries to `$WORKSPACE_ROOT/payloads/canaries.txt`. Agents will grep responses, bundles, logs, and caches for these.

### Cross-platform measurement helpers

Define wrapper helpers and use them everywhere instead of bare `ps`/`top`/`lsof` calls. Branch on `uname -s`:

```bash
rss_kb() { # arg: pid
  case "$(uname -s)" in
    Linux)  awk '/^VmRSS:/ {print $2}' /proc/"$1"/status ;;
    Darwin) ps -p "$1" -o rss= | awk '{print $1}' ;;
    *) echo "" ;;
  esac
}
fd_count() { # arg: pid
  case "$(uname -s)" in
    Linux)  ls /proc/"$1"/fd 2>/dev/null | wc -l ;;
    Darwin) lsof -p "$1" 2>/dev/null | tail -n +2 | wc -l ;;
    *) echo 0 ;;
  esac
}
```

If `pidstat` (from `sysstat`) is available, prefer it as a primary source and fall back to the helpers above.

### Heap snapshots — programmatic only

`chrome://inspect` requires a display and cannot run on SSH/CI hosts. Use a programmatic path as the primary mechanism:

- Add the `heapdump` (or `v8-profiler-next`) npm package to demos that need heap snapshots. Trigger a snapshot via a signal handler or HTTP endpoint on the demo:
  `process.on('SIGUSR2', () => require('heapdump').writeSnapshot('/path/under/$WORKSPACE_ROOT/metrics/heap-<ts>.heapsnapshot'))`.
- For the Pro Node renderer, use the renderer's documented `kill -USR2 <pid>` path (writing to the workspace's `metrics/` directory).
- `chrome://inspect` and DevTools may still be used as a manual follow-up step, but never as the only measurement.

---

## Phase 2 — Demo scaffolding (parallel, feature-driven)

Spawn N parallel sub-agents (general-purpose), one per demo. **Demos are selected based on the effective feature set** from Phase 0:

| Feature(s) in scope                                                                                                   | Demo to scaffold                                                                  |
| --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `ssr-no-streaming`, `ssr-execjs`, `redux`, `router`, `helpers`, `props`, `rails-context`, `hydration`, `registration` | **Demo A: SSR + Redux + React Router**                                            |
| `turbo`, `hotwire`, `auto-bundling`, `assets`, `csp`                                                                  | **Demo B: Hotwire/Turbo + react_component**                                       |
| `streaming`, `node-renderer`, `error-handling`                                                                        | **Demo C: Pro streaming SSR** (skipped if `--skip-pro`)                           |
| `rsc`, `rsc-payload`, `immediate-hydration`                                                                           | **Demo D: Pro RSC** (skipped if `--skip-pro`)                                     |
| `auto-bundling`, `generators`, `helpers`, `i18n`                                                                      | **Demo E: Multi-component auto-bundling**                                         |
| `caching`, `prerender-cache`                                                                                          | **Demo F: Cache-permutation app** (multiple `cached_react_component` configs)     |
| `config`, `doctor`, `generators`, `licensing`                                                                         | **Demo G: Config/install matrix** (boots multiple times with mutated config)      |
| `replay-console`, `error-handling`                                                                                    | tests inside whichever demo applies; not a standalone demo                        |
| `csp`                                                                                                                 | adds CSP middleware to Demo A or B; not standalone                                |
| `all` or empty scope                                                                                                  | quick → A only; standard → A, B, C, D, E; deep / exhaustive → A, B, C, D, E, F, G |

`standard` deliberately omits Demos F and G to fit its 2–4 hr ceiling — caching and config-matrix exercises are inherently slower (multiple boots, multiple cache permutations). They are included by default in `deep` and `exhaustive`. To force them at `standard`, pass `--features caching,prerender-cache,config,doctor,licensing` (which selects F and G) alongside `--tier standard`. The Phase 0 plan printout always lists the resolved demo set explicitly so the exclusion is visible.

The total number of concurrent scaffolding agents is capped by the tier's `Max parallel agents` value. If more demos are required than the cap allows, queue and process in waves.

Each scaffolding agent:

- Creates `$WORKSPACE_ROOT/demos/<demo-name>/`.
- Resolves the **Rails version** by reading the runtime dependency on `railties` from `$GEM_ROOT/react_on_rails.gemspec` (and the Rails matrix declared in `react_on_rails/Gemfile.development_dependencies` / CI configs if more granular), then pinning to the latest minor allowed by that constraint at the time of the run. Record the resolved version in `$WORKSPACE_ROOT/00-env.md`. All demos in a single run use the same resolved version so results are comparable.
- `rails _<resolved-version>_ new <demo-name> --skip-javascript ...`.
- Installs the locally-built gem from `$WORKSPACE_ROOT/payloads/*.gem` (`gem 'react_on_rails', path: '<workspace-payloads>'`) and the locally-packed npm package from `$WORKSPACE_ROOT/payloads/*.tgz` (`"react-on-rails": "file:<workspace-payloads>/react-on-rails-*.tgz"`).
- Runs `bin/rails generate react_on_rails:install --typescript` (or non-TS for one of the demos).
- Builds the documented happy path. Verifies it boots, hydrates, and SSRs cleanly. Saves baseline screenshots/curl output.
- Plants the leak canaries from Phase 1 in the demo's env, DB seed, controller `@user`, and one sample render-function-thrown error.
- Captures **baseline metrics** (cold-start memory, RSS after 50 warm requests, p50 latency for the hot route) to `$WORKSPACE_ROOT/metrics/<demo>-baseline.json`. Subsequent stress phases compare against this baseline.
- Logs every command to `$WORKSPACE_ROOT/logs/scaffold-<demo>.log`.
- Reports back: demo path, baseline OK/FAIL, anomalies during install (deprecation warnings, generator errors).

If any baseline fails before stress: that's already a finding (severity: high, "happy path broken"). Continue with other demos; flag in report.

---

## Phase 3 — Black-box brutal usage round (parallel personas)

Spawn one sub-agent per persona × demo, **capped by the tier's `Max parallel agents` value across all currently in-flight phases**. If the cap is reached, queue further spawns; never exceed it. Each persona runs all stress vectors **applicable to the demo's features**, and **must explicitly cover the three cross-cutting concerns** (data leakage, memory leakage, performance degradation) for every vector.

**Personas:**

- **Extreme user** — pushes scale. 100 components/page, 10MB props, 5000 components in registry, deep nested Redux, infinite-scroll list rendered server-side.
- **Novice React dev** — writes plausibly-wrong code. Component returns `undefined`, throws on first render, uses `window` in SSR, inline `() => ({ })` props (new ref each render), `setState` in render, infinite `useEffect` loop, missing dependency array, stale closure, `Date.now()` / `Math.random()` in render output.
- **Distracted senior** — copy-paste from another framework. Mismatched gem/npm versions, Turbo enabled but `setOptions` missing, `defer: true` and `async: true` on the same page, `auto_load_bundle: true` with manual `javascript_pack_tag`, `prerender: true` on a `window`-dependent component, fragment_cache wrapping `react_component` with no per-user key.
- **Attacker** — XSS payloads in props (`'><script>...`, `javascript:` URLs, SVG with `onload`), prototype pollution attempts (`__proto__`, `constructor.prototype`), prompt-injection-style strings in railsContext, oversized JSON, NaN/Infinity/BigDecimal/Date/Symbol values, circular refs, malformed UTF-8, U+2028/U+2029 separators in props, `</script>` close tags.
- **Ops engineer (deep+ tier only)** — production-fault simulation. `RAILS_ENV=production` precompile then change `process.env.RAILS_ENV` after build, missing `RENDERER_URL`, partial precompile (kill webpack mid-build), `public/packs` purged mid-request, manifest digest mismatch, slug timeout simulation.
- **Malicious user (deep+ tier only)** — server bundle leak via accidental client import, secrets in error messages, replay_console XSS, RSC payload tampering, signed-asset URL replay.

**Per-vector procedure:**

1. **Pre-mutation snapshot.** With the demo in its baseline state (Phase 2 install, no vector applied), capture a fresh `<demo>-pre-vector-<NNN>.json` measurement: RSS, FD count, p50/p95/p99 latency at the tier's primary concurrency. This is the comparison anchor for _this_ vector specifically (avoids confounding the Phase 2 baseline with drift from earlier vectors run against the same demo).
2. Modify the demo (only files inside the demo dir) to introduce the failure.
3. Run the demo (`bin/dev` or `bin/rails s` + `bin/shakapacker-dev-server`). For Pro RSC, also start node renderer.
4. Hit it: curl, headless browser (`npx playwright` or `puppeteer` if available; otherwise raw HTTP), parallel requests via `oha` (preferred) or `ab` for concurrency vectors. **Fallback when neither is installed:** use a curl-loop helper (see below) and **explicitly note in the finding card and the run report** that measurements came from the curl-loop fallback so cross-tool comparisons are not made silently.
5. **Run the cross-cutting battery** for the vector:
   - **Data leakage:** issue 2 requests with different fake user IDs / locales / canaries; diff HTML, JSON props, RSC payload, cached fragments, logs. Grep all responses + the client bundle for any canary string from the _other_ user's context. Log "no leak" or finding.
   - **Memory leakage:** loop the request N times (per tier); record RSS, FD count, renderer worker `process.memoryUsage()` at sampling intervals; compute slope. Slope above threshold → finding.
   - **Performance degradation:** drive concurrent load at the tier's concurrency levels via the chosen tool; record p50/p95/p99/throughput; compare against the **pre-mutation snapshot from step 1** (primary) and the Phase 2 baseline (secondary). Regression beyond threshold → finding.
6. **Post-vector teardown.** Revert the demo to baseline (e.g., `git -C "$DEMO_DIR" reset --hard` if the demo is its own git repo, or restore from a Phase 2 snapshot tarball in `$WORKSPACE_ROOT/payloads/`) before the next vector runs against the same demo.
7. Capture: HTTP status, response body, server logs, browser console, hydration warnings, memory growth (RSS/FD via the cross-platform helpers), latency table.
8. Classify: **broke loud** / **broke quiet** / **survived** / **degraded** / **leaked-data** / **leaked-memory**.
9. For each non-survived outcome, write a finding card to `$WORKSPACE_ROOT/reports/findings/<NNN>-<slug>.md` (schema below) and reference both the pre-mutation snapshot and the Phase 2 baseline in `metrics_refs`.

**Load-test helpers (use one, in this priority):**

```bash
# 1. oha (preferred): JSON output, accurate percentiles
oha --no-tui -j -n "$N" -c "$C" "$URL" > "$WORKSPACE_ROOT/metrics/<demo>-<vector>.oha.json"

# 2. ab: ApacheBench fallback
ab -n "$N" -c "$C" "$URL" > "$WORKSPACE_ROOT/metrics/<demo>-<vector>.ab.txt"

# 3. curl loop fallback (no oha, no ab): coarse but comparable within a single run
{
  for i in $(seq 1 "$N"); do
    curl -s -o /dev/null -w "%{time_total}\n" "$URL"
  done
} | sort -n > "$WORKSPACE_ROOT/metrics/<demo>-<vector>.curl-loop.txt"
# Compute p50/p95/p99 from the sorted file with awk; flag the finding card with
# `tool: curl-loop` in metrics_refs so cross-tool comparisons are not silently made.
```

**Vector library (filtered by effective feature set):**

- _(props/rails-context)_ Component name case mismatch, props with functions/Symbol/Date/BigDecimal/100MB/circular/nested 10k-key.
- _(registration)_ Same component rendered N times without `random_dom_id`. `registerStore` after first mount. Double-register via two pack imports.
- _(helpers)_ Render function returns `undefined`, `null`, `{}`, `{ renderedHtml: 123 }`, a Promise, a Promise that rejects, a Promise that never resolves.
- _(replay-console)_ `console.log("</script><script>alert(1)</script>")` server-si

…(truncated)
