Commit Graph

7 Commits

Author SHA1 Message Date
Edward Langley 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>
2026-09-20 13:33:49 -07:00
Edward Langley 955c30a864 fix(web): land the review findings main is missing
Repair-forward for the accidental push at 959f249, which put the PRE-review
version of the browser-widget work onto main. The three defects review found
are all in the version main currently carries; this is the delta that
supersedes it.

1. `categoryList` did not call `indicator()`. It inlined U+25B8/U+25BE, the
   SMALL triangles, where `indicator()` — the page's declared ONE spelling —
   the page's own grid group headers, and src/ui/category_panel.rs all draw
   U+25B6/U+25BC. The browser's category panel disagreed with the TUI's and
   with its own grid, inside the file whose comment says "only this spelling
   is the page's".

2. Two specs pinned the divergent pair, so the obvious cure would have failed
   tests that shipped with the feature. widgets.spec.js AND pointer.spec.js —
   the latter written on a different branch, which had grown the same wrong
   assertions independently. Three files converging on a spelling nothing
   compares is design-principles §5's predicted failure, observed. Proved
   discriminating by running the corrected specs against the pre-fix bundle,
   where they fail with `Received string: "▸ Product (4) R"`.

   The pointer.spec.js line is in this PR and not a later one because main
   already carries that file: without it this change leaves CI red. It is one
   assertion corrected by the same rule as its sibling, not feature work.

3. `el.tiles.hidden` was a no-op, removed with its false comment. `#tiles`
   declares `display: flex`, an author-origin rule that beats the UA sheet's
   `[hidden] { display: none }`, so the guard never hid anything. Worth
   knowing before restoring it: `#tiles` being ALWAYS laid out is why its
   height never changes and it stays out of the ResizeObserver feedback path.

Also `playwright.config.js` takes IMPROVISE_HARNESS_PORT (default 8099, so CI
is unchanged). The hard-coded port plus `reuseExistingServer` let Playwright
silently ADOPT a server left by another worktree and test THAT tree. It fired
in both directions in one session: a false negative (a fix reported absent)
and — the dangerous half — a false POSITIVE, 5 passed against a tree verified
to contain no #tiles at all. A failure sends you looking; a false pass prompts
nobody.

The evidence prose records all of it, plus the ResizeObserver acyclicity proof
that retires "argued, not tested" there: frameCost.cols is row header LEVELS
(independent of projected rows) and frameCost.rows derives from the column
window, so cols -> rows and nothing depends on rows. No cycle to oscillate.

Gates: 1359 Rust tests, rustfmt, comment-run and evidence linters, 21/21
Chromium harness (on a private port, so not another checkout's tree).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:03:14 -07:00
Edward Langley 757abf3b88 docs: REVERT the mobile fix (user ruling), caption the stale drill cast
Read the revert first, because this commit undoes the headline change of the
one before it and a subject line saying "caption" would have hidden that.

**REVERTED: 3fcb88b's `@media (max-width: 900px)` block.** User ruling: the
mobile defect is to be FILED, not fixed in this PR. It is improvise-o31u, P2,
carrying the measurement, the proposed fix (`overflow-x: auto` on `.player`
plus `fit: false` or an explicit `terminalFontSize`), and the note that a
working fix was written and measured here before being reverted, so the next
person starts from evidence. Measured both ways at 390x844, so the cost of the
revert is on the record rather than implied:

    3fcb88b as committed   font 11.9457px   player width 880
    this commit             font  4.83258px  player width 356

`document.documentElement.scrollWidth` is 390 and equal to `clientWidth` in
both, so the revert does not introduce horizontal page scroll — it restores an
unreadable font, and nothing else.

**F1, the stale drill cast, captioned rather than re-recorded or dropped** —
also a user ruling. `drill.cast` shows the narrow records format with a single
`Value` column, which improvise-2zq and improvise-c5v replaced with a column
per measure plus the `+cat` and `+` columns, so it demos a UI the software no
longer has, now at the site root. The caption is `p.dated`: body size, body
colour, set off with a rule, and ABOVE the player rather than under it —
verified by measuring its computed style against `body` and its box against the
player's, because "captioned" and "captioned in grey 10px under the fold" are
the same word and different things. improvise-2oxt stays open; improvise-e61
stays closed, with its notes now saying plainly that the caption is an honest
label on an UNMET criterion rather than a met one.

**F7: the harness readiness probe** was `/index.html`, which used to be the app
and is now the landing page — so it would answer 200 against a tree with no
bundle assembled at all. Now `/app/index.html`. Proved discriminating rather
than assumed: served a directory containing only the landing page, and the old
probe returns 200 while the new one returns 404.

`docs/index.html` is now 213 lines, past improvise-e61's "under 200". The
criterion is reinterpreted on the ticket rather than the file crammed to fit:
the number was a proxy for single file, vanilla, no build step, no framework,
and all four hold.

Also corrected, in the record rather than the tree: improvise-bck's evidence
cited the deployed root for its page-load and bundle-size measurements, and
this PR moved the app out from under those URLs.

Verified: 6/6 harness shell specs pass with the new probe, four casts still
play 37 lines each at the root, call to action still 200, cargo test 1311,
both comment checkers clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:23:57 -07:00
Edward Langley 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>
2026-09-20 10:02:03 -07:00
Edward Langley 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>
2026-09-19 23:23:44 -07:00
Edward Langley 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>
2026-09-19 23:14:16 -07:00
Edward Langley 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>
2026-09-19 22:45:03 -07:00