ec03f950b3d383357927eeda5b48756a7d20277b
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
89f67afd4c |
fix(harness): make a vacuous watchdog denial fail, and single-home the window
PR #65's review found this PR had planted a fresh instance of the defect it exists to repair. Both blocking findings fixed, each demonstrated. FINDING 1. "a peer that cannot be reached is named, and the watchdog stays out of it" could not see the property its comment claimed. `shell.arm` runs only after `await transport.ready`, which never resolves on a socket that never opens — so the watchdog is never armed there and denying "sent no document" denies an unreachable event. Reverting PR #21's routing left that clause passing. The aggravating half: `context/test-evidence/clients.md` already said so, and this PR EDITED that paragraph — inserting "runFor(10s) advances the watchdog" immediately above the sentence explaining it is never armed. Not text the PR left alone; text the PR walked past mid-sentence. The clause is gone and the test keeps the naming property it really guards: a `close` on a socket that never opened must not read as "disconnected". And the shape cannot return — `outlastTheWatchdog` now asks the page `timesArmed() > 0` before jumping, so a watchdog denial re-added to that path FAILS BY NAME. It asks *ever armed* rather than *armed now* on purpose: by the time the disconnect test jumps, a correct page has already disarmed, which is the property, so the obvious spelling would have denied the passing case. Building the guard is what surfaced that distinction; reading had not. FINDING 2. The spec kept its own `HANDSHAKE_TIMEOUT_MS` beside a comment saying a silent disagreement "would pass for the wrong reason". It did: the spec at 1000 against the page's 8000 made the suite green over the reverted routing, because the jump landed inside the window. The constant is now exported from `web/shell.js` and imported by both, so the disagreement is unconstructible. Shrinking it in its one home still catches the routing revert — that is the demonstration the guard did not go quiet. Filed, not fixed: improvise-78v6 (#61 also deleted the help-overlay spec and nothing replaced it — a PR about a dropped guard that did not diff the dropping PR's test names) and improvise-5ray (--check-evidence collects only `### ` and resolves only Rust fns, so every browser entry's `####` is invisible; appending one that names nothing exits 0, which is how the dangling heading cleared the gate for a release). Verified on the merged tree: node 26/26, bundle 3/3, Playwright 17 passed + 3 skipped, cargo test --workspace 1378 passed, comment-runs clean, --check-evidence 501 resolving, all cited ids resolve, no .beads diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH |
||
|
|
9863d96eef |
fix(harness): restore the close-routing guard PR #61 dropped, on a faked clock
Adversarial review of PR #61 found one real coverage loss, and it is the defect class that PR's own body names: an assertion that survives the deletion of the thing it was about. `shell.spec.js` lost "a socket that opened and then dropped reads as disconnected, and stays that way" and both MutationObserver negative assertions. Reverting PR #21's routing half in `web/app.js` — the socket `error` and `close` arms calling `setShell` instead of `fail` — left the node suite 26/26 and Playwright 13/13 green. `shell.test.js` cannot cover it, and why is the whole boundary argument: "a loud failure DISARMS the watchdog" tests `createShell` in isolation and cannot see whether `app.js` ROUTES the close event through `fail()`. That routing is the half gotcha 48 says keeps getting lost; this was the third time, after two earlier mis-confirmations that simulated the `opened` flag and not the routing. Both tests are back under their original names, so the two `####` headings in context/test-evidence/clients.md name live tests again. They cost ~75ms each rather than ~9s, because `page.clock` fakes the PAGE's timers: the page, the socket, the listeners and the `setTimeout` call are all real and only the passage of time is simulated. That is not a weakening, and the injections are the proof in both directions — if `runFor` did not advance the page's timers, a broken routing could not surface the watchdog's message and they would pass: - both arms reverted to `setShell` -> "…and stays that way" fails - close arm only -> "…and stays that way" fails - close arm drops the `opened` flag -> "…the watchdog stays out of it" fails Three injections, three outcomes, none of them the unit test. The helper guards its own premise, because it turned out not to: `runFor` without `clock.install()` does nothing AND throws nothing, so a test that forgot the install asserted "the watchdog never spoke" against a watchdog that never ran — 6/6 green. `outlastTheWatchdog` now requires the page's own `Date.now()` to have moved, and that omission fails by name. Also: `render_surface.test.js` matched `model.<field>` over source INCLUDING comments, so un-painting a field and mentioning it in a comment passed — demonstrated against the pre-fix version, then fixed and re-demonstrated. And the PR's speed claim is corrected where it is documented. The eleven deleted per-widget tests cost 3.3s between them against 3.4s for the three contract tests replacing them: they were free. Essentially all of 36.9s -> 15.5s was two real-timer waits, one of them this guard. The wait was the cost, not the test. Filed, not fixed: improvise-igxp (the help overlay's rows are asserted as present, never as content — inherited verbatim from the test contract.spec.js replaced) and improvise-4ktt (the js job's bundle-freedom is a shell-glob convention, not a gate). The `####`-vs-`###` blind spot in --check-evidence, which is why the dangling heading survived the gate, is a comment on improvise-r0qa. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
9838fc0211 |
test(harness): the help overlay follows the app to /app/ (#47)
The merge of `gh/main` was clean and that is the hazard: #47 moved the app from the site root to `/app/` and rewrote all six `goto` calls, while my help test — added on a branch that did not exist when #47 was written — still navigated to `/index.html`, which is now the landing page. Git had no reason to call that a conflict. Also folds in a cosmetic fix held back from the previous push: `step` had two `── panels ──` section headers after `cycle-panel-focus` landed in its own block. One block now. Verified on the merged tree: 1324 tests, clippy, fmt, both comment-run modes, wasm for all eight gated crates, and 7/7 Chromium shell tests against the assembled site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
82ef93d4cb | Merge remote-tracking branch 'gh/main' into wave/h-shared-frontend | ||
|
|
414d02c4a6 |
feat(protocol): one home for UI strings (improvise-x7sw, ktv1, k099)
Three tickets asked one question — where do UI strings live relative to a
serde wire enum — and answering it three times is the disease this unit exists
to cure. One answer, in `improvise-protocol/src/help.rs`:
**A UI string is keyed by a command name, never by a key, and never hangs off
the enum that crosses the wire.** `AppMode::label`/`hint_text` are gone;
`help::mode_label`/`help::hint_line` replace them, so the enum a client
deserializes carries no English at all (improvise-k099). **And wherever a
string names a key, the key is looked up rather than spelled**:
`Hint { keys, command, caption }` takes `keys` off `Keymaps::default_keymaps()`,
`command` is the semantic value a localized front end keys off, and `caption`
is the default English — the `AxisColor`/`axis_fg` shape.
- **improvise-x7sw**: `help::which_key` is the which-key popup's rows, keys and
captions together. The client already holds the keymaps, which is the answer
the ticket preferred, so `ViewState::transient_prefix` stays an address.
- **improvise-ktv1**: the help pages are data — `help::pages()` — with every
key row derived. `src/ui/help.rs` keeps its ratatui styles and loses ~310
lines of content; `web/app.js` paints rows it does not author, and
`harness/tests/shell.spec.js` asserts that `?` really draws them.
**What this deletes is the measure of it.**
`every_hint_key_resolves_in_its_mode` and its 60-line hint tokenizer are gone:
they existed to hold a hand-written string honest against the keymap, and the
string is derived now, so the property is true by construction
(`doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)`).
`Keymap::binding_hints` goes with them — `which_key` is the one walker, and the
binding dump reads it. What replaced the test checks the *other* direction, a
mode advertising a command nothing binds, which was never checkable before.
Two defects the derivation exposed, both fixed here: `key_label` dropped
modifiers, so Ctrl-arrows read as bare arrows, and `Char(' ')` printed a literal
space instead of `Space`.
**What is deliberately not derived**, and says so: the import wizard's keys
(its whole keymap is one `Any` binding into `HandleWizardKey`, which decides
per step inside the command — a real hole, left visible), `gg`/`yy`/`ZZ` and
the ex-commands (`HelpLine::Literal`), and the captions, which no test can
judge. `context/code/protocol.md` §8 has the argument.
Cost, stated: the hints changed shape. They are longer, they name arrow keys
beside `hjkl`, and two sub-lists on the Panels page lost their visual indent.
That is the price of the keys being true.
Evidence: 1321 tests pass, and the 7 Chromium shell tests pass against the
assembled site. Two faults were injected into the help overlay — dropping the
`paintHelp` call, and removing the `mode != Help` guard so it is never hidden —
and each fails the new spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
2ccd21f3e8 |
docs: the landing page is the front door, the app moves to /app/
User ruling on improvise-kh8, overturning the arrangement the previous commit
shipped: "The landing page should be served at the root of the repository and
link to the in-browser demo." The argument for the other arrangement is left
standing in the ticket rather than deleted — it is overruled, not wrong to have
considered — and the root is now `docs/index.html`.
The app moves under `app/` WHOLE, both bundle directories included. Every path
in `app.js` and `worker.js` is relative, so moving the bundle together keeps
every one of them correct and the wasm-pack generated glue untouched;
`worker.js`'s `fetch("demo.improv")` resolves against the worker's own URL, so
the seed follows for the same reason. The four `stamped` guards in
`assemble.sh` follow the files: they are the only check that the cache-bust
substitution worked, and a guard aimed at a path nothing writes fails in the
direction that looks like success.
`harness/tests/shell.spec.js` navigates to `/index.html` six times, including
the `?ws=` and `?doc=` variants, and every one of those now lands on the
landing page. All six are repointed at `/app/index.html`. That file is outside
this unit's declared scope and is the second deliberate stray in this PR.
VERIFIED AGAINST THE ASSEMBLED TREE, not against `docs/` directly, because a
path move is what re-arms the trap this page already caught once. With real
bundles from `web/build.sh`:
* `npx playwright test tests/shell.spec.js --project=chromium` — 6 passed,
with `/app/{index.html,app.js,style.css,worker.js,demo.improv}` and both
`?v=` stamped bundles all served 200;
* the four casts play 37 lines each at the site root, no failed request and
no page error;
* the full journey: load the root, click the call to action, land on `/app/`,
and the grid draws out of the in-page worker — 12 cells, all editable,
a "Revenue" header, `#status` reading "in-page worker · demo.improv".
The call-to-action check was proved able to fail by pointing the link at
`./aap/`: 404, while the page still looks entirely correct.
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> |
||
|
|
6f9efcf2e8 |
docs: improvise-xps8 describes a bug that does not exist — turn it into a guard
Review findings on PR #30, all three applied. 1 [P1]. The claim that the handshake watchdog overwrites a genuine mid-session disconnect 8s later is FALSE on any tree that has PR #21. `close` routes BOTH arms through `setShellFailure` (app.js:248-250), which clears the timer synchronously (app.js:165-169). It was believed because it was measured honestly — against a tree predating PR #21 — and then reported as "surviving" that PR on the strength of a simulation that re-created only the `opened` flag and not the other half of the fix, the routing. A simulated fix that is not the fix you named reproduces a bug the real code does not have, and reads exactly like a confirmation. Rather than delete the claim, execute the property it denied: the open-then-dropped test now runs past HANDSHAKE_TIMEOUT_MS + 2s and asserts "sent no document" never appears. Proved discriminating by removing `setShellFailure`'s `clearTimeout` — the assertion then fails with exactly the message xps8 described. gotcha 48 and the spec's comment block corrected; the ticket is cited in neither. Recorded as doctrine(a_simulated_fix_must_be_the_fix_you_named), the sibling of an_injected_fault_must_be_the_fault_you_named that fails the other way: a wrong fault proves a check that cannot fire, a wrong simulated fix proves a bug that does not exist. 2 [P1]. The previous commit created the contradiction it was meant to remove: design-principles recorded the measured ~2.2s while kb.pl still said "under 2 seconds". The ambiguity between per-binary and wall clock is what let it drift, so both now state two numbers — no single binary over 2s, wall clock is the sum, measured 1309 tests / 33 binaries / ~2.2s / longest 1.42s — in kb.pl, design-principles and repo-map alike. Also 16 -> 17 in doctrine(binding_dump_diff). 3 [P2]. The new audit exclusion was unanchored, so `improvise-gp` was a prefix match that would silently exclude a real future ticket. Anchored `^…$`. It immediately caught the example id the explanatory paragraph itself spelled out — a recipe is code, twice over. CLAUDE.md: declined, with evidence. Both copies of the duplicated tail sit inside `<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->` markers written by `bd setup claude` and `bd setup codex` and refreshed against that hash. A process change there is one tool invocation, not two hand edits, and a pointer makes `bd setup <recipe> --check` report it modified. Recorded as kb_issue_found(claude_md_tail_is_generated_not_duplicated) so it is not re-proposed. kb.pl: doctrine 54->55, kb_issue_found 11->12; invariant 51, decision 47, location 34 unchanged. kb_consistent passes. cargo test --workspace: 1309 passed, 0 failed, 2 ignored. harness (Playwright): 12 passed, 3 skipped (webkit has no OPFS). Dead-ID audit with the anchored recipe: 157 tokens, all resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f12a2d3517 |
docs: reconcile the docs with the code after wave 1
An independent review checked every factual claim in the docs against the code at |
||
|
|
4abadb8e6d |
test(harness): drop two expected-failures whose trigger is platform-dependent
An expected-failure is green when the bug is present and red when it is
absent. One whose trigger depends on the host therefore does not merely
flake — it **inverts CI's meaning**. These two failed the browser job twice
with "expected to fail, but passed", and the second time was *after* the
determinism work in the previous commit, which is the evidence that the
problem is not the assertions' precision.
Neither bug reproduces on the GitHub runner:
- improvise-m18f, where the unconditional `close` listener overwrites
"cannot reach …" with "disconnected" — on the runner `close` does not
follow `error` inside the window, even against a server that accepts the
TCP connection and then refuses the upgrade.
- The handshake watchdog replacing a mid-session "disconnected" with
"accepted the connection but sent no document" — on the runner it does not
land, though the page demonstrably opened and then dropped, since both are
asserted as setup.
The trigger for both is the **order and timing of `error`, `close` and a
timer on the host's network stack**. That is not a property of `web/app.js`
and not something this harness can pin, so pretending otherwise was the
mistake. Both are real, both were measured on macOS/Chromium, and the
measurement is recorded in the file with the exact status sequences so the
next person does not re-derive it — including the fact that the watchdog one
**survives** the m18f fix, because that fix is about which message `close`
writes rather than about the timer still running.
What remains is the portable half, and it is the half that was doing the
work: every message the page must write ("bad frame", "cannot reach",
"connected"/"disconnected", "sent no document"), the one case where the
watchdog must stay silent, and the two standalone-deployment tests. Filed
for the orchestrator; `web/app.js` is not this unit's file.
12 passing, 3 skipped (WebKit has no OPFS), 23s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
affb9a1812 |
fix(harness): make the two expected-failures deterministic, and unstale 4 docs
The browser job **failed in CI** on the previous head while passing locally, with both `test.fail` tests reporting "expected to fail, but passed". That is the worst possible outcome for an expected-failure: it is a check that is green when the bug is present and red when it is absent, so a flaky one inverts CI's meaning rather than merely being noisy. Copilot independently flagged the same test from a different direction on the same head. Two timing assumptions, both wrong off my machine: **A closed port is not a deterministic "refused before open".** Port 9's `close` timing is a property of the host's network stack, and on the GitHub runner it did not arrive inside the window, so `#status` still read "cannot reach" and the assertion passed. Replaced with the harness's **own http server**: it accepts the TCP connection and then fails the WebSocket upgrade, so `error` and `close` both fire promptly, everywhere. The page cannot tell the two apart and neither can `web/app.js` — in both cases the socket never opened. **A timer does not order a close after an open.** The watchdog test's mock closed 300ms in; on a slow host that can land before the page sees `open`, in which case `armWatchdog` never runs and the test passes having exercised nothing. Both mocks now close on the page's **first frame**, which the page sends only after `transport.ready` resolves — so "opened, then dropped" is true by construction rather than by luck. And the setup assertions now assert the **precondition of the bug** rather than that something happened: the m18f test polls the recorded `#status` history for "cannot reach" before enabling `test.fail`, because a bare non-empty check is already satisfied by "connecting to …" (Copilot's exact point). Verified three ways: `--repeat-each=3` green; breaking `paintStatus` gives 2 real failures, not expected ones; simulating PR #21's fix still flips the m18f guard to "expected to fail, but passed". **Four checked-in explanations this branch made false**, also Copilot's, and gotcha 48's third rule for the second time in this PR — when a fact is stated in more than one place, changing it is a grep, not an edit: - `worker-server/src/lib.rs` said `storage_contract` "has still never been run" against `OpfsFs` and that browser verification is deferred. - `bindings.rs` said it is "the only part of this crate that no test in the repository can reach". Still true of the shim, for a narrower reason now worth stating: the harness drives `OpfsFs` directly and never instantiates it. - `vfs.rs::holds_a_document` and its test both said `attach` indexes the zero-byte entry it creates. It no longer does, which is the whole of the fix three commits ago — so the check is now belt and braces rather than the mechanism, and says so. `web/app.js` and `web/worker.js` carry the same stale "untested JavaScript" claim and are **not this unit's files**; reported for the orchestrator rather than edited across the boundary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3c36a3b57f |
test(harness): Firefox runs the contract too, and test.fail stops hiding setup
Two Copilot findings on PR #24, both correct, both applied. **`test.fail` was masking setup failures.** Applied before navigation, it made any earlier failure — the page not loading, the script not starting, `#status` never updating — an *expected* failure, so CI stayed green through it. That is precisely the check-the-checker defect these tests exist to catch, wearing the name of its cure. Both tests now assert their setup first and apply `test.fail` immediately before the one assertion the known bug owns. Verified by breaking the setup (making `paintStatus` a no-op): before, 2 passed; after, **2 failed**. **Firefox now runs the whole battery, and it passes.** improvise-xf1's acceptance criterion names Chrome, Firefox and Safari, and the reason is real rather than ceremonial: `createSyncAccessHandle` is the one API this design rests on (gotcha 28) and each engine implemented it separately, so Chromium alone was one engine's opinion. `storage_contract` against `OpfsFs` now passes on **two** engines. **Safari is not covered, and the criterion is met for two of three.** Playwright's WebKit build provides no OPFS at all — `navigator.storage.getDirectory()` itself rejects with `UnknownError` — so there is nothing there for `OpfsFs` to be right or wrong about. Rather than delete the project and quietly narrow the claim, it is kept and the spec skips on a **runtime probe**: the day that build gains OPFS the checks start running with no CI change. The skip is conditional on the probe and not on the browser name, and an unexpected skip on any other engine **fails** with "that is a regression, not a known gap" — a silently skipped check being the failure this harness exists to prevent. The shell spec stays Chromium-only on purpose: it is about `web/app.js`'s own logic, which is engine-independent, and running `routeWebSocket` plus the worker handshake three times is slower and flakier to no benefit. 14 passing, 3 skipped, 36s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f90d1757be |
test(harness): the four assertions the close-listener fix owes, and a third bug
The harness is the one place a line of `web/*.js` is executed, not only the one place `storage_contract` reaches `OpfsFs`. PR #21 declined Copilot's request for regression tests on its close-listener fix because there was no JavaScript test infrastructure in this repository at all — no `package.json`, no runner, nothing in CI running a line of `web/app.js` — and standing up a parallel one beside this harness would have been two answers to one question. That decline is only honest if the assertions land here, so here they are, and `playwright.config.js` now says out loud that a change to `web/*.js` belongs in this spec directory. Playwright rather than a headless-DOM runner, because what goes wrong in these files is a real Worker, a real WebSocket, real OPFS and real event ordering — none of which a DOM shim has. `close` fires on a connection that never opened and on one that was established and dropped, and nothing on the event tells them apart: `readyState` is already CLOSED in any close listener, and both arrive as 1006 with `wasClean: false`. So: 1. **close-before-open must leave "cannot reach" standing.** `test.fail`, because the close handler on this branch is unconditional and overwrites it (improvise-m18f). That IS the regression guard, and it is aimed at the implementation actually here rather than at a mutant invented for the occasion — against an unconditional handler, "always say cannot reach" would pass and prove nothing. Verified by simulating PR #21's `opened` flag in the served copy: the test passes and Playwright reports "expected to fail, but passed", so the fix cannot land silently either. 2. **open-then-close must read "disconnected".** Green under both implementations — verified under the simulated fix too, since this is the arm that makes suppressing `close` wrong. 3. **The watchdog must stay out of the never-opened path.** Green, but for a reason its own comment does not lead you to expect: `armWatchdog` runs only after `transport.ready` resolves, which happens on `open`, so on a socket that never opened the watchdog is never armed at all. Asserted on the same page load as the "cannot reach" check, since both need the same 9-second wait. 4. **The watchdog must not overwrite a mid-session disconnect.** It does. Found by measuring rather than predicted, and it is a **third** bug: on the open-then-close arm the watchdog is armed, nothing disarms it, and 8s later "disconnected" is replaced by "accepted the connection but sent no document" — which is false, the peer connected and then went away. Same class as the bug `setShellFailure` fixed (a watchdog must be disarmed, not pre-empted) and it **survives** improvise-m18f's fix, which is about which message `close` writes rather than about the timer still running — confirmed under the simulated fix, where this one still fails. `test.fail` and filed; `web/app.js` is not this unit's file. Measured before asserting, which is how 3 and 4 came out opposite to the expectation they were handed: never-opened: connecting… -> cannot reach -> disconnected open-then-close: connecting… -> connected -> disconnected -> accepted the connection but sent no document 11 passing in the harness, 32s. The two `test.fail` entries are expected failures, so the suite is green and both bugs are pinned in a form that goes red the moment either is fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5bfa747942 |
fix(assemble): verify each cache-bust stamp on its own, and rename a test
Two Copilot findings on PR #24, one applied as asked and one applied only in part, with the measurement that says why. **The combined `grep` was a real hole.** `grep -n "?v=" a b c d` succeeds when *any one* file matches, so under `set -e` three failed substitutions still produced a "successful" assembly. Those greps are the only guard against the generated wasm-pack glue changing shape — which is the exact failure this cache-busting exists to end, twice, during improvise-9x6 — so each stamped reference is now checked on its own and names itself when it is the one missing. Inherited from the inline CI step rather than written here, but it became mine when it became a script. Proved by pointing one `sed` at a pattern that cannot match: the old form exited 0, the new one exits 1 naming the file. **The repaint count was the right objection to the wrong remedy.** Copilot asked for "count the relevant grid mutations/repaints and prove the assertion by removing the guard". Done, and it does not discriminate: with `if (closed) return` removed from `edit`'s `close`, `close` still runs exactly twice, `typed_cell` is still called exactly once, `#grid` still takes exactly two childList mutations, and the final text is identical. The only difference is one extra *entry* into `repaint()` that produces no DOM change — because the second close is `close(false)`, so it does not re-send, and everything else it does is idempotent. Which is exactly what improvise-hwpi meant by "harmless before, but only by accident". Counting `repaint()` entries would need a test hook inside `web/app.js`: a page that knows it is being tested. So the half of the finding that stands is the **name**, which claimed more than the body checked — the same defect as `open_and_refresh_see_an_empty_origin_root` two commits ago. Renamed to `committing an edit sends one frame and shows the host's value`, and the measurement is recorded in the comment so nobody repeats it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7e991360df |
test(harness): a headless browser, and the first execution of OpfsFs
improvise-vvl3, absorbing improvise-xf1. The condition the user set when
deferring browser testing — "WE HAVE TO HAVE SOMETHING TO TEST" — has been
met: after improvise-9x6, djm, hwpi and the demo model there is a complete
self-contained application whose entire untested surface is what a harness
covers.
`python3 -m http.server` plus one Playwright project, in `harness/`:
harness/opfs-contract/ a wasm crate that runs
`improvise_io::storage::contract::storage_contract`
against `OpfsFs` — the shared function, not a
transcription of it
harness/www/opfs/ the page and the dedicated Web Worker it runs in;
`createSyncAccessHandle` exists nowhere else
harness/tests/ two specs
harness/build.sh site + harness bundle -> harness/serve
It lives at the repo root rather than under `web/`, because `web/` is the
*source of what ships* and every file in it is copied to the site. The
harness serves the **assembled** tree (`web/assemble.sh`, extracted from
the CI workflow in this commit so there is one answer to "how is the site
arranged" rather than two) plus its own `/opfs/` — a harness that runs
against a different arrangement of files is testing something nobody
deploys. `harness/opfs-contract` is deliberately not a workspace member: it
only builds for wasm32, so a member would break `cargo test --workspace`.
What this runs that nothing has ever run:
- `storage_contract` against `OpfsFs`, at `""` **and** at `"docs"`. Two
bugs fell straight out — improvise-vhcz's `list` path shape, whose fix
the `"docs"` run guards (reverting it fails with `lists the one
document`), and the zero-byte entry fix in the previous commit, which
this found by failing on `starts empty`.
- The `keys()` async-iterator walk in `refresh`, hand-rolled through
`js_sys::Reflect` and named by xf1 as the least certain code in the file.
Three entries written out of order, so one passing by accident cannot
carry it.
- `FileSystemReadWriteOptions`' `at` on read and write: a long write then a
short one, which is where an append or a non-zero start shows up.
- `commit_deletes`, both halves — queued but still in OPFS before, gone
after — and **across a reload**, which is the only way to ask whether
OPFS persisted. That reload is also improvise-bck's persistence criterion
at the storage layer.
- improvise-hwpi's three one-assertion shell tests, which that ticket said
it could not write: status after a bad frame, status with no peer, one
repaint on commit. Plus the first-frame watchdog, which was found by
reading and only because someone went looking.
Check the checker, throughout, because a harness that passes against a stub
— or that silently skips because the worker never started — is exactly this
repo's characteristic defect wearing a green tick:
- `run_canary` deliberately fails, through the *same* reporting path every
real check uses, and the spec asserts it is reported as a failure with its
message intact.
- The spec asserts the report contains exactly the expected check names, in
order, so a battery that quietly shrinks is a failure.
- Every shell assertion was injected and watched to fail: removing the
bad-frame catch, the watchdog's `setTimeout`, the socket `error` listener
and the `transport.send(frame)` on commit each fails its own test and
only its own.
- The commit test types `20000` and asserts `20,000`, so a page that echoed
what was typed instead of drawing what the host returned would fail.
One live bug is pinned rather than fixed. `a peer that cannot be reached`
asserts the *history* of `#status`, because improvise-m18f is real and
reproduced here: the socket's `close` listener overwrites the precise
"cannot reach" with "disconnected". That is filed and deliberately not the
same fix as the watchdog's (gotcha 48), so the test asserts both halves and
will speak up whichever one moves.
CI gets a `browser` job that can fail the build, and `pages-deploy` now
needs it. Playwright's Chromium download is ~80 MB and well under a minute
on ubuntu-latest, next to the two wasm-pack builds the job already needs —
so "impractical in this CI" does not arise. A harness nobody runs is worse
than none, because it reads as coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|