fix(workbench): show the diagnostic code on the connection gate; deterministic runtime-owner assertion - #589
Merged
Conversation
…rministic runtime-owner assertion The connection gate rendered ProjectClientError.message alone, which projectError had rewritten to 'Workbench request failed with HTTP <status>.' - so the no-flag contributor loop showed 'Workbench request failed with HTTP 200.' with no AB8003, no reason, and a 200 presented as the failure. ProjectClientError now keeps the diagnostic message and code and carries status only for a 4xx/5xx response; connectionFailureText renders '<code> - <message> (HTTP <status>)' on the gate (ConnectionGate, extracted to workbench-screen.tsx) and the topbar. The client-side AB8003 refusal names the page origin, the foreground origin, and --workbench-dev-origin. mcp-app-preview-browser.test.ts asserted the second /runtime-bootstrap request synchronously after the create event; requestRecorder() in tests/support/http.ts lets the test await the nth request through a promise the route handler settles.
🦋 Changeset detectedLatest commit: ff62f76 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
…ver rebuild and recovered-source refusals - projectFailureText (was connectionFailureText) also formats the Overview rebuild alert, so a refused rebuild reads 'AB8003 — … (HTTP 403)' instead of the bare server message. - agent-bundle patch changeset: the Workbench dist ships inside the published package (precedent #579), so the gate text change is user-visible. - Unit tests: rebuild() rejection carries code/message/status; a refused session re-bootstrap during recovered-source refresh reaches onError as a ProjectClientError with code and status.
…HTTP status only from a failed response - #recover reports a failed attempt through onError when its line differs from the last report, so a foreground restarted without --workbench-dev-origin moves the gate from 'Foreground project event stream disconnected.' to the AB8003 refusal instead of retrying silently. - ForegroundRouteClientError.responseStatus records the status of a failed foreground response (fromResponse); ProjectClientError.status comes from it, so client-constructed 401s (superseded/invalidated session) carry no '(HTTP 401)'. - Tests for both; docs en/zh mention the live-restart case.
…he AB8003 line; docs wording contributor-hmr.e2e step 4 keeps the page open while the foreground restarts without --workbench-dev-origin and asserts the alert becomes the exact status-less AB8003 line (the recovery bootstrap is an Origin-less GET the foreground answers 200 and the UI refuses); step 5 is the fresh load as before, via about:blank because a goto to the shown URL is same-document. Docs en/zh name that line explicitly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the two follow-ups left by #581 (
fb56fa06a). Workbench-only:packages/agent-bundle/srcis untouched.Problem
projectErrorrewrote everyForegroundRouteClientErrorintoProjectClientError('Workbench request failed with HTTP <status>.', code), and the gate rendered.messagealone. In the documented no-flag contributor loop (Rsbuild UI onhttp://localhost:3000, foreground onhttp://127.0.0.1:3100, no--workbench-dev-origin) that readWorkbench request failed with HTTP 200.— noAB8003, no reason, and a 200 presented as the failure when the foreground had answered fine and the client refused the bootstrap body. Recovery attempts that kept failing were swallowed, so restarting the foreground without the flag while the page was open left the gate onForeground project event stream disconnected.forever.packages/workbench/tests/mcp-app-preview-browser.test.ts.fixture.bootstrapRequestswas a plain array pushed from the fixture server's handler and asserted synchronously right after the in-pagecreateevent. The iframe fetches/runtime-bootstrapafter that event, so the second-request assertion raced the network (flaked once under contention, passed alone).Change
Gate, topbar, and the Overview rebuild alert show
<code> — <message>, plus(HTTP <status>)only for a failed foreground response.packages/workbench/src/project-client.ts:ProjectClientErrorkeeps the underlying diagnostic's message and code and gainsstatus, taken fromForegroundRouteClientError.responseStatus— set only byfromResponse, i.e. when the foreground actually answered 4xx/5xx; failures the client constructs itself (refused 200 bootstrap body, superseded/invalidated session with a nominal 401) carry none. NewprojectFailureText(reason, fallback)formats the one line and omits the parts a failure lacks; non-ProjectClientErrorreasons keeperrorMessage(reason, fallback)(the existingclient-helpers.tshelper — nothing copied).#refreshRecoveredSourcereports throughprojectErrortoo.#recovernow reports a failed attempt throughonErrorwhen its line differs from the last report (retries run every 250 ms; identical refusals are not re-reported), so the gate moves from the disconnect line to the refusal that keeps recovery from completing.packages/workbench/src/main.tsx: the threesetConnectionErrorsites (bootstrap rejection,connecterror listener,beforeInstanceChangeclose failure) and the Overviewrebuild()alert useprojectFailureText; the topbar'sForeground server unavailable: …status is the same line. The rebuild alert therefore reads e.g.AB8003 — Request origin is not this foreground server. (HTTP 403)instead ofWorkbench request failed with HTTP 403..ConnectionGatemoves out ofmain.tsxintopackages/workbench/src/workbench-screen.tsxbesideTopbar;main.tsximports it and the inline copy is gone in the same commit (extract and rewire, one change). Markup is unchanged (overview.e2eandpacked-release.e2elocate the gate by heading).packages/workbench/src/mcp/mcp-route-client.ts: the client-sideAB8003refusal (page origin is neitherbody.originnor indevOrigins) names both origins and the fix instead ofForeground session bootstrap origin does not match this browser.;ForegroundRouteClientErrorgainsresponseStatus(see above). No new error class;diagnostic()is untouched.What the no-flag loop shows on the gate now, exactly:
No
(HTTP 200): the foreground returned 200 with a well-formed body naming its own origin, and the UI refused it — a 200 was not the failure, and printing it was the misleading part of the old line. A refusal the foreground itself answers with, e.g. 403{"diagnostic":{"code":"AB8003","message":"Request origin is not this foreground server."}}, rendersAB8003 — Request origin is not this foreground server. (HTTP 403); a status-route 500 rendersAB8007 — Request could not be completed. (HTTP 500); aProjectClientErrorwithout a code (Foreground project event stream disconnected.) renders unchanged; a client-invalidated session rendersAB8019 — Foreground authentication was invalidated.with no status.Deterministic runtime-owner assertion.
packages/workbench/tests/support/http.tsgainsrequestRecorder()→{ arrived(n), paths, record(path) }.arrived(n)resolves inside the handler that records the nth request, or on the microtask queue whennhave already arrived; pending waiters settle FIFO; no timers. The four "a request arrived" assertions inmcp-app-preview-browser.test.ts(two per runtime case: after the firstfactoryand aftercreate:runtime-binding-b) becomeawait within(fixture.bootstrapRequests.arrived(n), 5_000 * timeScale); the "no new request" checks stay synchronous reads of.paths. No sleeps, no polling;withinonly turns a missing request intoTimed out after …instead of the 45 s test timeout.tests/support/http.ts— the fixture HTTP module that already ownscloseServerfor loopback fixture servers — not inworkbench-fixture-config.ts, which is the Rsbuild config factory (createWorkbenchFixtureConfig) and has nothing to do with fixture servers.mcp-app-frame.test.tshas the same array +expect.pollshape and can adopt the recorder in a follow-up.tests/support/browser-origin.tsholds the onewithBrowserOriginstub (was inline inmcp-route-client.test.ts;project-client.test.tsneeded it too).Proof
project-client.test.ts(+6:connect()against a 200 bootstrap for a foreign page origin rejects withcode 'AB8003', the exact message,status undefined, the same object ononError,connection.state 'unavailable'; a refusedrebuild()carries{ code: 'AB8003', status: 403 }and formats asAB8003 — … (HTTP 403); a refused session re-bootstrap during recovered-source refresh reachesonErroras aProjectClientErrorwith code and status; a session the client invalidated mid-bootstrap carriescode 'AB8019'and no status; two identical refused recovery attempts produce one report after the disconnect line and none once the next attempt succeeds; aprojectFailureTextmatrix — code+status, code only, no code, plainError, hostile getter, non-Error,undefined; the 500 case now asserts{ code: 'AB8007', message: 'Request could not be completed.', status: 500 }),workbench-screen.test.ts(+3:ConnectionGateunavailable + 403 →<p role="alert">AB8003 — Request origin is not this foreground server. (HTTP 403)</p>; unavailable + no-flag 200 → code prefix and noHTTP; connecting → no alert),mcp-route-client.test.ts(both AB8003 rejections assert the full message forhttp://localhost:3001/:3000againsthttp://127.0.0.1:4100).pnpm test:unit: 3730 passed, 0 failed.contributor-hmr.e2e.test.tsextended with steps 4 and 5. Step 4, live: with the page open and connected, the foreground is restarted on the same port withoutworkbenchDevOrigins(the compiled dev server keeps proxying to it); the event stream drops, recovery re-bootstraps through the proxy, and the test asserts theForeground connection unavailableheading andgetByRole('alert')toHaveTextof the exact line above (built fromdevOriginand the foreground URL) — i.e. the gate settles on the refusal, not onForeground project event stream disconnected.(this is the browser-level proof of the recovery-reporting fix; before it the alert never left the disconnect line). Step 5, fresh load (about:blankfirst — agototo the URL already shown is a same-document navigation): bootstrapGET /api/project/sessionis 200 with noOriginrequest header, bodyorigin= foreground URL and nodevOrigins; the same heading and exact alert text, containingAB8003and--workbench-dev-origin <devOrigin>, not containingHTTP 200orWorkbench request failed; noBundle dashboard; no page errors; and the server-side half — the sameOriginthrough the proxy is now 403AB8003. 4/4 consecutive runs green.mcp-app-preview-browser.test.ts10× sequential in the integration config: 10/10 passed (4/4 tests each; 8.8–11.9 s per run, 98 s total). Extra contention: 3 concurrent loops × 3 → 9/9; 2 concurrent loops × 2 pinned to two cores (taskset -c 0-1, CI-like) → 4/4.pnpm typecheck✓ ·pnpm lint✓ (0/0, 1266 files) ·pnpm test:unit✓ (3730) · integration subsetpnpm test:integration:runfiltered tomcp-app-preview-browser.test.ts,contributor-hmr.e2e.test.ts,overview.e2e.test.ts✓ ·pnpm docs:site:build✓ (typecheck, Rspress build, language parity).Docs
website/docs/{en,zh}/guide/development/workbench.mdx, the one paragraph describing the no-flag failure:Workbench request failed with HTTP 200.is replaced by the exact gate line above, the rule that(HTTP <status>)is appended only when the foreground response itself failed (the 403 example), and the live case — restarting the foreground without the flag while the page is open shows the disconnect line, then the sameAB8003refusal.reference/security.mdx,reference/cli.mdx, andpackages/agent-bundle/README.mddo not describe the gate's text and are unchanged.Files
packages/workbench/src/{project-client.ts,main.tsx,workbench-screen.tsx,mcp/mcp-route-client.ts}packages/workbench/tests/{project-client,workbench-screen,mcp-route-client,mcp-app-preview-browser,contributor-hmr.e2e}.test.ts,packages/workbench/tests/support/{http,browser-origin}.tswebsite/docs/{en,zh}/guide/development/workbench.mdx.changeset/workbench-gate-diagnostic-code.mdChangeset
'agent-bundle': patch.packages/workbenchis private, but itsdistships inside the publishedagent-bundlepackage (dist/workbench), so the gate text is user-visible in the next release — the same reasoning as #579's Workbench-only changeset. Theskip-changesetlabel this PR opened with was removed on the first reviewer's finding.Self-review
Pass 1 —
change-risk-reviewer(inherited model; TraceDecay daemon down, static review).skip-changesetwas wrong because the Workbenchdistis published insideagent-bundle(precedent feat(workbench): launch the standalone MCP Inspector and open it in a new tab #579). Fixed —.changeset/workbench-gate-diagnostic-code.md, label removed.rebuild()rejections lostWorkbench request failed with HTTP <n>.and rendered the bare server message without code/status). Fixed — the rebuild alert is formatted byprojectFailureTexttoo (renamed fromconnectionFailureTextsince it no longer serves only the gate); unit test on a refusedrebuild()pins{ code, message, status }and the rendered line.#refreshRecoveredSourcewrapping had no distinguishing test. Fixed — test: recovered stream opens with a forgotten session, the status read succeeds, the snapshot re-bootstrap is refused 403AB8003, andonErrorreceives aProjectClientErrorwith code and status.Pass 2 —
generalPurposeongpt-5.6-sol-medium, after the fixes above.#recoverswallowed every failed attempt, so a live foreground restart without the allowlist left the gate onForeground project event stream disconnected.and never showedAB8003. Fixed —#reportRecoveryFailurereports an attempt whose line differs from the last report (deduplicated, no listener flooding at the 250 ms retry cadence); unit test covers two identical refusals → one report, success → no further report; docs en/zh state the live case.status >= 400treated client-constructed errors (AB8019 … invalidated, nominal 401) as failed responses, contradicting the docs/changeset claim. Fixed —ForegroundRouteClientError.responseStatusis set only byfromResponse;ProjectClientError.statuscomes from it, no heuristic; unit test: a session invalidated mid-bootstrap hasstatus undefinedand renders without(HTTP …).projectFailureTextoutputs for code+status / code only / no code; no remainingWorkbench request failed with HTTPproducer orconnectionFailureTextreference;browser-origin.tshas two test importers;requestRecordercannot strand a waiter and resolves already-satisfied counts; the changeset quotes themcp-route-client.tsmessage verbatim; no dead modules, duplicated helpers, sleeps/polling, or en/zh parity gaps.Pass 3 —
generalPurposeongpt-5.6-sol-medium, on the pass-2 fix commit (db59f5672) only.(HTTP 403)example, reasoning that a restarted un-allowlisted foreground answers/api/project/sessionwith 403. Reworded in en/zh to "the same status-lessAB8003 — Origin …line". The premise is not what happens: the recovery bootstrap is the browser's same-origin GET withoutOrigin, so the foreground answers 200 and the UI refuses the body exactly as on a cold load — now proven in the browser by the new live step 4 ofcontributor-hmr.e2e.test.ts, which asserts that exact status-less line after a live restart.connect()rejection then recovery — no double report; disconnect → refusal → both reported; A/B/A transitions each reported);main.tsxlistener is last-error-wins;close()interplay; all 14 directForegroundRouteClientErrorconstructions are client-side and only the threefromResponsecalls setresponseStatus;statusuntouched and nostatus === 404consumers; both new unit tests deterministic (the recovery test's twosetImmediateticks cover the fetch/body/status microtasks; 20/20 focused runs); en/zh equivalent; no hygiene issues.