diff --git a/.claude/skills/prerelease-test/SKILL.md b/.claude/skills/prerelease-test/SKILL.md index ebe2af93e20..93d35c6a2e8 100644 --- a/.claude/skills/prerelease-test/SKILL.md +++ b/.claude/skills/prerelease-test/SKILL.md @@ -23,17 +23,42 @@ Its value comes from three habits, not from breadth of assertions: reclassifies a meaningful share of findings — and it is what makes the report trustworthy enough for maintainers to act on directly. +## Where the work lives + +Campaigns run on the long-lived branch **`testing/prerelease`** (`main` + a `prerelease-testing/` +directory, never merged back). It is this skill's working memory between releases: + +- `prerelease-testing/REGISTRY.md` — every finding of every past pass with its current status and a + "re-verify on every pass" shortlist. Read it before scoping; re-verified items are half the pass. +- `prerelease-testing/fixtures//` — reusable apps, drivers and probes with their expected + results on the latest release. Copy a fixture to scratch and run it there; extend it rather than + rebuilding from scratch. +- `prerelease-testing/runs/-v/` — the current pass (below). At close-out, move its + top-level docs into `history/`, fold improved fixtures into `fixtures/`, update `REGISTRY.md`, and + delete the rest. + +At the start of a pass: `git fetch origin && git rebase origin/main` on the branch, then +`git push --force-with-lease`. Nothing outside `prerelease-testing/` changes there, so it never +conflicts. If the branch does not exist yet, create it from `main` with the same layout. + ## Deliverables -Produce these under `prerelease-testing/-/` in the repo and commit them to a work -branch as you go (never to `main`): +Produce these under `prerelease-testing/runs/-v/` and commit them to +`testing/prerelease` as you go (never to `main`): -- `FINDINGS.md` — executive summary, numbered findings with repro + evidence + regression status, - refuted claims, and per-cluster summaries. Templates: `references/reporting.md`. +- `CAMPAIGN_STATE.md` — what is under test, the change table with risks, the re-verify list, items + and port ranges, environment. Agents read it first. +- `AGENT_BRIEF.md` — `references/agent-brief.md` with every placeholder filled. +- `FINDINGS.md` — executive summary, re-verification table, numbered findings with repro + evidence + + regression status, refuted claims, and per-cluster summaries. Templates: `references/reporting.md`. - `RELEASE_PLAN.md` — triage into fix-before-release vs file-as-issue. Rubric below. -- One directory per test cluster containing the sample app sources (no `.web/`, `node_modules/`, - venvs), the driver scripts, `NOTES.md` with exact rerun commands, logs and screenshots. -- `README.md` — what each cluster covers and how to reuse it for the next release. +- One directory per test cluster (and per verifier) containing the sample app sources (no `.web/`, + `node_modules/`, venvs), the driver scripts, `NOTES.md` with exact rerun commands, and only the + trimmed logs and screenshots that are evidence for a finding. + +Keep bulk out of git: about 2 MB per cluster and 10 MB per pass. Full logs, frame dumps and +screenshots stay in the scratch dir. (A past archive reached 320 MB in six weeks by committing every +run output, which made it useless as a reference.) Findings must be reproducible by a stranger from `NOTES.md` alone. That is the bar: a fix agent gets handed the finding and nothing else. @@ -46,7 +71,11 @@ Work these in order. Phases 2–5 can overlap; 0 and 1 gate everything. Find the pre-release branch (`git ls-remote --heads origin 'r/pre-*'`, or the user names it), then read **every** `CHANGELOG.md` on it: the root one plus `packages/*/CHANGELOG.md`. The top entry of -each is what this train ships. +each is what this train ships. A release run can leave several `r/pre-*` branches with the same +date, one per materialization step; the one to test is the branch whose head carries the release +tag (`git tag --points-at origin/`). Use the tag as `` from here on, and list +`git log --oneline v..v` for the change table — the changelog links issue numbers, which +do not always match the PR that shipped. Run the discovery script to extract every package's version and confirm each is published: @@ -63,17 +92,44 @@ is what lets you test it as a user rather than as a checklist. ### Phase 1 — De-risk with a smoke test -Before any fan-out: one venv, `reflex init --template blank`, `reflex run`, then drive it in -Chromium with `.claude/skills/prerelease-test/scripts/drive_app.py`. This shakes out environment -problems (proxy, bun installs, browser path) once, in a context where you can debug them, instead -of inside ten parallel agents. +Build the campaign's venvs first, from a neutral directory (never from the checkout). This +example installs reflex-enterprise from PyPI; to use a user-supplied offline wheel instead, replace +the `ENT_SPEC=...` line with `ENT_WHEEL= \`: + + export SB= + cd "$SB" && NEW_VERSION= PREV_VERSION= CTRL_VERSIONS="" \ + ENT_SPEC='reflex-enterprise[mcp]==' \ + /.claude/skills/prerelease-test/scripts/bootstrap_envs.sh "$SB" --enterprise + +It builds `new`, `prev`, `ctrl-`, `driver`, `new-ent`, `prev-ent` under `$SB/envs/`, and prints +every reflex package each one resolved. A final `prev` resolves without pre-releases, and with +every reflex package it requires held to that release's upload time. Otherwise the previous +release's `>=` sibling floors resolve to the new train's alphas or finals, and the "baseline" +quietly runs half the code under test. Check the printed versions before trusting a baseline. + +Before any fan-out: `$SB/envs/new/bin/reflex init --template blank`, `$SB/envs/new/bin/reflex run`, +then drive it in Chromium with the driver venv +(`$SB/envs/driver/bin/python /.claude/skills/prerelease-test/scripts/drive_app.py`), in +dev and in prod. This shakes out environment problems (proxy, bun installs, browser path) once, +in a context where you can debug them, instead of inside ten parallel agents. A dev server's first +page load can take longer than the driver's settle time; pass an `expect_text` action on text the +page must show. The scripts carry PEP 723 metadata, so `uv run --script ` runs each one in its own isolated environment (the driver pulls in playwright that way); running them with an existing driver venv's interpreter works too. -Also confirm the dependency graph resolves the way the changelog says it should (e.g. optional -dependencies genuinely absent, sub-packages pinned as intended). +Then check that what PyPI serves is what the tag contains, and how it resolves: + + cd "$SB" && uv --no-config run --script /.claude/skills/prerelease-test/scripts/check_wheel_sources.py \ + --repo --base v --ref v --site-packages $SB/envs/new/lib/python3.*/site-packages + +It compares every source file changed between the two tags with the installed copy, and reports +files missing from the install, which is expected only for unpublished workspace packages. Also +read the published `reflex` wheel's `Requires-Dist`, and do a stock `pip install reflex==` +(no `--pre`). A sibling floor below the train's version (`reflex-components-core>=`) means +a user who upgrades only `reflex` keeps the old sibling and none of its fixes. That is a +maintainer decision for the final, so report it. ### Phase 2 — Feature exploration fan-out @@ -116,8 +172,15 @@ checkout) against the new reflex and run its demos — ag-grid, map, dnd, mantin plugin, OIDC. Baseline against the previous stable whenever something fails, so you can say whether the new release broke it. -Before running anything, grep the published enterprise wheel for names the changelog says moved or -were removed; that finds the breakage in seconds instead of hours. +Before running anything, grep the published enterprise wheel for names the changelog says moved, +were removed, or were deprecated (a deprecated API rxe still calls prints a warning its users cannot +fix). Grep it too for its own copies of logic the train changed. A security change in core, +such as client-IP resolution, header handling or credential redaction, is only as good as every +parallel surface: rxe's REST event API, MCP and rate limiters each resolve the same values their own +way. Drive the same handler over the websocket and over the REST API and compare what it sees. + +If the user supplies an enterprise build with fixes (an offline wheel), re-run the original repro +against it, with the previous rxe on the same reflex as the positive control. ### Phase 5 — Packaging audit @@ -138,6 +201,17 @@ counts line up with `pyi_hashes.json` (a package absent from the manifest must s Stub content hashes legitimately differ from the committed manifest because the build hook regenerates them at release time — compare *presence and counts*, and wheel against sdist. +The audit reads the published files; it does not build. A wheel built *from* an sdist runs the +stub hook again in an isolated build env. For an alpha train, that env resolves the hook's unpinned +`reflex-base` to the newest final, and modules that need the alpha's new symbols silently lose +their stubs. Build each changed component sdist once and count the `.pyi` files against the +published wheel. Use a throwaway uv cache, because uv reuses a cached sdist build for later +ordinary installs of the same version and would taint every venv built after it: + + UV_CACHE_DIR=$SB/uv-sdist-cache uv --no-config pip install --python \ + --prerelease=allow --no-binary == + find /lib/python3.*/site-packages/ -name '*.pyi' | wc -l + ### Phase 6 — Triage and report Write `FINDINGS.md` first (everything confirmed), then `RELEASE_PLAN.md` splitting findings into: @@ -171,8 +245,10 @@ resolved graph is all-final. findings. - **Concurrency.** Dev servers are heavy (bun + vite + granian). On a 4-CPU box run about two agents at a time and one dev server per agent; more just makes everything slow and flaky. -- **Commit as you go.** Each cluster's artifacts land on the work branch when that cluster - finishes, so a long campaign is never one unrecoverable batch. +- **Commit as you go.** Each cluster's artifacts land on `testing/prerelease` when that cluster + finishes, and a periodic commit of the run directory covers the agents still writing. A long + campaign outlives usage limits and container restarts, so it must never be one unrecoverable + batch. See "Surviving interruptions" in `references/orchestration.md`. - **Never fix framework code during the campaign.** Testing and fixing are separate engagements with separate review standards; mixing them costs you the independence that makes the findings credible. Record the repro and move on. (If the user then asks for fixes, that is a new task — diff --git a/.claude/skills/prerelease-test/references/agent-brief.md b/.claude/skills/prerelease-test/references/agent-brief.md index 4e95c07ef9d..b24bf877e35 100644 --- a/.claude/skills/prerelease-test/references/agent-brief.md +++ b/.claude/skills/prerelease-test/references/agent-brief.md @@ -1,7 +1,8 @@ # The agent brief -Copy this into the campaign scratchpad (e.g. `$SB/AGENT_BRIEF.md`), fill the `<...>` placeholders, -and tell every test agent to read it first. It carries the rules that keep results trustworthy and +Copy this into the run directory (`prerelease-testing/runs//AGENT_BRIEF.md` on +`testing/prerelease`), fill the `<...>` placeholders, and tell every test agent to read it first, +after `CAMPAIGN_STATE.md`. It carries the rules that keep results trustworthy and the environment traps that otherwise get rediscovered — expensively — by each agent in turn. Keep it in a file rather than pasting it into each prompt: agents re-read it when they get @@ -18,9 +19,11 @@ and hunt for anomalies. You REPORT issues; you never fix framework code. ### HARD RULES -1. **NEVER install reflex (or any workspace package) from the local checkout at ``.** No - `uv sync`, no `uv run`, no `pip install -e`, no `uv pip install .` anywhere under it. - Everything installs from PyPI. We are testing what users receive, not what the tree contains. +1. **NEVER install reflex (or any workspace package) from a local checkout** — ``, any + worktree of it, or a reflex-enterprise checkout. No `uv sync`, no `uv run`, no + `pip install -e`, no `uv pip install .` anywhere under one. Everything installs from PyPI (or a + user-supplied wheel file). We are testing what users receive, not what the tree contains. A + train version that is not installable from PyPI is a finding — do not work around it. 2. **Never run python with the checkout as your working directory.** `/reflex/` shadows the installed package, so `import reflex` silently picks up unreleased source and your results become fiction. Run scripts from a neutral directory and start each repro with an assertion @@ -32,26 +35,45 @@ and hunt for anomalies. You REPORT issues; you never fix framework code. ``` A guard that names some other venv fails on every run and gets deleted, which leaves you with no guard at all. -3. Reading the checkout is fine and encouraged — release source is on branch ``. +3. Reading the checkout is fine and encouraged — release source is the tag `v` on branch + `` (`git -C show v:`; fetch tags first). For PR context load the GitHub MCP tools via ToolSearch (`select:mcp__github__pull_request_read`). 4. Do NOT run any `git` write commands (add/commit/checkout/...) in the checkout. The orchestrator commits artifacts. 5. Do NOT fix bugs you find — record precise repro steps instead. -6. Kill every server and browser you start before you finish (track PIDs; verify with `ps`). Other - agents share this machine — run at most ONE dev server at a time unless your cluster needs two. +6. Kill every server and browser you start before you finish. Other agents share this machine — + run at most ONE app server set at a time unless your cluster needs two. Start servers in their + own process group (`setsid reflex run ... &`) and stop the group (`kill -- -`, then + `kill -9` after a grace period). Never `pkill -f `: the pattern also matches the shell + running the command, which kills your own tool call. +7. Never bind ports outside your assigned range, and never 3000/8000. Before starting a server, + check the port is free (`curl --noproxy '*' localhost:` must fail). A leftover server from + an earlier step answers your next test, and the result is fiction. ### Environment - Scratchpad root: `SB=` -- **Prebuilt shared venv (READ-ONLY — never install into it):** `$SB/envs/` has - `reflex==` and the alpha sub-packages. Use `$SB/envs//bin/reflex` directly. -- Need other deps or versions? Make your OWN venv: +- **Prebuilt shared venvs (READ-ONLY — never install into them)**, built by + `/.claude/skills/prerelease-test/scripts/bootstrap_envs.sh`: `$SB/envs/new` + (`reflex[db]==`, the version under test), `$SB/envs/prev` (``, the + baseline), `$SB/envs/ctrl-` (positive controls: versions a finding you re-verify reproduced + on — run the control FIRST; a re-verification without a reproducing control proves nothing), `$SB/envs/new-ent` / `prev-ent` (+ reflex-enterprise), + `$SB/envs/driver`. +- Need other deps or versions? Make your OWN venv `$SB/envs/-`, always with cwd=`$SB` + and `--no-config` (the checkout's pyproject has an `exclude-newer` window that hides fresh + packages): ``` - uv venv $SB/envs/ --python 3.11 - uv pip install --python $SB/envs//bin/python --prerelease=allow 'reflex==' + cd $SB && uv --no-config venv $SB/envs/- --python 3.12 + cd $SB && uv --no-config pip install --python $SB/envs/-/bin/python \ + --prerelease=allow --only-binary :all: 'reflex[db]==' 'pydantic' ``` - Always pass `--prerelease=allow` for alphas; PyPI is the default index — never point it at the - checkout. Python 3.12/3.13/3.14 are available via `uv venv --python 3.14` etc. + `--prerelease=allow` lets transitive deps resolve to pre-releases too, so pin those for a + user-like graph. A baseline venv for a FINAL release must not use it, or the release's `>=` + sibling floors resolve to the alphas under test; pin every sibling, or omit the flag. + `--only-binary :all:` matters: uv reuses a cached sdist build for later installs of the same + version, so one `--no-binary` experiment anywhere on the machine taints every venv built after it. + Run sdist experiments with their own `UV_CACHE_DIR`. Python 3.11–3.15 are available via + `uv venv --python 3.1x`. - **Playwright driver venv:** `$SB/envs/driver/bin/python` (playwright, httpx, websockets). Chromium: `/opt/pw-browsers/chromium` — launch with `p.chromium.launch(executable_path="/opt/pw-browsers/chromium")`. @@ -62,10 +84,21 @@ and hunt for anomalies. You REPORT issues; you never fix framework code. Do NOT export those variables into the reflex server's environment — it breaks bun's package installs through the proxy, which looks like a framework bug and is not. - Set `REFLEX_TELEMETRY_ENABLED=false` for every reflex command. -- Node and bun are preinstalled. Enterprise apps need `CI=true` to bypass the dev login gate. +- Node and bun are preinstalled. Enterprise apps from the PyPI wheel need `CI=true` to bypass the + dev login gate, and refuse `reflex run --env prod` / `reflex export` for a logged-out user (a + user-supplied offline wheel does not; the `testing/prerelease` fixtures have a local account stub). - App working dirs: `$SB/apps//...`. Run servers on YOUR ASSIGNED PORTS: - `reflex run --frontend-port --backend-port `. The first run does a bun install (1–2 min); - poll the frontend URL until it returns 200 for up to ~6 minutes before concluding failure. + `reflex run --frontend-port --backend-port `. Prod (`--env prod`) serves both from ONE + port: `--frontend-port

--backend-port

` with `REFLEX_API_URL=http://localhost:

`. The + first run does a bun install (1–2 min); poll the frontend URL until it returns 200 for up to ~6 + minutes before concluding failure. A dev server's first page render can lag the 200: wait for + text the page must show before reading it. +- Redis: start your own on the last backend port of YOUR range, which no app server then uses + (`redis-server --port --save '' --appendonly no &`), and point the app at it with + `REFLEX_REDIS_URL`. Prod with Redis forks several granian workers (capped at 8 by default; + `GRANIAN_WORKERS` overrides). +- Chromium does not apply Playwright `extra_http_headers` to the websocket upgrade: inject headers + (`X-Forwarded-For`, cookies) through a small forwarding proxy, or a raw websocket client. - `--loglevel debug` gives verbose server logs; redirect to a file and actually read it. ### What "testing" means here @@ -91,6 +124,11 @@ and hunt for anomalies. You REPORT issues; you never fix framework code. falling back (this environment blocks that mirror). Do report it if the fallback itself fails. - Transient bun "incorrect peer dependency" warnings during a one-time upgrade migration, provided the final lockfile is consistent. +- A `/favicon.ico` 404 on apps without a favicon; AG Grid / AG Charts unlicensed-trial banners; the + uv `UV_NATIVE_TLS` deprecation warning; the "Compiling 100% N+1/N" counter overshoot; granian's + "more workers than CPU cores" warning. +- Everything already filed or open in `prerelease-testing/REGISTRY.md` on `testing/prerelease` — + report those only if they CHANGED. If you see something surprising that is not on this list, investigate it — several real findings have surfaced first as an unexplained warning. @@ -99,15 +137,36 @@ have surfaced first as an unexplained warning. 1. Copy reusable artifacts into the repo (plain `cp`/`rsync`, no git): `DEST=//` - - the app source dirs, EXCLUDING `.web/`, `node_modules/`, `.states/`, `assets/external/`, - `*.db`, venvs + - the app source dirs, EXCLUDING `.web/`, `node_modules/`, `.states/`, `reflex.lock/`, + `assets/external/`, `*.db`, venvs, `__pycache__` - your Playwright/repro scripts - - `NOTES.md`: what you tested, how to rerun it (exact commands), what you observed, including - benign quirks -2. Return structured findings: every discrete check as a test entry (pass/fail/anomaly/skipped) - with enough repro detail that another agent can reproduce a failure from `NOTES.md` and your - scripts alone, without your conversation. An "anomaly" is anything surprising (console error, - warning, traceback, visual glitch, perf cliff) even when functionality works. + - `NOTES.md`, written incrementally (the orchestrator commits DEST periodically, so a session + that dies mid-cluster still leaves a record): what you tested, how to rerun it (exact + commands), what you observed per check, including benign quirks and baseline results + - only the trimmed logs and screenshots that are evidence for a finding; keep DEST under ~2 MB. + Full logs stay under `$SB/apps//`. +2. Your FINAL MESSAGE is this structured report (the orchestrator parses it). When a Workflow + gives you a result schema, return the schema's fields instead; this text form is for plain + agent calls: + ``` + CLUSTER: + SUMMARY: <2-4 sentences> + ARTIFACTS: + TESTS: + - [pass|fail|anomaly|skipped] :

