57a25807af010519e68098111bb2532c77c6d998
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
36ade35240 |
test(web): a Node suite between the boundaries, Playwright only on them
E2E and integration tests are rare and pin boundary contracts; unit tests assume those contracts and test between them (user, 2026-09-20). The browser half has four boundaries — RenderModel→DOM, JS→wasm, page→peer, worker→OPFS — and everything between them needed no browser at all. `web/shell.js` and `web/serial.js` are the seams: pure modules that RETURN A DESCRIPTION rather than take a DOM double. `gridPlan(model)` answers every question `gridTable` used to answer while creating elements — the data-row index across group headers, what a group header spans, a null group label against an empty one, an empty edit buffer against no editor — and `app.js` is now a loop that builds elements from it. Painting stays untested by Node; what to paint stopped being untestable. 26 tests in `node --test web/tests` (0.12s, no Rust toolchain, no wasm bundle, no npm install), plus three in `web/tests/bundle/` that read the generated .d.ts and so run in the browser job. Playwright goes from 29 tests/36.9s to 16/15.5s on one machine: `widgets.spec.js` and `pointer.spec.js` are gone, replaced by `contract.spec.js`, which asserts that EVERY field the render model projects reaches the screen. The four widgets those specs were written for were one failure at one boundary, and one assertion catches the fifth. It caught it: `doc_name`/`doc_dirty`/`doc_stored_as` reach no element (improvise-5jna, already open). They are named in `UNPAINTED` with the ticket, so the list is the debt written down rather than an exemption. Thirteen faults injected into the Node suite and eight into the served page, each watched to fail the test it names; the last of those was strengthened because the first version passed. `web/assemble.sh` grew an import-completeness guard whose own first version was blind to the multi-line import it exists for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3d3bec43e0 |
docs(web,harness): cap the JavaScript and the browser harness
web/app.js's transport/watchdog/frameCost arguments and web/worker.js's two ordering rules — including the one nothing here can test, that an `async` message listener does not serialize — are now prose in context/code/clients.md. The harness keeps its own section: why a worker, why a report, the engine coverage and what `wipe` deliberately cannot clear. harness/tests/*.spec.js's comments were the record of what each assertion proves; they are in context/test-evidence/clients.md under the exact test titles, including the `test.fail` measurement that could NOT become a CI guard because its trigger is the host's network stack, and the closed-invalid improvise-xps8 report that a simulated fix appeared to confirm. ws-server/src/main.rs's ```text fence was prose claiming the doctest exemption, not a runnable doctest; the synopsis is now two lines and the full form is in the prose file. No fences remain in this unit. Verified: node --check on every JS file touched; the non-comment diff for both '*.rs' and '*.js' is empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bf02472d89 |
fix(web): an async message listener does not serialize, and a watchdog must not overwrite a real error
Three findings from the review-only PR #13, which put 28 already-merged
commits in front of a second reader, plus two comments that had gone false.
worker.js — `self.onmessage` was `async` and awaited `handle()`, which awaits
`server.flush()`. An async listener does NOT serialize: the event loop may
start the next `message` while an earlier one is suspended at an await, so two
`on_message`/`flush` pairs interleave and a later frame's replies can be posted
before an earlier frame's. That breaks the one rule the file names first — a
`GridUpdate::Cells` that overtakes its labels is DROPPED by the client's fold,
not replayed (gotcha 33) — and the pre-start `pending` drain had the same race
twice over, since `server` was assigned before the loop that drained it, so a
message arriving mid-drain overtook the frames it was queued behind. Every
frame is now chained onto one tail promise, which also makes "arrived before
start() finished" an ordinary case of the same rule instead of a second path.
app.js — the handshake watchdog fires 8s after the handshake and writes "the
peer did not answer". A peer that failed LOUDLY has already said something more
precise (worker `error`, `messageerror`, socket `error`), and the watchdog then
overwrote it with the vaguer guess. Those three paths now disarm it:
`setShellFailure` clears the timer rather than merely pre-empting it. The
watchdog is for silence only, which is what it was always for.
README — the "In the browser" section still said the deployed page needs a
server and that the in-page worker "is not built yet". Both landed in the same
unreviewed batch (improvise-9x6, improvise-djm), so the README described the
published site as a client with nowhere to connect while index.html described
it correctly. Rewritten, and it now states the caveat the commits state: no
line of that JavaScript has ever run.
Two comments that had gone false with it, and for the same reason — a follow-up
commit updated one copy and not the others. `web/app.js`'s keyboard note said
key resolution was "text entry, and nothing else" and that upstream keys would
work "until improvise-bpq2 lands"; bpq2 landed, moved the grid, panel and tile
cursors to the client, and settled that the host must not know the keymap.
`Host::handle`'s `ClientFrame::Key` arm said the same thing;
|
||
|
|
bdce1636bb |
feat(worker-server): the whole App in a Web Worker, over postMessage + OPFS
improvise-9x6: the server half of the standalone static-web deployment (epic improvise-tm6). `improvise-worker-server` hosts the authoritative `App`, the command pipeline, the projection layer and VFS persistence behind `self.onmessage`/`self.postMessage`. It is small because everything it needs already existed: the frames are `improvise_protocol::frame`'s (improvise-xks6), the session loop is `Host` (previous commit), the basis check is `Sessions::apply`/`Checked` (improvise-ondj), and persistence is `Storage` + `OpfsFs` (improvise-6mq, i34). What is genuinely new is three things. `vfs.rs` — how a document gets INTO a browser's empty filesystem. Loading is unchanged: it is `persistence::load_from` on a `Storage`, the same call the native binary makes. Seeding is the new problem, and it is a `Storage::write`, so a fetched demo, an upload and a saved document take the same parse path and a seed never clobbers an edit. `list` is the document picker a browser has no OS file manager for. `worker.rs` — the loop: one port, one client, the `Viewport` handshake (dropped rather than closed, since there is no connection to close), and the filter from `Addressed`. Every decision is here and every test points at it. `bindings.rs` — `cfg(wasm32)`, decision-free, and the only part no test here can reach: it awaits `OpfsFs::open`/`attach`/`refresh`/`commit_deletes` and forwards strings. Two gaps found and reported rather than papered over. No client on any transport can ask its host to save — `effect::Save` is `ViewOnly`, so the wire vocabulary derived from the model effects cannot name it — and a wire `ModelCommand::SetCell` never sets `model_state.dirty`, because dirtiness is raised by a separate `MarkDirty` effect the key path emits. So the worker persists on its own behalf, keyed off the command variant rather than off a flag that is never set; `touches_document` carries the argument. `ClientFrame::Key` is still answered with nothing. A worker CAN call `reduce_key` — it holds the `App` — but nothing in the projection stream carries the host's own `ViewState`, so a half-wired arm diverges rather than under-delivers. `a_key_resolved_on_the_host_moves_state_no_client_can_hear` demonstrates it against a real `App` and `Sessions` (improvise-bpq2). 22 tests, all host-side: 16 in the crate and 6 in `tests/post_message.rs`, which drives the worker with nothing but the JSON a `MessagePort` carries. Bundle: 1,334,892 bytes raw, 496,965 gzipped — 485 KB against a 3 MB budget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH |