Repository navigation
Conversation
7 of 11 tasks
knihit
marked this pull request as ready for review
October 26, 2026 03:46
knihit
requested review from
georgebearden,
ihmaws,
shsenior and
simonkrol
as code owners
October 26, 2026 03:46
knihit
force-pushed
the
wc/05-canvas
branch
5 times, most recently
from
October 27, 2026 21:08
e54a05a to
9fcbc48
Compare
knihit
force-pushed
the
wc/05-canvas
branch
2 times, most recently
from
October 27, 2026 22:06
0fc7a25 to
aeba79c
Compare
Base automatically changed from
wc/04-stub-api
to
integration/workflow-canvas
October 28, 2026 14:31
Move the scan out of the stub core's test into test-support/purity.ts, parameterised by the rules of one core, so a second core can be held to the same rule. The scan's own tests move with it, unchanged.
Open a session with the launch code in the address, load the workflow, and draw the intent, each stage in list order and the end with React Flow, read-only. The graph is built by an IO-free core. Loading, error and empty states are shown.
Open one shared session in the Playwright global setup, because a launch code works once, and answer the page's session call with it. Add canvas tests, an axe check of the error state, and regenerate the Designer screenshot.
Give each node and edge a readable aria-label built in the graph core, and replace the edit instructions of React Flow with read-only wording. Zoom out far enough to fit the 40 stages the API allows. Leave out a diagnostic line that is not a whole number from 1, refuse an API version other than 1, and keep the canvas colours in canvas.css only. Read the real replies of the stub with the page's parsers in a test.
The badge font shorthand named Arial, which each host resolves to a different face, so the Designer screenshot differed between a local run and CI. Inherit the page font so the capture is the same everywhere, and refresh the screenshot.
The server may repeat an identical diagnostic, which gave two list items the same key. Add the index to the key and test a repeated problem.
The test deep-compared two whole replies, so the HTTP Date header made it fail whenever the two GETs fell in different seconds. Compare the status and body instead.
ihmaws
reviewed
Oct 28, 2026
Pasting the printed address into the same tab only changes the fragment, so the page stayed on "no launch code". Reload on hashchange, and make the reload e2e test open the address in the same tab.
…-proof test - Reload on a pasted launch address only when the fragment has the shape of a launch code (22 to 256 URL-safe characters), so an anchor such as #some-anchor no longer throws the loaded workflow away. - Keep the whole name, agent and outcome in a title on each node, and write out an outcome that is too long for the intent node under the graph. - Reject a reply with two stages of one name instead of hiding a node. - Announce the loading and error states with a status and an alert region. - Make the Playwright port a setting, WC_APP_PORT (default 4173), read in one place. - Replace the wall-clock limit on the long-line check with counts of operations, so load cannot fail it. - Scan the no-IO cores without their comments, and catch window, document, storage, sockets and import.meta.env. - Add an e2e check that 40 stages fit in the canvas, and fix the README note.
…, scan as TypeScript - A launch code pasted into the open tab opens a session in the background. The page changes to the new workflow only when that works, so a refused code leaves the workflow on the page. - Clip by drawn width, not by characters, so Japanese and emoji text is listed in full under the graph. The stage name, agent and harness are listed there too when a node cuts them short. - Read the sources of the no-IO cores as TypeScript. Aliases, XMLHttpRequest, WebSocket, sendBeacon, storage, import.meta, animation frames, dynamic imports and a // inside a string are handled; a local name such as document is not taken for the global. - Test that the first load runs once under strict mode.
7 tasks done
…pt every code shape A workflow that replaces the one on the page gets a new canvas, so it is fitted again. Offers settle by one rule: the newest workflow wins, and an error shows only while no workflow does, checked again after the last wait. A launch code is any 16 to 256 characters without whitespace, as the API allows.
…code A refused launch address left the open workflow in place and said nothing. The page now shows a dismissible notice with the reason. Names that the server puts in backticks are shown as code, without the ticks.
11 tasks done
The launch code works once and the page removes it from the address, so a reload ended in "no launch code". The page now keeps the session token and harness in sessionStorage under one key, behind a small port, and a load with no code in the address resumes from it. - A pasted code replaces the kept session only when its workflow is shown. A refused code keeps the old one. - A 401 on the kept token clears it and asks for a fresh launch address. - The launch code is never stored. - STUB_STRICT=1 makes dev:stub follow the contract (the code works once). The stub that Playwright starts always does.
This was referenced Oct 28, 2026
…ode is spent A first load that failed after the launch code worked left no stored session, so a reload had no code and no token. The session is now kept when the code opens it and none is kept; a pasted code still replaces a kept session only once its workflow is shown. A 401 on any call that carries the token now clears it, through one shared guard. A spent code in the address no longer beats a valid kept session: the session is tried first, and the error of the code is shown only if both fail. The page gets a Content-Security-Policy meta tag (own files only). The README and API text now say the token lasts for the tab, is copied by Duplicate tab and window.open, and is dropped on any 401. The advice after a lost session is to get a fresh launch address from the server.
This was referenced Oct 28, 2026
…orm targets The test now compares every directive and source, so adding data: or blob: to a script source, or ws: to connect-src, fails. The policy gains base-uri 'none' and form-action 'none', which default-src does not cover.
A tab that held a session from an earlier server run lost the new session when the first load failed: the dead one was cleared on its 401 and a reload found nothing. The session of a code in the address now replaces the kept one as soon as the code opens it, unless a newer session was kept meanwhile. A spent address that falls back to the kept session now says so in a notice, and the messages say an address works once.
…wn session is dead A pasted launch address opens a session, but the page keeps the shown session until the pasted workflow is on screen. If the shown session had died and the first load of the pasted one failed, a reload tried only the dead session and the spent address was lost. The session of a pasted address now waits as a pending session under its own key. A reload tries the pending session before the kept one, falls back to the kept one with a notice if it fails, and showing a workflow keeps its session and clears the pending one. The tests now use two sessions with the same harness, and pin which session the fallback resumes and that a stale notice stays quiet.
…he load began A pasted address whose session answers after a newer paste was kept no longer becomes the pending session, so a reload resumes the session the page shows. A pending session that failed briefly stays until it gets a 401 or a newer session replaces it, and a plain reload no longer tells that an address was not opened.
This branch has not been deployed
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.
Description
Canvas (
workflow-canvas/src/canvas/,src/graph/):@xyflow/react12.12.0, exact pin): the intent, each stage in list order with its agent and harness, and the end, joined by run-order edges. This is the first screen that uses the contract and the stub from the earlier PRs.src/graph/.buildGraph(workflow, harness)returns nodes and edges placed in one column. A test fails if the core imports anything but its own modules or reads a clock, randomness, the environment or the network. It is the same scan the stub core uses, now shared fromtest-support/purity.ts.aria-labelbuilt in the graph core, React Flow's edit instructions are replaced by read-only wording, nodes can be reached with the Tab key, and the focus ring is 3px. The canvas colours live only incanvas.css.Loading the workflow (
src/api/,src/launch/,src/workflow/,src/designer/):WorkflowApiis the port with the two calls the page makes;createHttpApiis its adapter overfetch. It ignores fields it does not know and turns a failed call into anApiErrorwith a message fit to show.#<launch code>), removes it from the address bar, trades it for a token withPOST /api/sessionand loadsGET /api/workflow. The token is kept in the session storage of the tab, so a reload resumes (see the last section). The first load starts once, before React renders (createWorkflowSource), because a launch code works once and strict mode runs effects twice.linethat is not a whole number from 1 is left out, and anapi_versionother than 1 is refused before the workflow loads.Docs and config:
workflow-canvas/README.mddescribes the canvas, the layers around it and how to open the page with a launch code. CHANGELOG has an entry under Unreleased.VERSIONis untouched.package-lock.jsonis updated for the new dependency.How to run
cd workflow-canvas npm ci npm run dev:stubdev:stubstarts the stub API and Vite together and prints the launch code to open the page with. Ctrl+C stops both. SetWC_API_TARGETto point the page at another server that followsapi/openapi.yaml.Fixes from the hands-on QA run
watchLaunchCode), but only for a fragment shaped like a launch code (16 to 256 characters without whitespace; the API sets no alphabet), and the page changes to the new workflow only if that works. An anchor such as#some-anchorchanges nothing. This is product code insrc/launch/,src/workflow/andsrc/main.tsx, with a README note and a CHANGELOG line.e2e/tests/reload.spec.tsopens the launch address, reloads, and expects the workflow to load again with no request toPOST /api/session.Fixes from the adversarial review
title; an outcome too long for the intent node is also written out under the graph.role="status",role="alert").WC_APP_PORT(default 4173), read inplaywright.shared.tsonly.Type of Change
package.json,Cargo.toml, or CLI dependency)workflow-canvas/Testing
npm test(tests/scripts/**/*.test.js): not run; this PR changes onlyworkflow-canvas/, the rootCHANGELOG.mdand its own docs.cli/changed: not applicableChecked in
workflow-canvas/:npm run lint,npm run typecheck(3 projects) andnpm test(39 files, 706 tests) pass at584cbe5, and so do the reload and policy e2e specs (14 tests, against the stub). The full e2e suite runs at the top of the stack (#97). The build prints only the existing chunk-size warning.Tests added:
lineandapi_versionrules), the Designer states and the settle hook.stub-api/http/ui-wire.test.tsparses the real replies of the stub with the page's parsers, so the page and the contract cannot drift apart.Screenshots
Designer tab, full page, light theme (regenerated by
npm run screenshots):Checklist
Notes for Reviewers
integration/workflow-canvas, which already holds Add a stub API for the workflow canvas contract #81. The PR has 14 commits; the last three (6a38de2,b68a4ad,7294fd9) answer the adversarial reviews.Fixes from the second adversarial review
createWorkflowSource,watchLaunchCode; unit tests and e2e for a refused and an accepted code; a test that the first load runs once under strict mode).const f = fetch),XMLHttpRequest,WebSocket,sendBeacon, storage,import.meta, animation frames and dynamic imports, tells a local name such asdocumentfrom the global, and does not take//in a string for a comment. 33 of its new tests fail on the old scan.Fixes from the third adversarial review
+,/,=or.pass. A longer anchor is offered as a code and refused, and the page keeps the workflow.titleholds the full text, and realistic names and sentences are listed); the holes of the no-IO scan (the README says it guards against accidents and is not a sandbox); a timeout on the two calls (they get one in Read runs through the typed client and add the run core #115, and the launch code call has none in Run the workflow from the Execution tab with live node states #93).Fixes from the second hands-on QA run
LaunchNotice;noticeanddismissNoticeon the workflow source). This reverses the earlier rejection of a message for a refused paste: nothing on screen said the code was refused. Unit tests for the source and the notice, and an e2e assertion.MessageText).Fixes: a reload keeps the session
sessionStorage, behind a small port (launch/session-memory.ts), and a load with no code in the address resumes from it. Script on the page can read the token. It lasts for the life of the tab, including copies made by Duplicate tab orwindow.open, and a browser may restore it with the tab;index.htmlcarries a Content-Security-Policy that lets the page run only its own files. The token goes only to the same-origin/api, and nothing logs it.LoadResult.remember, called bycreateWorkflowSource).STUB_STRICT=1 npm run dev:stubmakes the dev stub follow the contract (the code works once). The stub that Playwright starts always does, so the e2e suite runs against a single-use code.api/openapi.yaml: one sentence says a client may keep the token for the life of its browser tab, that it survives a reload, and that the client drops it on any 401. Nothing else in the contract changes.loadWorkflow(resume, 401 clears, a newer session is not forgotten, the code is never stored), the source (remember on show, replace, refused paste, racing codes), and e2e for reload, 401 after reload, a refused paste and "the launch code is not in storage". Hands-on onSTUB_STRICT=1 npm run dev:stubin a browser: open, reload, restart the stub then reload, paste a new address, a refused paste, and a fresh tab.Round 6: the session is kept as soon as a code opens it
POST /api/sessionworked and the firstGET /api/workflowthen failed (timeout, 5xx), nothing was stored and the spent code left a reload with no way in. The session is now kept right after the code opens it when none is kept. A pasted code still replaces a kept session only once its workflow is shown, and a refused paste keeps it.loadOnOpentries the code, then the kept session, and shows the error of the code only if both fail.openapi.yamland the README now say the token lasts for the tab, survives a reload, is copied by Duplicate tab andwindow.open, and is dropped on any 401. The advice is now "get a fresh launch address from the server".<meta>policy (default-src 'self', inline styles,data:images and fonts). It breaks nothing: the e2e suite and the dev server show no violation, and a test checks that an inline script is refused.STUB_STRICT=1): open, reload, a 500 on the first GET then reload, a 401 on save and on run start, a spent code with a kept session, a pasted spent code, and two tabs.Round 7: a fresh address over a dead session, and the policy pinned
GET /api/workflowthen failed (timeout, 5xx), the new session was not stored, the dead one was cleared on its 401, and a reload said "no launch code". The session of a code in the address now replaces the kept one as soon as the code opens it, unless a newer session was kept meanwhile. A pasted code still replaces a kept session only once its workflow is shown, and a refused paste keeps the old one.test-support/csp.test.tscompares every directive and source, so addingdata:orblob:to a script source, orws:toconnect-src, fails. The policy gainsbase-uri 'none'andform-action 'none'.loadOnOpenwith a dead kept session and a failing first load, the notice (unit for the loader and the source), and an e2e test of the dead session. An existing test (a slow first open that answers after a pasted code) failed against a first draft of the fix, so the early store replaces only the session that was kept when the open began; a new test pins that case over a kept session.vite preview, strict stub restarted between steps): the old build ends in "no launch code" after the reload, the new build resumes. With the new policy, open and reload show no violation and no console error, an injected<base>and a form submit are each blocked, and the notice shows for a spent address over a kept session.data:nit, the README wording of the inline script, and the old nits N-1 to N-7.Round 8: a pasted address whose first load fails
GET /api/workflowof the pasted session failed (a 5xx or a timeout), a reload sent the dead token, got a 401 and said to get a fresh address. The pasted code was already spent, so the session was lost for good.launch/session-memory.tsgainsrecallPending,holdPendinganddropPending). A reload tries the pending session before the kept one. If it fails, the kept one opens with the refusal notice. Showing a workflow keeps its session and clears the pending one. A 401 on a token drops only that token, and a newer session kept meanwhile is not replaced.loadWorkflowandloadOnOpen(hold, promote, drop on 401, order, fallback and refusal text), the source (a pasted session resumed after its first load failed over a dead kept session), and an e2e test of the same case. The e2e test fails on the previous code at the reload.vite preview, strict stub restarted between steps, free ports): the tab showed a workflow, the stub was restarted so its session was dead, a fresh address was pasted and its firstGET /api/workflowanswered 500 once. Before: the reload sent the dead token, got a 401, drew no node and cleared the storage. After: the reload sends oneGETwith the pasted session's token, gets a 200 and draws the 4 nodes. The same run on the build at the top of the stack (Test editing in the Designer end to end and add the changelog entry #97) gives the same result.Round 9: a late session no longer outranks the shown one
vite preview,STUB_STRICT=1): the old build resumed the late session on reload, the new build resumed the shown one.npm test(39 files, 706 tests), lint, typecheck and the reload e2e specs pass.