+ REVERIFIED: + - : fixed|still-broken|changed — + ISSUES: + - TITLE: + SEVERITY: critical|high|medium|low + REGRESSION: yes|no|unknown (yes = the previous release behaves correctly) + REPRO: + EVIDENCE: + NOT_COVERED: + ``` + Every ISSUE must be reproducible by a stranger from `NOTES.md` and your scripts alone, without + your conversation. An "anomaly" is anything surprising (console error, warning, traceback, + visual glitch, perf cliff) even when functionality works. ### Timeboxing diff --git a/.claude/skills/prerelease-test/references/clusters.md b/.claude/skills/prerelease-test/references/clusters.md index 5c5d86aba5f..5739349a500 100644 --- a/.claude/skills/prerelease-test/references/clusters.md +++ b/.claude/skills/prerelease-test/references/clusters.md @@ -38,6 +38,10 @@ unassigned, and as a source of interaction ideas. | Registration / multi-app | Two apps in one process; `AppHarness` twice | Sequential and simultaneous; check for registration leakage between them | | Optional dependencies | Bare install, each extra, and the upgrade path | Import the optional package's feature without the extra and judge the error message quality | | DevTools / perf | App with nested substates, custom components, client-only wrappers | Walk React fibers for `displayName`; record navigation main-thread cost; verify perf claims rather than trusting them | +| Frontend state store | Every mutation kind (append/sort/reverse/delete key/type change/nested model) with the DOM compared to the backend state after each step | A third-party component that mutates the objects it is given (AG Grid cell edits) next to a *memoized* consumer of the same var (`rx.memo`, a chart, a second grid); a handler that stores the edited value as is vs one that normalizes it; recharts on the same data (it deep-freezes what it receives) | +| Process model | Prod with Redis and several workers, dev with hot reload | Import-time side effects under a forking supervisor (threads, logging `QueueListener`, schedulers, DB engines, module-level random values); every start method; SIGTERM/SIGINT/kill -9 of one worker; a broken-then-fixed module in dev | +| Forms | The same legacy forms (dict handlers, id-only, `id == name`, checkbox / switch / groups, sliders, selects) run on old and new | Diff exactly what the handler receives between versions — every changed key or value needs a changelog line; empty selections in every handler kind (dict, TypedDict, form-data object) | +| Network / security headers | A probe app recording `router.session` and headers, plus a forwarding proxy and a raw websocket client | Spoofed / repeated / non-UTF-8 headers; trusted-proxy depth 0, 1, 2; every surface that reads the value — websocket, uploads, API routes, enterprise REST / MCP — must agree | ## Downstream and packaging clusters @@ -49,4 +53,9 @@ Two clusters that are not feature-shaped but catch the highest-severity problems names. A bare `AttributeError`/`ImportError` with no pointer to the replacement is a finding even when the removal itself was intended. - **Packaging.** `.claude/skills/prerelease-test/scripts/audit_pyi.py`, plus a check that each package's declared dependency pins - match what the changelog says and that installing from the sdist works. + match what the changelog says and that installing from the sdist works — and that a wheel *built* + from each changed sdist keeps every stub (SKILL.md, Phase 5). Read the `reflex` wheel's sibling + floors: a stock `pip install -U reflex` that keeps old siblings does not deliver their fixes. +- **Hosting CLI.** Never against the real Reflex Cloud: point the CLI at a local fake control + plane (derive the payload shapes from the CLI source and the PRs' tests) and record the requests + it receives. Compare deploy archives (file list, exclusions, compression) between versions. diff --git a/.claude/skills/prerelease-test/references/orchestration.md b/.claude/skills/prerelease-test/references/orchestration.md index da311d6812b..baa43e81416 100644 --- a/.claude/skills/prerelease-test/references/orchestration.md +++ b/.claude/skills/prerelease-test/references/orchestration.md @@ -128,9 +128,9 @@ produce confusing, unreproducible results. Every cluster prompt should open with the same preamble, then the cluster-specific brief: ``` -FIRST read /AGENT_BRIEF.md and follow every rule in it (isolated PyPI-only installs — never -install from the checkout; end-to-end browser testing; artifact + NOTES.md deliverables; kill your -processes). +FIRST read /CAMPAIGN_STATE.md, then /AGENT_BRIEF.md, and follow every rule in +it (isolated PyPI-only installs — never install from the checkout; end-to-end browser testing; +artifact + NOTES.md deliverables; kill your processes). Your cluster key: "". Working dir: /apps//. Artifact destination: //. Your RESERVED ports: frontend -, backend - — always pass them explicitly, and never bind outside them. @@ -146,9 +146,32 @@ verifier's range, which is the collision the map exists to prevent. ## Running it - Two agents at a time is right for a 4-CPU box; the workflow's own concurrency cap handles the - rest of the queue. + rest of the queue. A third agent fits when one of them runs no app server (a CLI or packaging + cluster, a verifier working through Python-level probes). - Between phases, read the results before launching the next one. - Schedule a check-in (`send_later`, ~45 min) during long runs so a wedged agent or a leaked dev server gets noticed. Check `ps` for stray `reflex run` processes and commit finished clusters. - When the workflow completes, its full structured result is in the task output file; parse it for the report rather than re-reading transcripts. +- Record each agent's report as it arrives (a short `reports/.md` in the run directory) + and commit it. `FINDINGS.md` is then assembled from files rather than from conversation context, + which may have been compacted by the time the last agent reports. + +## Surviving interruptions + +A full campaign runs for hours, and usage limits, spend limits and container restarts all happen +mid-run. Plan for them: + +- **Commit continuously.** Agents cannot run git, so run a background loop that commits and pushes + the run directory every ~10 minutes, skipping any file over 2 MB (`git reset -q -- `). + Start it with `setsid nohup &` so it outlives the tool call, and stop it with a sentinel + file at the end. A restarted container loses the loop: start it again. +- **Resume, don't respawn.** An agent stopped by a limit keeps its transcript. When the limit + resets, message it (`SendMessage` to its id) to continue, and tell it that every server, Redis, + proxy and browser it had running is gone after a restart. A fresh agent would redo the work + without its notes. +- **Scratch survives, processes do not.** `$SB` (venvs, apps, logs) normally survives a worker + restart; check `ps` and the ports before resuming anything. +- **When the configured model is unavailable** (a spend limit with no reset in sight), finish the + remaining verifications yourself rather than shipping unverified findings. Say so in + `FINDINGS.md` and to the user, since it departs from the independence the method relies on. diff --git a/.claude/skills/prerelease-test/references/reporting.md b/.claude/skills/prerelease-test/references/reporting.md index 242eae436f7..2c15894ba87 100644 --- a/.claude/skills/prerelease-test/references/reporting.md +++ b/.claude/skills/prerelease-test/references/reporting.md @@ -25,9 +25,16 @@ Index: - FINDING-001: (SEVERITY) - ... +## Re-verification table +| registry id | what | positive control (version, result) | new result | cluster | +|---|---|---|---|---| +<Every REGISTRY.md item re-run this pass: fixed / still broken / changed, with the control that +proves the check can fail.> + ## FINDING-00N: <title> (SEVERITY) -- Cluster: `<cluster>` | Regression vs <prev stable>: yes/no | Verifier: CONFIRMED +- Cluster: `<cluster>` | Regression vs <prev stable>: yes/no | Verifier: CONFIRMED / NARROWED / + RECLASSIFIED (say what changed: severity, scope, regression status) - Repro: <exact commands/steps, self-contained> - Evidence: <log excerpt, console error, screenshot path> - Root cause (verifier analysis): <file:line and mechanism, when known> diff --git a/.claude/skills/prerelease-test/scripts/bootstrap_envs.sh b/.claude/skills/prerelease-test/scripts/bootstrap_envs.sh new file mode 100755 index 00000000000..97ba0d6ff81 --- /dev/null +++ b/.claude/skills/prerelease-test/scripts/bootstrap_envs.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Build the isolated, PyPI-only virtualenvs a pre-release pass uses, as $SB/envs/<name>. +# +# usage: NEW_VERSION=0.10.1a1 PREV_VERSION=0.10.0 bootstrap_envs.sh <scratch-dir> [--enterprise] +# then: export SB=<scratch-dir> +# +# Venvs (names the agent brief refers to): +# new reflex[db]==$NEW_VERSION the version under test +# prev reflex[db]==$PREV_VERSION the previous release (before/after baseline) +# ctrl-X reflex[db]==X for each X in $CTRL_VERSIONS positive controls (versions a finding reproduced on) +# driver playwright, httpx, websockets, pip browser drivers and HTTP probes +# new-ent / prev-ent (with --enterprise) + reflex-enterprise and oidc-provider-mock +# +# Enterprise: set ENT_WHEEL to a user-supplied offline wheel (bypasses the login gate; never commit it), or +# ENT_SPEC to a PyPI requirement such as 'reflex-enterprise[mcp]==0.9.8' (run enterprise apps with CI=true). +# +# Resolution: a version under test that is a pre-release resolves with --prerelease=allow. A final one (prev, a +# final control) resolves without it, and with every reflex package it requires held to that reflex release's +# PyPI upload time (--exclude-newer-package; reflex uploads last in its train). Otherwise the previous release's +# `>=` sibling floors pick up the new train's alphas or finals, and the baseline silently runs code under test. +# Everything else resolves as a user's install would today. PYDANTIC_PIN (default +# 'pydantic<2.15') keeps the transitive graph user-like. Packages install from wheels only (ONLY_BINARY, default +# ':all:'): uv reuses a cached sdist build for later installs of the same version, so one --no-binary experiment +# would otherwise leak into every venv built after it. Do sdist experiments with their own UV_CACHE_DIR. +# +# Never run uv with a reflex checkout as the working directory: its pyproject's exclude-newer window hides fresh +# packages, and the checkout would shadow the installed package. A venv whose install completed is reused; delete +# it to rebuild. Index variables are cleared, so everything resolves from PyPI. +set -euo pipefail +SB="${1:?usage: NEW_VERSION=... PREV_VERSION=... bootstrap_envs.sh <scratch-dir> [--enterprise]}"; ENT="${2:-}" +unset UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL UV_FIND_LINKS +[ -n "${ENT_WHEEL:-}" ] && ENT_WHEEL="$(realpath -m "$ENT_WHEEL")" +: "${NEW_VERSION:?set NEW_VERSION, e.g. 0.10.1a1}" +: "${PREV_VERSION:?set PREV_VERSION, e.g. 0.10.0}" +PY="${PYTHON_VERSION:-3.12}" +PYDANTIC_PIN="${PYDANTIC_PIN:-pydantic<2.15}" +ONLY_BINARY="${ONLY_BINARY-:all:}" +mkdir -p "$SB/envs" "$SB/apps" "$SB/logs" "$SB/downloads"; SB="$(cd "$SB" && pwd -P)"; cd "$SB" + +is_pre() { [[ "$1" =~ (a|b|rc|\.dev)[0-9]+ ]]; } + +held_siblings() { # reflex version, python: an --exclude-newer-package flag per reflex package it requires + curl -fsS "https://pypi.org/pypi/reflex/$1/json" | "$2" -I -c 'import json, re, sys +d = json.load(sys.stdin) +cut = max(u["upload_time_iso_8601"] for u in d["urls"]) +reqs = [re.match(r"[A-Za-z0-9._-]+", r).group().lower() for r in d["info"]["requires_dist"] or []] +print(" ".join(f"--exclude-newer-package={n}={cut}" for n in sorted({"reflex", *reqs}) if n.startswith("reflex")))' +} + +mk() { # name, version-that-decides-resolution, pip args... + local name="$1" ver="$2" dir="$SB/envs/$1" held; shift 2 + [ -f "$dir/.bootstrap-ok" ] && return 0 + local opts=() + [ -n "$ONLY_BINARY" ] && opts+=(--only-binary "$ONLY_BINARY") + uv --no-config venv -q --clear --python "$PY" "$dir" + if is_pre "$ver"; then + opts+=(--prerelease=allow) + elif [ "$ver" != 0 ]; then + held="$(held_siblings "$ver" "$dir/bin/python")" + read -ra held_opts <<< "$held" + opts+=("${held_opts[@]}") + fi + uv --no-config pip install -q --python "$dir/bin/python" "${opts[@]}" "$@" + touch "$dir/.bootstrap-ok" +} +# greenlet must arrive through the db extra: never add it by hand to new/prev. +# The published reflex wheel pins reflex-base to its own version, so reflex[db]==X brings the matching reflex-base. +mk new "$NEW_VERSION" "reflex[db]==$NEW_VERSION" "$PYDANTIC_PIN" +# PREV_EXTRAS adds packages to prev only (e.g. greenlet for a 0.9.x prev, whose db extra predates it). +# shellcheck disable=SC2086 +mk prev "$PREV_VERSION" "reflex[db]==$PREV_VERSION" "$PYDANTIC_PIN" ${PREV_EXTRAS:-} +for v in ${CTRL_VERSIONS:-}; do + # 0.9.x controls need greenlet by hand (SQLAlchemy 2.1 dropped it from its default install). + mk "ctrl-$v" "$v" "reflex[db]==$v" "$PYDANTIC_PIN" greenlet +done +mk driver 0 playwright httpx websockets pip +[ -x /opt/pw-browsers/chromium ] || "$SB/envs/driver/bin/python" -m playwright install chromium >/dev/null 2>&1 \ + || echo "playwright chromium install failed; set the Chromium path manually" + +if [ "$ENT" = "--enterprise" ]; then + ENT_REQ="${ENT_WHEEL:-${ENT_SPEC:-}}" + [ -n "$ENT_REQ" ] || { echo "--enterprise needs ENT_WHEEL=<offline wheel path> or ENT_SPEC=<requirement>"; exit 3; } + case "$ENT_REQ" in *.whl) [ -f "$ENT_REQ" ] || { echo "enterprise wheel missing: $ENT_REQ"; exit 3; }; ENT_REQ="$ENT_REQ[mcp]";; esac + mk new-ent "$NEW_VERSION" "reflex[db]==$NEW_VERSION" "$PYDANTIC_PIN" "$ENT_REQ" oidc-provider-mock + # shellcheck disable=SC2086 + mk prev-ent "$PREV_VERSION" "reflex[db]==$PREV_VERSION" "$PYDANTIC_PIN" ${PREV_EXTRAS:-} "$ENT_REQ" oidc-provider-mock +fi + +# Print every reflex package per venv: a baseline that resolved a sibling to the version under test shows here. +for e in new prev $(for v in ${CTRL_VERSIONS:-}; do echo "ctrl-$v"; done) new-ent prev-ent; do + [ -x "$SB/envs/$e/bin/python" ] || continue + echo "== $e: $(cd "$SB" && "$SB/envs/$e/bin/python" -I -c 'import importlib.metadata as m +names = sorted({d.metadata["Name"].lower() for d in m.distributions()}) +keep = [n for n in names if n.startswith("reflex")] + ["sqlalchemy", "greenlet", "pydantic"] +print(" ".join(f"{n}={m.version(n)}" for n in keep if n in names))')" +done +printf 'export SB=%q\n' "$SB" diff --git a/.claude/skills/prerelease-test/scripts/check_wheel_sources.py b/.claude/skills/prerelease-test/scripts/check_wheel_sources.py new file mode 100644 index 00000000000..be20a5c82f6 --- /dev/null +++ b/.claude/skills/prerelease-test/scripts/check_wheel_sources.py @@ -0,0 +1,193 @@ +"""Check that the installed release packages carry exactly the source a release tag contains. + +Lists every Python package source file that changed between two git refs (``reflex/`` and +``packages/<pkg>/src/<module>/``), maps each to its location in a venv's site-packages, +and compares the bytes. A file that differs, or one deleted at the tag that is still +installed, means PyPI serves something other than the tag: a mis-built or mis-tagged +release. A changed file of a package that is not installed in the venv is reported +separately — expected for unpublished workspace packages and for ones outside the +install graph, a finding when the package should be there. + +Usage (PEP 723 script, no dependencies; run it from any directory): + uv run --script check_wheel_sources.py --repo <checkout> --base v0.10.0 --ref v0.10.1a1 \ + --site-packages $SB/envs/new/lib/python3.12/site-packages +""" + +# /// script +# requires-python = ">=3.11" +# /// + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from pathlib import Path + +PROJECT_NAME = re.compile(r"^name\s*=\s*[\"']([^\"']+)[\"']", re.MULTILINE) +PACKAGE_SOURCE = re.compile(r"packages/([^/]+)/src/(.+)") + + +def git(repo: str, *args: str) -> subprocess.CompletedProcess[bytes]: + """Run a git command in the checkout. + + Args: + repo: Path to the reflex checkout. + *args: The git arguments. + + Returns: + The completed process, with stdout and stderr as bytes. + """ + return subprocess.run(["git", "-C", repo, *args], capture_output=True, check=False) + + +def installed_dists(site: Path) -> set[str]: + """Collect the normalized distribution names installed in a site-packages directory. + + Args: + site: The site-packages directory. + + Returns: + Normalized names (lowercase, ``-``/``_``/``.`` folded to ``-``). + """ + return { + re.sub( + r"[-_.]+", "-", d.name.removesuffix(".dist-info").rsplit("-", 1)[0] + ).lower() + for d in site.glob("*.dist-info") + } + + +def dist_name(repo: str, ref: str, package_dir: str) -> str: + """Read the distribution name a workspace package declares. + + Args: + repo: Path to the reflex checkout. + ref: The git ref to read ``pyproject.toml`` at. + package_dir: The directory name under ``packages/``. + + Returns: + The normalized distribution name, falling back to the directory name. + """ + shown = git(repo, "show", f"{ref}:packages/{package_dir}/pyproject.toml") + match = ( + PROJECT_NAME.search(shown.stdout.decode()) if shown.returncode == 0 else None + ) + return re.sub(r"[-_.]+", "-", match.group(1) if match else package_dir).lower() + + +def classify( + repo: str, ref: str, path: str, site: Path, names: dict[str, str] +) -> tuple[str, str] | None: + """Compare one changed file of the checkout with its installed copy. + + Args: + repo: Path to the reflex checkout. + ref: The tag under test. + path: The changed file, relative to the checkout. + site: The site-packages directory. + names: Cache of distribution names by ``packages/`` directory, filled as needed. + + Returns: + The distribution and one of ``same``, ``differs``, ``stale`` (deleted at the tag, + still installed) or ``deleted``; ``None`` for a file no distribution ships. + """ + if not path.endswith((".py", ".pyi", ".js", ".jsx", ".json", ".css")): + return None + if path.startswith("reflex/"): + target, dist = site / path, "reflex" + elif match := PACKAGE_SOURCE.fullmatch(path): + package_dir, rel = match.groups() + if package_dir not in names: + names[package_dir] = dist_name(repo, ref, package_dir) + target, dist = site / rel, names[package_dir] + else: + return None + at_ref = git(repo, "show", f"{ref}:{path}") + if at_ref.returncode != 0: + return dist, "stale" if target.exists() else "deleted" + if not target.exists() or target.read_bytes() != at_ref.stdout: + return dist, "differs" + return dist, "same" + + +def report(results: dict[str, list[str]], uninstalled: dict[str, list[str]]) -> int: + """Print the files that do not match the tag and the overall verdict. + + Args: + results: Changed files of installed distributions, by status. + uninstalled: Changed files of distributions missing from the venv, by name. + + Returns: + ``1`` when a file differs or a deleted file is still installed, otherwise ``0``. + """ + for path in results["differs"]: + print(f" DIFFERS OR MISSING {path}") + for path in results["stale"]: + print(f" DELETED AT TAG, STILL INSTALLED {path}") + for dist, paths in sorted(uninstalled.items()): + print(f" not installed: {dist} ({len(paths)} changed files)") + if results["differs"] or results["stale"]: + print("FAIL: the installed packages do not match the tag") + return 1 + print("PASS") + return 0 + + +def main() -> int: + """Compare the changed source files and print a summary. + + Returns: + ``1`` when an installed file differs from the tag or a deleted file is still + installed, ``2`` when the refs or the site-packages directory cannot be read or + ``reflex`` is not installed there, otherwise ``0``. + """ + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", default=".", help="path to the reflex checkout") + parser.add_argument("--base", required=True, help="previous release tag") + parser.add_argument("--ref", required=True, help="tag under test") + parser.add_argument( + "--site-packages", required=True, type=Path, help="site-packages of the venv" + ) + args = parser.parse_args() + site: Path = args.site_packages + dists = installed_dists(site) if site.is_dir() else set() + if "reflex" not in dists: + print(f"reflex is not installed in {site}", file=sys.stderr) + return 2 + # Without renames, a moved file is a deletion plus an addition, so a stale copy at + # the old path is still caught. + diff = git( + args.repo, + "diff", + "--no-renames", + "--name-only", + f"{args.base}..{args.ref}", + "--", + ) + if diff.returncode != 0: + print(diff.stderr.decode().strip(), file=sys.stderr) + return 2 + + names: dict[str, str] = {} + results: dict[str, list[str]] = {"same": [], "differs": [], "stale": []} + uninstalled: dict[str, list[str]] = {} + for path in diff.stdout.decode().splitlines(): + if (found := classify(args.repo, args.ref, path, site, names)) is None: + continue + dist, status = found + if dist not in dists: + uninstalled.setdefault(dist, []).append(path) + elif status in results: + results[status].append(path) + + print( + f"{args.base}..{args.ref}: {len(results['same'])} changed source files " + f"identical in {site}" + ) + return report(results, uninstalled) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/skills/prerelease-test/scripts/drive_app.py b/.claude/skills/prerelease-test/scripts/drive_app.py index ce4a56963b3..5c0869c0620 100644 --- a/.claude/skills/prerelease-test/scripts/drive_app.py +++ b/.claude/skills/prerelease-test/scripts/drive_app.py @@ -208,7 +208,10 @@ def main() -> int: read_error: str | None = None with sync_playwright() as p: - browser = p.chromium.launch(executable_path=CHROMIUM, headless=not args.headed) + browser = p.chromium.launch( + executable_path=CHROMIUM if Path(CHROMIUM).exists() else None, + headless=not args.headed, + ) page = browser.new_context().new_page() page.on("console", lambda m: console.append({"type": m.type, "text": m.text})) page.on("pageerror", lambda e: page_errors.append(str(e))) @@ -278,8 +281,12 @@ def main() -> int: else [m for m in console if not is_benign(m["text"])] ) problems = [m for m in shown if m["type"] in ("error", "warning")] + # A page that rendered no text inspected nothing: a dev server's first load often + # outlasts --settle, and a crashed render leaves the body empty too. Neither is clean. + empty_page = not read_error and not load_error and not body.strip() clean = not ( problems + or empty_page or page_errors or failed or bad_status @@ -298,6 +305,7 @@ def main() -> int: "action_error": action_error, "screenshot_error": screenshot_error, "read_error": read_error, + "empty_page": empty_page, "console": shown, "page_errors": page_errors, "failed_requests": failed, @@ -325,6 +333,10 @@ def main() -> int: print(f" ACTION FAILED: {action_error}") if read_error: print(f" PAGE UNREADABLE: {read_error}") + if empty_page: + print( + " EMPTY PAGE: no text rendered (slow first dev load? add an expect_text action)" + ) if screenshot_error: print(f" SCREENSHOT FAILED: {screenshot_error}") for m in shown: