Public: anyone can read this; the crew can change it.
test/apps-ws.test.ts fails 5 of 8 in the full gate run, passes alone #10986
Check · Review · Land · Live done — Landed and live in production.
I merged `main` into this branch, kept the intent of both sides in the three conflicting files, and pushed. `bun scripts/gate.ts` and `bun x tsc --noEmit` are clean. The 116 tests I ran (113 passed, 3 skipped, 0 failed) cover `test/apps-ws.test.ts`, `test/check-list.test.ts`, `test/apps.test.ts`, `test/apps-adversarial.test.ts` and `test/hardening.test.ts`. The skipped ones are the unix-socket cas…
shipped production is serving it deploy da127e7
I merged main into this branch, kept the intent of both sides in the three conflicting files, and pushed. bun scripts/gate.ts and bun x tsc --noEmit are clean. The 116 tests I ran (113 passed, 3 skipped, 0 failed) cover test/apps-ws.test.ts, test/check-list.test.ts, test/apps.test.ts, test/apps-adversarial.test.ts and test/hardening.test.ts. The skipped ones are the unix-socket cases, which this sandbox can't run; the forge's check runs them on the push.
How each conflict was resolved:
scripts/gate.ts: Takesmain's new shape, where the gate is every test file minusEXCLUDEDand the separateGATElist is gone. It drops thetest/apps-ws.test.tsexclusionmainhad added, because this branch fixes that test, so it is gated again.src/apps/proxy.ts: Keeps this branch'sappWebSocketWith(max), which lets an app's config set its own frame limit. It addsmain's per-port socket hold, whichopentakes out andclosereleases.src/http/server.ts: Takesmain's reorganisedserve, withfetch: handleandserveInternal. The WebSocket handler is nowwithShells(appWebSocketWith(app.config.wsMaxMessage)), and the imports keep bothtype AppSocketandisWebSocketHandshake. A staleappWebSocketmention in a comment insrc/spawn/cell.tsnow saysappWebSocketWith.
Try: on the preview, open a WebSocket to an app and send a frame; it should echo back through the proxy.
🤖 Generated with Claude Code
test/apps-ws.test.ts is back in the gate. Its run time drops from about 30 s to about 4 s, and running the tests in random order no longer breaks it.
Why it failed in the gate. There were two causes:
- The frame-limit test was slow. It sent 16 MiB frames three times. Under load that ran past its 30 s timeout, and the tests after it failed too.
- The tests depended on their order. "An app that stops closes its sockets" leaves the app stopped. The gate shuffles test order, so any test that ran after it found no app running. With the frame-limit test fixed, this still failed 1–9 of 11 tests per seed.
What changed
src/apps/proxy.ts: the websocket handler is now made byappWebSocketWith(maxMessage = WS_MAX_MESSAGE). The same value sets Bun'smaxPayloadLengthandbackpressureLimitand the app-side 1009 check. The production limit is still 16 MiB.src/app.ts/src/http/server.ts: a new optional config field,wsMaxMessage, is passed toappWebSocketWith. Only the test sets it. The shell's own 64 KiB keystroke cap is separate and unchanged.test/apps-ws.test.ts: the forge in this test runs with a 2 MiB limit (LIMIT), and the frame-limit test checks against that. The stop test now redeploys production in afinallyand waits for that deploy to finish, so the next test finds the app up.scripts/gate.ts: the file'sEXCLUDEDentry is removed.src/spawn/cell.ts: a doc comment now namesappWebSocketWith.
What to look at. This branch already had an earlier, larger attempt (a rewrite of proxy.ts) from about 200 releases ago. I didn't build on it. I started from today's main and added a merge commit on top, so nothing was force-pushed. Compared with main, the change is 6 files, +41/−24.
Checks
bun scripts/flake-hunt.ts admit test/apps-ws.test.ts: 10/10 green, random order, under load (seeds 2081648570, 4153796764, 4118917717, 3274170533, 3876785141, 2488489320, 423412369, 3419342259, 2430598469, 907661019).bun scripts/gate.tsandbun x tsc --noEmitare clean.test/check-list.test.tsandtest/mcp-size.test.tspass. This change doesn't touch the MCP.- Because the change touches
src/app.ts,scripts/fast.tsnamed 2,215 test files, about 187 minutes. Instead I picked the tests covering the proxy and the shared websocket handler:apps-ws,apps-proxy-*,machine-shell,machine-shell-pageandcheck-list. All 85 pass. The forge's full check covers the rest. - In this sandbox the forge runs apps uncontained (
FORGE_TEST_SOCKET=port), so the forge's check on this push runs the contained path.
Left out
- There is no test that the live 16 MiB value itself is enforced. The test checks the limit at 2 MiB, and production uses the same code with 16 MiB passed in.
keep_planwasn't available to me in this run, so the issue has no plan list.
Try: bun scripts/flake-hunt.ts admit test/apps-ws.test.ts, or bun test --randomize test/apps-ws.test.ts.
🤖 Generated with Claude Code
test/apps-ws.test.ts is back in the gate: its entry is removed from EXCLUDED in scripts/gate.ts.
Why it failed under load. The frame-limit test pushed the real 16 MiB limit through the proxy three times. That took about 30 s, so under load it went past its timeout. Separately, the "app stops" test left the app stopped, so in shuffled order the tests after it failed too.
What changed
- Frame limit you can set:
src/apps/proxy.tsnow exportsappWebSocketWith(maxMessage = WS_MAX_MESSAGE)in place of the fixedappWebSockethandler. A newwsMaxMessageoption inForgeConfig(src/app.ts) passes the limit throughserveinsrc/http/server.ts. Production still defaults to 16 MiB; only the test lowers it. - Smaller test frames: the frame-limit test now runs at 2 MiB. It still covers the same three cases: one byte over from the browser is refused, one byte over from the app closes with 1009 "message too big", and exactly at the limit passes both ways as text and binary.
- Stop test cleans up: the stop test still checks the close reason and the 502 page. In a
finallyit now redeploys the app and waits for that run to finish, so the tests after it find the app running. - Comment: the doc comment in
src/spawn/cell.tsnames the renamed handler.
What I chose not to do: the real 16 MiB limit is no longer tested end-to-end. The code path is the same and only the number differs, and moving 16 MiB is what made the test slow.
Checks: today's main is merged in. bun scripts/gate.ts passes, bun x tsc --noEmit is clean, and bun test test/apps-ws.test.ts test/mcp-size.test.ts gives 15 passed, 0 failed. Three more runs of apps-ws with --randomize each passed 11 of 11 in about 5 s. An earlier commit on this branch records 10 of 10 green runs from bun scripts/flake-hunt.ts. In this sandbox the apps run uncontained (FORGE_TEST_SOCKET=port), so the forge's check is the first run inside the full sharded gate under load.
To try it: bun test --randomize test/apps-ws.test.ts.
🤖 Generated with Claude Code
test/apps-ws.test.ts is back in the gate: its line comes out of EXCLUDED in scripts/gate.ts. Nothing new was pushed this run. The branch already holds today's main, and the issue this waited on is closed. The forge's check on this push was still running when I looked, so I have no result from it yet.
Why it failed in the full run. The frame-limit test pushed the real 16 MiB limit through the proxy three times. That took about 30 s alone and longer under load, so it ran out of time. Separately, the "app stops" test left the app stopped, so in the gate's shuffled order the tests after it failed too.
What changed
- A frame limit tests can lower:
src/apps/proxy.tsnow hasappWebSocketWith(maxMessage)instead of the fixedappWebSockethandler. A newwsMaxMessageoption insrc/app.tspasses the limit throughserveinsrc/http/server.ts. Production still uses 16 MiB. - Smaller test frames: the frame-limit test runs at 2 MiB. It still checks one byte over from the browser, one byte over from the app (closed with 1009 "message too big"), and exactly at the limit in text and binary. A run of the file now takes about 5 s instead of about 30 s.
- The stop test restarts the app: after its checks it redeploys the app and waits for that to finish, so the tests after it find the app running.
- Comment: a doc comment in
src/spawn/cell.tsnames the renamed handler.
What I chose not to do: the real 16 MiB value is no longer tested end to end. The code path is the same and only the number differs, and moving 16 MiB is what made the test slow.
Checks: bun scripts/gate.ts and bun x tsc --noEmit pass. bun test test/apps-ws.test.ts test/mcp-size.test.ts gave 15 passed and none failed. Three more runs of apps-ws in random order each passed 11 of 11. In this sandbox the apps run without their usual containment, so the forge's check is the first run of this file inside the full gate under load.
To try it: bun test --randomize test/apps-ws.test.ts
🤖 Generated with Claude Code
test/apps-ws.test.ts used to fail 5 of its 8 tests. Now all its tests pass, and it is back in the gate.
- Size limit can be set:
apps/proxy.tsnow builds its websocket handler withappWebSocketWith(maxMessage). The largest frame it allows still defaults to 16 MiB, andForgeConfig.wsMaxMessagecan lower it. The frame-limit test uses this to run at 2 MiB instead of pushing 16 MiB frames.src/http/server.tspasses the setting through. Production behaviour is unchanged. - Test fixes: the stop test now starts the app again before the next test runs.
scripts/gate.tsno longer excludes the file. - Fix for the failing check:
test/security-floor-doors.test.tswas stale, and it fails on main too. #10954 pinned the exact textcompress(request, response).then(securityFloor). Then #10975 changed that line inserver.tsto.then((r) => securityFloor(resourcePolicy(request, r))). The test now checks that the compressed answer still passes throughsecurityFloor, without requiring that exact text. The security floor itself is still applied on that path.
Checks run here: bun scripts/gate.ts, bun x tsc --noEmit, test/apps-ws.test.ts (11 pass) and test/security-floor-doors.test.ts (7 pass). I did not run the full suite; the forge runs it on the push.
To try it: bun test test/apps-ws.test.ts test/security-floor-doors.test.ts.
🤖 Generated with Claude Code
test/apps-ws.test.ts is back in the gate: its line comes out of EXCLUDED in scripts/gate.ts. It no longer depends on timing or on the order the tests run in. The forge has not yet run its check on this push; the full gate under load is the part not yet tried.
Why it failed in the full run. The frame-limit test sent three 16 MiB frames through the proxy. That took about 30 s on its own and longer under load, so it ran out of time. The "app stops" test also left the app stopped, so in shuffled order the tests after it failed.
What changed
- Frame limit tests can lower:
src/apps/proxy.tsnow hasappWebSocketWith(maxMessage)instead of the fixedappWebSockethandler. A newwsMaxMessageoption insrc/app.tspasses it throughserveinsrc/http/server.ts. Production still uses 16 MiB. - Smaller test frames: the frame-limit test runs at 2 MiB and still checks one byte over from the browser, one byte over from the app (closed with 1009 "message too big"), and exactly at the limit in text and binary. The file now runs in about 5 s instead of about 30 s.
- Stop test brings the app back: after its checks it redeploys the app and waits for that to finish, so the next test finds it running.
- Comment: a doc comment in
src/spawn/cell.tsnames the renamed handler. - Merge with
main: both sides had changed the same assertion intest/security-floor-doors.test.ts, and this branch's version is replaced bymain's, so that file matchesmain.
Left out: the real 16 MiB value is no longer tested end to end. The code path is the same, only the number differs, and moving 16 MiB is what made the test slow.
Checks: bun scripts/gate.ts and bun x tsc --noEmit pass. bun test test/apps-ws.test.ts test/security-floor-doors.test.ts test/mcp-size.test.ts test/mirror-credit.test.ts gives 32 passed, 0 failed. In this sandbox apps run uncontained, so this file has only been run alone here, never in the full sharded gate.
To try it: bun test --randomize test/apps-ws.test.ts
🤖 Generated with Claude Code
No push this run: the branch already contains all of today's main and is mergeable. The forge's check on the current head (d6ad24b86) is still queued, so the change has not yet been run inside the full sharded gate under load.
test/apps-ws.test.ts is back in the gate (its line comes out of EXCLUDED in scripts/gate.ts). It no longer depends on timing or test order.
Why it failed in the full run. The frame-limit test sent three 16 MiB frames through the proxy, about 30 s alone and longer under load, so it ran out of time. The "app stops" test also left the app stopped, so in shuffled order the tests after it failed.
What changed
- Lower limit for tests:
src/apps/proxy.tshasappWebSocketWith(maxMessage)instead of the fixedappWebSockethandler. A newwsMaxMessageoption insrc/app.tspasses the limit throughserveinsrc/http/server.ts. Production still uses 16 MiB. - 2 MiB frames: the frame-limit test still checks one byte over from the browser, one byte over from the app (closed with 1009 "message too big"), and exactly at the limit in text and binary. The file runs in about 5 s instead of about 30 s.
- App brought back: after its checks, the stop test redeploys the app and waits for it to finish, so the next test finds it running.
- Comment: a doc comment in
src/spawn/cell.tsnames the renamed handler. - Merge:
test/security-floor-doors.test.tsismain's version, which fixed the same assertion this branch had.
Left out: the real 16 MiB value is no longer tested end to end. The code path is the same and only the number differs, and moving 16 MiB is what made the test slow.
Checks: last run, on this same head: bun scripts/gate.ts and bun x tsc --noEmit pass. bun test test/apps-ws.test.ts test/security-floor-doors.test.ts test/mcp-size.test.ts test/mirror-credit.test.ts gives 32 passed, 0 failed. In this sandbox apps run uncontained, so the full gate under load has not been tried here.
To try it: bun test --randomize test/apps-ws.test.ts
🤖 Generated with Claude Code
Nothing new to push this run. The branch is the same commit as last run (d6ad24b86) and already has everything on main; no new commits have landed there since. The forge's check on this head is still queued, so the change has not yet been run inside the full sharded gate under load.
test/apps-ws.test.ts is back in the gate: its line comes out of EXCLUDED in scripts/gate.ts. It no longer depends on how long the run takes or on the order the tests run in.
Why it failed in the full run. The frame-limit test sent three 16 MiB frames through the proxy. That took about 30 s alone and longer under load, so it ran out of time. The "app stops" test also left the app stopped, so in shuffled order the tests after it failed.
What changed
- A limit tests can lower:
src/apps/proxy.tshasappWebSocketWith(maxMessage)instead of the fixedappWebSockethandler. A newwsMaxMessageoption insrc/app.tspasses the limit throughserveinsrc/http/server.ts. Production still uses 16 MiB. - 2 MiB frames in the test: it still checks one byte over from the browser, one byte over from the app (closed with 1009 "message too big"), and exactly at the limit in text and binary. The file runs in about 5 s instead of about 30 s.
- The app comes back: after its checks, the stop test redeploys the app and waits for that to finish, so the next test finds it running.
- Comment: a doc comment in
src/spawn/cell.tsnames the renamed handler. - Merge:
test/security-floor-doors.test.tstakesmain's version, which fixed the same assertion this branch had changed.
Left out: the real 16 MiB value is no longer tested end to end. The code path is the same and only the number differs; moving 16 MiB is what made the test slow.
Checks: last run, on this same head: bun scripts/gate.ts and bun x tsc --noEmit pass. bun test test/apps-ws.test.ts test/security-floor-doors.test.ts test/mcp-size.test.ts test/mirror-credit.test.ts gives 32 passed, 0 failed. In this sandbox apps run uncontained, so the full gate under load has not been tried here.
To try it: bun test --randomize test/apps-ws.test.ts
🤖 Generated with Claude Code
Read every changed line. The websocket frame limit is now a parameter (default still 16 MiB), the same value drives Bun maxPayloadLength, backpressureLimit and the app-side 1009 check, and only tests set wsMaxMessage. No new input, auth, or header surface, so no security findings. The stop test restores the app in a finally, which fixes the order dependence, and the apps-ws gate exclusion is removed, which is what #1073 asks for. Minor, non-blocking: the real 16 MiB value is no longer exercised end to end.
I merged
maininto this branch, kept the intent of both sides in the three conflicting files, and pushed.bun scripts/gate.tsandbun x tsc --noEmitare clean. The 116 tests I ran (113 passed, 3 skipped, 0 failed) covertest/apps-ws.test.ts,test/check-list.test.ts,test/apps.test.ts,test/apps-adversarial.test.tsandtest/hardening.test.ts. The skipped ones are the unix-socket cases, which this sandbox can't run; the forge's check runs them on the push.How each conflict was resolved:
scripts/gate.ts: Takesmain's new shape, where the gate is every test file minusEXCLUDEDand the separateGATElist is gone. It drops thetest/apps-ws.test.tsexclusionmainhad added, because this branch fixes that test, so it is gated again.src/apps/proxy.ts: Keeps this branch'sappWebSocketWith(max), which lets an app's config set its own frame limit. It addsmain's per-port socket hold, whichopentakes out andclosereleases.src/http/server.ts: Takesmain's reorganisedserve, withfetch: handleandserveInternal. The WebSocket handler is nowwithShells(appWebSocketWith(app.config.wsMaxMessage)), and the imports keep bothtype AppSocketandisWebSocketHandshake. A staleappWebSocketmention in a comment insrc/spawn/cell.tsnow saysappWebSocketWith.Try: on the preview, open a WebSocket to an app and send a frame; it should echo back through the proxy.
🤖 Generated with Claude Code
Continues #10986