spaces: the drags, the pinch and the menus a phone could not reach #915
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
| # CI validation gates — validate, never release. | |
| # | |
| # Every PR and every push to main runs: typecheck, the single-file build, | |
| # the splice conformance gate (scripts/shell-gate.mjs — same code the local | |
| # release runs before signing), and the CRDT convergence rig at CI depth. | |
| # | |
| # Deliberately absent: signing, publishing, deploying. Releases are cut | |
| # locally so the signing key never leaves the maintainer's machine and the | |
| # signed bytes are the served bytes (docs/RELEASING.md). Keep it that way — | |
| # do not add secrets or release steps to this workflow. | |
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| concurrency: | |
| # PR branches share one group per ref, so a new push collapses onto the tip — | |
| # only the tip matters there. Pushes to main get a group PER COMMIT, because | |
| # every merge commit is a distinct state and the post-merge run is the safety | |
| # net for interaction bugs between PRs merged close together. | |
| # | |
| # Per-commit, NOT merely cancel-in-progress:false — that setting alone does | |
| # not queue every run. GitHub keeps at most ONE pending run per group, so a | |
| # third push cancels the PENDING one instead of lining up behind it; it | |
| # protects the run in progress, not the queue. Merging 19 PRs in a row that | |
| # way cancelled 17 runs on main and left those merge commits unbuilt, and the | |
| # "four merges in a few minutes" incident this setting was meant to fix was | |
| # the same failure. A unique group per commit is what actually guarantees it. | |
| group: ci-${{ github.workflow }}-${{ github.ref }}${{ github.ref == 'refs/heads/main' && format('-{0}', github.sha) || '' }} | |
| # Moot on main now — each commit is alone in its group — but kept truthful: | |
| # superseded PR-branch runs still collapse. | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| # ≥23.6 required: the sync rig runs TypeScript via native type | |
| # stripping (node scripts/test-sync.ts). | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: | | |
| slides/package-lock.json | |
| spaces/package-lock.json | |
| dash/package-lock.json | |
| type/package-lock.json | |
| - name: Install | |
| working-directory: slides | |
| run: npm ci | |
| - name: Which targets anim treats as elements | |
| # The element/plain-object split ran on HTMLElement||SVGElement, which | |
| # excludes MathML — formula symbols became plain-object tweens, with | |
| # opacity assigned as a JS property and no style ever written. | |
| run: node scripts/test-anim-elements.ts | |
| - name: Transforms are not silently ignored | |
| # A CSS transform does nothing to a non-replaced inline element: it is | |
| # accepted, style.transform reads back verbatim, and the box never | |
| # moves. An animation on one reports perfect health and paints nothing, | |
| # which is exactly how it survived eight rounds of debugging once. | |
| run: node scripts/test-anim-inline.ts | |
| - name: i18n packed table is current | |
| # The per-locale catalogs are the source of truth; slides/src/i18n/ | |
| # packed.ts is generated from them and is what actually ships. A stale | |
| # packed.ts would silently ship yesterday's translations. | |
| run: node scripts/build-i18n.mjs --check | |
| - name: Language coverage | |
| # Core catalogs ship inside every saved file and cannot be corrected | |
| # without a release, so they must be complete. Packs are downloadable | |
| # and re-publishable on their own, so they get a floor and a report. | |
| run: node scripts/test-i18n-coverage.mjs | |
| - name: Language packs build and index cleanly | |
| # Emits every pack and builds the index payload WITHOUT signing (--dry | |
| # writes nothing and needs no key — signing stays local, see the header). | |
| # Catches a malformed pack catalog (missing label, duplicate language, | |
| # wrong app) here rather than mid-release. | |
| run: | | |
| node scripts/build-i18n.mjs --packs "$RUNNER_TEMP/packs" | |
| node scripts/sign-packs.mjs "$RUNNER_TEMP/packs" --dry | |
| - name: Typecheck | |
| working-directory: slides | |
| run: node_modules/.bin/tsc -b | |
| - name: Typecheck kernel (standalone) | |
| # Apps also typecheck kernel transitively through their imports; this | |
| # catches kernel-only errors under the kernel's own flags. | |
| working-directory: slides | |
| run: node_modules/.bin/tsc -p ../kernel | |
| - name: Build single-file shell | |
| working-directory: slides | |
| run: npm run build:single | |
| - name: Splice conformance gate | |
| run: node scripts/shell-gate.mjs slides/dist-single/Bento_Slides.bento.html | |
| # spaces: a second app proving the kernel seam — same pipeline, same gate. | |
| - name: Install (spaces) | |
| working-directory: spaces | |
| run: npm ci | |
| - name: Typecheck (spaces) | |
| working-directory: spaces | |
| run: node_modules/.bin/tsc -b | |
| - name: Build single-file shell (spaces) | |
| working-directory: spaces | |
| run: npm run build:single | |
| - name: Splice conformance gate (spaces) | |
| run: node scripts/shell-gate.mjs spaces/dist-single/Bento_Spaces.bento.html | |
| - name: Spaces i18n packed table is current | |
| # The per-locale catalogs are the source of truth; packed.ts is | |
| # generated and is what ships inside every saved file. A core catalog | |
| # cannot be corrected without a release, so an incomplete one fails the | |
| # build rather than shipping a half-translated interface. | |
| run: node scripts/build-spaces-i18n.mjs --check | |
| - name: Spaces invite rig | |
| # "Invite someone…" was `saveAs('copy')`, which serializes store.doc — | |
| # so everyone invited to a space received `collab.ownerPriv`, the room's | |
| # ROOT key: they could write, and they could revoke the person who | |
| # invited them. Nothing about the copy looked wrong. This runs the real | |
| # share functions against real WebCrypto keys and asserts on the bytes: | |
| # the owner private key appears NOWHERE in an invite, the invite is a | |
| # separate keypair that the owner actually signed (verified the way the | |
| # relay verifies it), and a view-only copy holds no signing key at all. | |
| # (Bundled: the kernel transport uses parameter properties, which node's | |
| # strip-only TypeScript loader refuses.) | |
| run: | | |
| slides/node_modules/.bin/esbuild scripts/test-spaces-invite.ts --bundle --platform=node --format=esm \ | |
| --outfile="$RUNNER_TEMP/test-spaces-invite.mjs" | |
| node "$RUNNER_TEMP/test-spaces-invite.mjs" | |
| - name: Spaces undo rig | |
| # Every checkpoint used to stringify the WHOLE document: measured on a | |
| # 200-page, 2.5 MB handbook, fifty edits left an undo depth of NINE, | |
| # because nine snapshots exhaust the 24 MB budget. A typing run now | |
| # records only its PAGE. That is fast and deep — and it is only correct | |
| # while the mutation touched nothing else, so `doc` stays the default | |
| # and this rig covers the cases where a wrong scope silently loses | |
| # work: mixed scopes, interleaved undo/redo, and two pages edited in | |
| # turn. (Bundled: store.ts imports './model' extensionless.) | |
| run: | | |
| slides/node_modules/.bin/esbuild scripts/test-spaces-undo.ts --bundle --platform=node --format=esm \ | |
| --outfile="$RUNNER_TEMP/test-spaces-undo.mjs" | |
| node "$RUNNER_TEMP/test-spaces-undo.mjs" | |
| - name: Spaces agent-surface rig | |
| # agent.ts is ~900 lines and shipped with no coverage at all: two of the | |
| # five defects an adversarial review found in round 2 lived here, | |
| # including a walk over author-supplied parent links that never | |
| # returned. What this pins is what makes the surface trustworthy — | |
| # validate() SILENT on documents that are correct but unusual (a false | |
| # positive teaches an agent to ignore warnings), every write verb | |
| # refusing what it cannot honour, no verb producing a document the app | |
| # itself could not produce, and every walk terminating. The termination | |
| # probes run in child processes with timeouts, because a hang cannot be | |
| # reported by the process it hangs. | |
| run: node scripts/test-spaces-agent.ts | |
| - name: Spaces journal rig (several timezones) | |
| # Every part of a daily note that can be wrong is wrong QUIETLY and on | |
| # someone else's machine: "today" in UTC is the wrong day for hours at | |
| # a time outside Greenwich, adding 86,400,000 ms is not adding a day on | |
| # the two DST boundaries, and `new Date('2026-08-06')` is UTC midnight | |
| # by spec. A date test that only ever runs in one timezone has not been | |
| # run — so this runs in five, including a half-hour DST offset. | |
| run: | | |
| for tz in UTC Europe/Berlin America/Los_Angeles Pacific/Kiritimati Australia/Lord_Howe; do | |
| echo "--- $tz" | |
| TZ=$tz node scripts/test-spaces-journal.ts | |
| done | |
| - name: Magic notes evaluator (several timezones) | |
| # Two failure modes, and the second is what would make the feature worse | |
| # than not having it: a WRONG answer (silent, confident, in somebody's | |
| # notes), and an answer where there should be none — "Meet Ana at 3" is | |
| # a sentence. The parser must consume the whole line or return nothing. | |
| # Dates run in several timezones for the reasons journal.ts records. | |
| run: | | |
| for tz in UTC Europe/Berlin America/Los_Angeles Pacific/Kiritimati; do | |
| echo "--- $tz" | |
| TZ=$tz node scripts/test-spaces-calc.ts | |
| done | |
| - name: Spaces Markdown round-trip rig | |
| # The About dialog promises, in eight languages, that "a space is never | |
| # a dead end … every page exports as Markdown". This is that sentence, | |
| # checked: the starter space is exported and read back, and no page may | |
| # come back with materially less text than it went in with, nor may a | |
| # single block vanish entirely. Block TYPES are deliberately not | |
| # asserted — Markdown has no callout, toggle, media embed or board, so a | |
| # callout leaving as a blockquote is the format being honest, not a bug. | |
| # (Bundled: the app's imports are extensionless.) | |
| run: | | |
| slides/node_modules/.bin/esbuild scripts/test-spaces-roundtrip.ts --bundle --platform=node --format=esm \ | |
| --outfile="$RUNNER_TEMP/test-spaces-roundtrip.mjs" | |
| node "$RUNNER_TEMP/test-spaces-roundtrip.mjs" | |
| - name: Spaces model rig | |
| # Properties that fail SILENTLY, and that a shipped file cannot be | |
| # talked out of once it exists: | |
| # · the load contract — an unreadable document must never be replaced | |
| # by an empty one; | |
| # · format additivity, and id repair derived from the BYTES, so two | |
| # readers of one file agree on every id (links key on them); | |
| # · untrusted html is parsed INERT — a detached div still loads what | |
| # it creates, which made the sanitizer its own vector on the OPEN | |
| # path; | |
| # · opening a document touches no network, so a mailed space cannot | |
| # carry a tracking pixel; | |
| # · an encrypted space is never snapshotted to IndexedDB in the clear; | |
| # · find and replace quote ONE number — what is counted is what | |
| # changes. | |
| # The last four are source assertions: the behaviour needs a DOM, a | |
| # failing request, IndexedDB and a clock, but every one of those | |
| # mistakes is made in the source. | |
| run: node scripts/test-spaces-model.ts | |
| - name: spaces rig manifest | |
| # Bookkeeping, not a ninth run of the eight steps above. scripts/ | |
| # test-spaces.mjs is the local entry point for the spaces suite (two of | |
| # those rigs cannot be handed to node directly, and running one by hand | |
| # produces a stack trace that reads like a broken product); this asserts | |
| # its list is still complete in both directions — every test-spaces-* | |
| # file on disk is listed there, and every rig it lists has a step in | |
| # this workflow. Both omissions are invisible: an unlisted rig never | |
| # runs locally, an unregistered one never runs here, and neither looks | |
| # like anything but green. test-spaces.mjs itself was unregistered from | |
| # the day it was written, which is how this check came to exist. | |
| run: node scripts/test-spaces.mjs --manifest | |
| # dash: the data app — same pipeline, same gate. | |
| - name: Install (dash) | |
| working-directory: dash | |
| run: npm ci | |
| - name: Typecheck (dash) | |
| working-directory: dash | |
| run: node_modules/.bin/tsc -b | |
| - name: Build single-file shell (dash) | |
| working-directory: dash | |
| run: npm run build:single | |
| - name: Splice conformance gate (dash) | |
| run: node scripts/shell-gate.mjs dash/dist-single/Bento_Dash.bento.html | |
| - name: Release channel rehearsal (dash) | |
| # Cuts a COMPLETE release — the real release.mjs, staged into a temp | |
| # directory — with a throwaway key this step generates and discards, and | |
| # then verifies the artifacts with kernel/src/update.ts itself, the code | |
| # every shipped file runs, with only the embedded public key swapped. | |
| # The point is the REFUSALS: a tampered manifest, a manifest signed for | |
| # another app, a downgrade replay and a one-bit-flipped shell must each | |
| # be rejected, and the conformance gate must refuse a shell whose doc | |
| # block closes its own script. A signing pipeline cannot be tested by | |
| # signing (the key never reaches CI), but this is everything except. | |
| run: node scripts/test-release-channel.mjs --app dash | |
| - name: Shell sizes | |
| # TWO MECHANISMS, DELIBERATELY. This one and the advisory PR build-size | |
| # comment (.github/workflows/pr-build-size*.yml) answer different | |
| # questions, and deleting either as "duplicate measurement" loses a | |
| # class of regression: | |
| # | |
| # this step — ABSOLUTE and PERSISTENT. A shell with enforce:true has | |
| # a ceiling and FAILS here; a shell with enforce:false prints its | |
| # drift from a committed `reference` and never fails. It is the only | |
| # thing that sees SLOW drift — forty merged commits at 300 B each, | |
| # where every single PR looked fine on its own. | |
| # the PR comment — RELATIVE and PER-PR. It reports this PR against its | |
| # own base and never fails. It is the only thing that attributes a | |
| # jump to the change that caused it, and it is blind to accumulated | |
| # drift. | |
| # | |
| # Neither subsumes the other. Rationale in docs/DECISIONS.md. | |
| # | |
| # Placed after ALL THREE builds on purpose: the rig skips a shell that | |
| # is not built, so while this sat in the spaces section it silently | |
| # measured nothing for dash. bento/spaces has NO CEILING as of | |
| # 2026-08-24 (the maintainer's call; at 98.6% it had started deciding | |
| # which fixes were affordable) — the step still prints its size and | |
| # drift, it just no longer fails on it. | |
| run: node scripts/test-spaces-size.mjs | |
| - name: Language pack verification rig | |
| # Packs come off the network, so the signature + hash-pin chain is the | |
| # only thing between a hostile channel and the strings in the UI. | |
| # Tampered pack, tampered index, unsigned index — each must be refused. | |
| run: node scripts/test-packs.ts | |
| - name: Tokenizer contract | |
| # The kernel's built-in highlighting tier. Renderers rebuild text from | |
| # the token stream, so losslessness is corruption-or-not; and multiline | |
| # tokens are declared, because a blockified token carrying \n collapses | |
| # a code block to one line with no error anywhere. | |
| run: node scripts/test-tokenize.ts | |
| - name: Code-morph identity engine | |
| # HeckelDiff decides which token on slide N is the same token as on | |
| # slide N-1; every wrong answer is a token that jumps, tears, or steals | |
| # another token's journey on screen. Regressions for the three pairing | |
| # bugs found in review ride along. | |
| run: node scripts/test-codediff.ts | |
| - name: First-page preview rig | |
| # Every save writes a static render of page one for file-manager | |
| # thumbnails. Two decisions in that path fail silently: an encrypted | |
| # deck must never carry one, and author content must never reach the | |
| # file carrying a script tag. | |
| run: node scripts/test-preview.ts | |
| - name: Media autoplay rig | |
| # An author set a clip's Autoplay to Off and it played anyway, every | |
| # show, since media shipped in v0.9.16. render.ts wrote | |
| # data-autoplay="" when off — PRESENT but empty — and Reveal decides | |
| # autoplay with hasAttribute(), so it played what our own value-based | |
| # selector correctly ignored. The property pinned here is that an | |
| # autoplay-off element carries NO such attribute, because its presence | |
| # is read by code we do not own. Measured on rendered DOM: the | |
| # difference between assigning '' and not assigning is invisible to a | |
| # grep. Self-skips without Chrome. | |
| run: node scripts/test-media-autoplay.ts | |
| - name: Return-gate rig | |
| # The web demo always starts fresh, so someone who saved from | |
| # bento.page and came back saw a blank starter and read it as lost | |
| # work. The gate that fixes it can be worse than the silence it | |
| # replaces: firing over a real document, or offering a host that has no | |
| # release (telling an Android user to install a Chrome extension). This | |
| # pins both, and that the mobile rows degrade until those hosts ship. | |
| run: node scripts/test-return-gate.ts | |
| - name: Offline-mode rig | |
| # Offline mode promises "nothing leaves this computer" and was broken | |
| # five ways at once (GHSA-5c3x-xqp6-g94r): a manual update check, a | |
| # language-pack download, a deck's remote <video>, in-flight requests | |
| # that kept running, and a second tab's socket that kept syncing. | |
| # | |
| # It stayed hidden because it reads as correct: an agent asked to audit | |
| # it said "secure" from the docs AND from the code. So the rig does not | |
| # check the call sites — it enforces the POLICY that no file outside | |
| # kernel/src/net.ts may touch a network primitive at all, which is what | |
| # stops the NEXT call site being wrong, plus the chokepoint's own | |
| # behaviour (refusal, aborting what is already in flight, cross-tab). | |
| run: node scripts/test-offline.ts | |
| - name: Untrusted-markup sanitizer rig | |
| # Every deck is untrusted input somebody may open, paste or receive over | |
| # collab, and script in that page owns the collab keys, the plaintext | |
| # autosave store and the write handle to the file on disk. An audit | |
| # measured `<svg onload>`, a rect `onclick`, `<img onerror>` in a | |
| # foreignObject and `<form action="javascript:">` all executing. | |
| # | |
| # The sanitizer is an ALLOWLIST, so what this pins is the DEFAULT: the | |
| # first version was a denylist that passed 40 checks with the critical | |
| # still open, because the rig tested call sites with source regexes | |
| # instead of the policy. These checks drive the real walk, and the | |
| # browser half needs Chrome — it self-skips where there is none, so the | |
| # node half still gates. | |
| run: | | |
| slides/node_modules/.bin/esbuild scripts/test-sanitize.ts --bundle --platform=node --format=esm \ | |
| --outfile="$RUNNER_TEMP/test-sanitize.mjs" | |
| node "$RUNNER_TEMP/test-sanitize.mjs" | |
| - name: Export-secrets rig | |
| # "Copy document JSON" shipped the room's owner private key while the | |
| # tooltip recommended pasting it into an AI chat, and "Save as | |
| # template…" wrote plaintext out of a password-protected deck. Both are | |
| # invisible in the product: the file looks right. This scans the source | |
| # for the two shapes — an export that forgets to strip collab secrets, | |
| # and a user-facing path that reaches serializeFile instead of | |
| # serializeAuto — so a NEW export path cannot reintroduce either. | |
| run: node scripts/test-export-secrets.ts | |
| - name: Blob content-address rig | |
| # Encrypted assets are fetched by content address from a relay the | |
| # threat model treats as untrusted. Nothing verified that what came | |
| # back matched what was asked for, and the substitution was cached to | |
| # disk, so one poisoned response persisted. | |
| run: node scripts/test-blobs.ts | |
| - name: home/ios bridge rig | |
| # The iOS host opens ANY self-contained HTML document and treats those | |
| # documents as mutually untrusted, so a page-supplied filename reaching | |
| # appendingPathComponent was an arbitrary write inside the container — | |
| # including over another deck the user would later open and trust. | |
| # Source-shape checks; the Swift half self-skips without a toolchain. | |
| run: node scripts/test-home-bridge.ts | |
| - name: tray mark is one source | |
| # tray-icon.svg is the only drawing of the mark. Everything else is | |
| # generated from it: the Android launcher vectors (which have no <rect>, | |
| # so each rounded corner becomes opaque path data nobody would diff), and | |
| # the iOS asset catalog — the mark plus the four brand colours the app's | |
| # chrome reads by name. A hex value retyped into Swift is a value that can | |
| # disagree with the logo while looking almost right, which is the kind of | |
| # wrong nothing notices. This fails if any of it drifts. | |
| run: node home/assets/make-icons.mjs --check | |
| - name: home/ios index rig | |
| # The iOS text extractor is a PORT of home/webext's, and the two are held | |
| # together by nothing but agreement — so this diffs them, driving the | |
| # real library.js against the compiled Swift over generated edge cases | |
| # and every document in the tree. A divergence means a deck is findable | |
| # by a phrase on one host and not the other, which nothing else notices. | |
| # This one really runs HERE: BentoIndex.swift is Foundation-only, and the | |
| # runner has Swift, so the diff happens on every push rather than only on | |
| # a Mac. That is the payoff for keeping UIKit out of that file. | |
| run: node scripts/test-tray-index.mjs | |
| - name: document index conformance corpus | |
| # home/fixtures/ is the corpus all three hosts answer — the JS reference | |
| # here, the Kotlin port under Gradle, the Swift port in the rig above. | |
| # This is the JS side of it, and it is the reference the other two are | |
| # diffed against, so a break here is a break in the thing they agree | |
| # with. It went unregistered and then unnoticed: the tray→home rename | |
| # left it importing ../tray/doc-index.mjs, and a rig nobody runs cannot | |
| # report its own import failure. | |
| run: node scripts/test-doc-index.mjs | |
| - name: home/ios release verifier rig | |
| # Starter decks are not bundled; a new document is FETCHED from the | |
| # signed release channel and written to disk as an executable page. The | |
| # signature and hash checks are what make that acceptable, so this proves | |
| # the verifier REFUSES — tampered payload, tampered signature, a real | |
| # signature over different bytes, no signature at all. Watching it accept | |
| # the live manifest proves nothing: `return true` passes that test. | |
| # Unlike the index rig above this CANNOT run here — CryptoKit is | |
| # Apple-only — so it probes for that module (not for swiftc, which the | |
| # runner has) and skips loudly. A Mac must run it before Releases.swift | |
| # changes. | |
| run: node scripts/test-tray-releases.mjs | |
| - name: WebExtension package rig | |
| # Validates the store package without writing it: every file the | |
| # manifest names exists, every icon is the size it claims, no unexpected | |
| # file is about to ship (probe/ is four capability probes — harmless to | |
| # us, alarming to a reviewer), and no permission is declared but unused. | |
| # A listing rejected for a missing 128px icon costs days of review. | |
| run: node scripts/pack-webext.mjs --check | |
| - name: WebExtension background rig | |
| # The half that WRITES, and previously the untested half. Two properties | |
| # it must hold: the target file comes from sender.url (browser-stamped) | |
| # and never from the page, and nothing is carried between messages, | |
| # because an MV3 service worker is evicted mid-save and state across that | |
| # gap fails intermittently on nobody's machine but the user's. | |
| run: node scripts/test-webext-background.ts | |
| - name: WebExtension bridge rig | |
| # home/webext decides, per save, whether to write in place or hand back | |
| # to the browser's own picker. Wrong in the permissive direction it | |
| # overwrites the open document with no dialog — which it did once. The | |
| # decision is pure logic over an options bag, so it is checkable here | |
| # rather than by reloading an extension and pressing Cmd-S. | |
| run: node scripts/test-webext-bridge.ts | |
| - name: WebExtension route rig | |
| # Turning an absolute path into "where this granted folder lives", from | |
| # paths the extension did not choose — browser history, for files that | |
| # may have moved or never been in a grant at all. Getting it wrong does | |
| # not produce a broken link; it records a folder at the wrong place and | |
| # every document in it silently opens something else. So it must prove, | |
| # never guess: the route resolves inside the grant AND dir.resolve() | |
| # agrees with the path's own tail. | |
| run: node scripts/test-webext-route.ts | |
| - name: WebExtension document-library rig | |
| # The popup is a document browser, and everything in it is DERIVED from | |
| # files on disk: the title out of the document block, the thumbnail out | |
| # of the preview block, the openable path out of a learned prefix. Each | |
| # is a parse against a format that moves, and each fails quietly — a row | |
| # showing a filename instead of a title, or a blank square. Nothing | |
| # throws, so nothing reports it. | |
| # | |
| # Runs AFTER the slides build on purpose: it reads the real shell, and | |
| # with a document spliced in at the real offset. Hand-written fixtures | |
| # would have passed the bug this replaces, where a metadata reader hunted | |
| # for the document block's CLOSING tag and so found nothing in any | |
| # document carrying an image. | |
| run: node scripts/test-webext-library.ts | |
| - name: WebExtension release-trust rig | |
| # Creating a document downloads an app shell and writes it to the user's | |
| # disk, where they later double-click it — executable HTML they go on to | |
| # trust. So the manifest's signature is checked against the key compiled | |
| # into the extension, and the download against the digest that signature | |
| # covers. Signature over the pin, pin over the bytes. | |
| # | |
| # It also reads a REAL manifest captured from the release origin, which | |
| # is the check that could not be faked into passing: the shipped code | |
| # read `url` off the top level of a SIGNED ENVELOPE that has no top-level | |
| # url, so the + button had never worked in any version — and the rig | |
| # agreed, because its fixture was written to the shape the code wanted. | |
| run: node scripts/test-webext-release.ts | |
| - name: WebExtension update-notice rig | |
| # Store installs update themselves; one loaded unpacked never will, and | |
| # Chrome ignores update_url for a development install. So the GitHub | |
| # route has to be told — and told in exactly one direction: a store user | |
| # must never see a notice they cannot act on, and must not even cost a | |
| # request. Every failure mode here is silent by nature. | |
| run: node scripts/test-webext-update.ts | |
| - name: WebExtension localisation rig | |
| # Every failure mode of a message catalogue is silent: a key the code | |
| # uses but English lacks renders as the key name, a locale missing a key | |
| # falls back so a German page carries English sentences, and a dropped | |
| # $1 leaves a hole in a translated sentence. All of them are visible | |
| # only to someone who reads that language. | |
| run: node scripts/test-webext-i18n.ts | |
| - name: Save-intent rig | |
| # A host polyfilling showSaveFilePicker sees only the options bag, and | |
| # must tell "overwrite the open document" from "make a second file" from | |
| # that alone. It could not, and a browser extension overwrote a deck. | |
| # `id` is the signal; if the three purposes ever collapse to one value | |
| # every host silently loses the distinction. | |
| run: node scripts/test-savepurpose.ts | |
| - name: Guarded storage rig | |
| # localStorage's absence is hostile: reading the PROPERTY throws, so | |
| # `if (localStorage)` is itself the crash and one bare read at module | |
| # scope takes the whole file down before it paints. Measured in a | |
| # sandboxed iframe. The rig simulates each failure mode and pins the | |
| # regression: importing a module that reads storage on load must not | |
| # throw when storage does. | |
| run: node scripts/test-storage.ts | |
| - name: App registry rig | |
| # scripts/apps.mjs drives what a release builds, stages and signs. A | |
| # release SIGNS, and the key never reaches CI, so the pipeline itself | |
| # cannot be tested here — but the registry drifting from the tree can, | |
| # and a wrong appId is silent: shipped shells verify it against their | |
| # own configureApp() id and just stop updating. | |
| run: node scripts/test-release-apps.mjs | |
| - name: Autosave per-app database rig | |
| # Every app shared one IndexedDB database AND one DB_VERSION, so spaces | |
| # has been writing into slides' store, and a version bump by any new app | |
| # would make every SHIPPED shell of every other app throw VersionError | |
| # and lose autosave. Renaming without carrying the data over would empty | |
| # a visible feature, so the migration is tested in both directions. | |
| run: | | |
| slides/node_modules/.bin/esbuild scripts/test-autosave.ts --bundle --platform=node --format=esm \ | |
| --alias:fake-indexeddb/auto=./slides/node_modules/fake-indexeddb/auto/index.js \ | |
| --outfile="$RUNNER_TEMP/test-autosave.mjs" | |
| BENTO_TEST_APP=bento-slides node "$RUNNER_TEMP/test-autosave.mjs" | |
| BENTO_TEST_APP=bento-spaces node "$RUNNER_TEMP/test-autosave.mjs" | |
| - name: Publish deletion-gate rig | |
| # A publish assembles site/ for ONE app and mirrors it authoritatively. | |
| # Measured: a spaces-shaped site/ deleted 47 live files — the signed | |
| # shell, the manifest, all 22 packs — with every other gate green, | |
| # because those gates are `if (existsSync(...))` and skip when the | |
| # artifact is missing. Shipped files cannot recover from that. | |
| run: node scripts/test-publish-gate.mjs | |
| - name: Model key tables match the format | |
| # slides/src/modelkeys.generated.ts is derived from model.ts and drives | |
| # validate()'s unknown-key check. A stale copy would report real | |
| # properties as unknown, and an agent acting on that deletes working | |
| # configuration — so the generated file is committed and pinned here. | |
| run: node scripts/build-modelkeys.mjs --check | |
| - name: Document validation rig | |
| # validate() is only useful if it is trusted: the rig proves the starter | |
| # deck and a realistic chart stay silent, and that a deliberately broken | |
| # deck trips every check. | |
| run: | | |
| slides/node_modules/.bin/esbuild scripts/test-validate.ts --bundle --platform=node --format=esm --outfile="$RUNNER_TEMP/test-validate.mjs" | |
| node "$RUNNER_TEMP/test-validate.mjs" | |
| - name: bento/dash model rig | |
| # The format is permanent — additive forever, no server to migrate a | |
| # single file. Two failures are unrecoverable once files exist: dropping | |
| # a field this build does not recognise, and letting anything but an | |
| # ABSENT block reach the starter document (which then overwrites live | |
| # data on the first save). Both are asserted from both sides. | |
| run: node scripts/test-dash-model.ts | |
| - name: bento/dash store + undo rig | |
| # Undo is a patch log rather than document snapshots, which buys history | |
| # that costs what the EDIT costs — and risks a failure a snapshot cannot | |
| # have: an inverse that does not restore what it displaced. Every op is | |
| # checked apply → undo → identical, because that failure is silent. | |
| run: node scripts/test-dash-store.ts | |
| - name: bento/dash import rig | |
| # Import is where a data tool does its damage, because every mistake is | |
| # SILENT: the file opens, the grid fills, the totals compute, and the | |
| # numbers are wrong. Two guards matter most and are checked from both | |
| # sides — an undecidable date order is REFUSED rather than guessed, and | |
| # the decimal convention is decided per COLUMN from the whole column. | |
| run: node scripts/test-dash-import.ts | |
| - name: bento/dash formula rig | |
| # A wrong number is the worst failure a spreadsheet has, and the | |
| # dangerous ones are plausible rather than loud: an error that becomes a | |
| # zero, a cycle that resolves to something, a column computed before the | |
| # column it depends on. All three are asserted, each with a control. | |
| run: node scripts/test-dash-formula.ts | |
| - name: bento/dash chart rig | |
| # A tile names columns and derives its series at render, so the whole | |
| # surface worth testing is the derivation. The failure that matters most | |
| # is a missing value drawn as a zero — "we sold nothing" instead of "we | |
| # do not know". | |
| run: node scripts/test-dash-chart.ts | |
| # ── the rest of bento/dash. Each of these asserts a way a spreadsheet can | |
| # be CONFIDENTLY WRONG, which is the failure that matters here: nobody | |
| # re-derives a number by hand, so a wrong one is not caught by the person | |
| # relying on it. Each rig argues its own case in its header; the lines | |
| # below say why it gates a merge. | |
| - name: bento/dash function pack | |
| # Lookups, multi-criteria, finance, statistics — including the three | |
| # places we deliberately depart from Excel because Excel's default is a | |
| # trap: an approximate VLOOKUP on unsorted data, an IRR that converges | |
| # on nothing, a PERCENTILE that clamps instead of refusing. | |
| run: node scripts/test-dash-functions.ts | |
| - name: bento/dash cell formulas | |
| # Evaluation ORDER. A cell computed before the cell it reads writes a | |
| # stale number into the document, looking authoritative. | |
| run: node scripts/test-dash-cellformula.ts | |
| - name: bento/dash A1 references | |
| # `$` pins against a COPY and not against structure, and a reference | |
| # into a deleted row is #REF! rather than whatever slid up into it. | |
| run: node scripts/test-dash-a1.ts | |
| - name: bento/dash defined names | |
| # A name is a redirection, and every way it can go wrong reports a | |
| # NUMBER: substituted inside quoted text or after a sheet's `!`, | |
| # outranking a column of the same name, spelled like a cell address and | |
| # therefore never consulted, or DROPPED when its rows are deleted | |
| # instead of becoming #REF!. Ends by mounting the real grid, because an | |
| # engine the app never passes the name table to is invisible. | |
| run: node scripts/test-dash-names.ts | |
| - name: bento/dash array formulas and spill | |
| # A spilled cell is COMPUTED, not stored: it must never reach the file, | |
| # a collision must BLOCK with #SPILL! rather than overwrite, two spills | |
| # wanting one cell must pick the same winner on every machine, and the | |
| # shape must survive the operators (2×3, not 6×1 — the wrong answer is | |
| # right in the only cell anybody checks). Datasets deliberately do not | |
| # spill; a column formula is the columnar answer to the same need. | |
| run: node scripts/test-dash-spill.ts | |
| - name: bento/dash cross-sheet references and what row 1 means | |
| # Both halves of one boundary. A per-CELL formula reaches another sheet | |
| # from EITHER kind of sheet — it used to work on a spreadsheet and | |
| # answer #REF! on a dataset in the same workbook, which was two call | |
| # sites rather than a decision. A COLUMN formula still may not, and now | |
| # names the boundary instead of calling a sheet in the tab strip an | |
| # unknown name: reaching across is either a position that moves or a | |
| # join with no key, and dash has `join`. Then the trap underneath: an A1 | |
| # row is the row the addressed sheet paints in its own gutter, so a | |
| # dataset's row 1 is its first DATA row and a spreadsheet copy of it is | |
| # one lower — safe only because the conversion shifts LOCAL references | |
| # and leaves QUALIFIED ones alone. That last one is measured against | |
| # both sheets, because it is the part that would rot in silence. | |
| run: node scripts/test-dash-xsheet.ts | |
| - name: bento/dash pivot | |
| # Subtotals are COMPUTED, not summed: an average of averages is a | |
| # different number, and a distinct count does not add up. | |
| run: node scripts/test-dash-pivot.ts | |
| - name: bento/dash dashboard | |
| # Cross-filter selections compose, and the categories of a column | |
| # PARTITION the sheet — no row dropped, none counted twice. | |
| run: node scripts/test-dash-dashboard.ts | |
| - name: bento/dash data story | |
| # A gap tweens as a gap. Interpolating an absent quarter through zero is | |
| # the animation telling a lie about the data. | |
| run: node scripts/test-dash-story.ts | |
| - name: bento/dash filter and sort | |
| run: node scripts/test-dash-filter.ts | |
| - name: bento/dash filtering is REACHABLE | |
| # The engine rig above proves sixteen predicates over an order vector; | |
| # the app used to offer ONE of them per column — a free-text "Contains" | |
| # box you had to already know the answer to spell into. This one drives | |
| # the column menu over a real DOM: it opens the value checklist on a | |
| # mounted grid, CLICKS a checkbox, and asserts the row indices | |
| # `store.order` ends up holding — plus that a truncated list never | |
| # pre-ticks itself, that a column's list honours the other columns' | |
| # filters and not its own, and that none of it dirties the document. | |
| run: node scripts/test-dash-filterui.ts | |
| - name: bento/dash rows and columns | |
| # Structure, and the rid watermark — a rid must never be minted twice, | |
| # which is collaboration's correctness precondition. | |
| run: node scripts/test-dash-rowcol.ts | |
| - name: bento/dash selection and clipboard | |
| run: node scripts/test-dash-select.ts | |
| - name: bento/dash conditional formatting | |
| run: node scripts/test-dash-condfmt.ts | |
| - name: bento/dash conditional formatting is REACHABLE | |
| # The engine rig above passes on six rule kinds; the app used to build | |
| # two of them, so "highlight cells greater than N" was missing from a | |
| # product that had already shipped it. This one drives the panel section | |
| # with panels.ts's own KIT over a real DOM and asserts the rule OBJECT | |
| # that would reach the document — plus that the menu and the panel | |
| # actually mount it, which is what makes the rest of it visible. | |
| run: node scripts/test-dash-condfmtui.ts | |
| - name: bento/dash a formula column is typed by what it returns | |
| # It used to be born `number`, hardcoded: a formula splitting surnames | |
| # out of "Lastname, Firstname" produced a NUMBER column of text that | |
| # filtered, sorted, exported and totalled wrongly. Inference is by the | |
| # JS type of the computed values, never their appearance — a numeric | |
| # STRING typed number is the SUM 0 bug (test-dash-coltype.ts) arriving | |
| # by a new door, and that is asserted by name. | |
| run: node scripts/test-dash-computedtype.ts | |
| - name: bento/dash paste special | |
| # Values only must land the COMPUTED value and NEVER an error object — | |
| # the same class of damage scripts/test-dash-fill.ts records, reached | |
| # from the other side. Also: formats-only cannot move a number (the | |
| # column is summed before and after), a copied formula's relative refs | |
| # translate while $A$1 does not and a CUT one does not translate at | |
| # all, and transpose is REFUSED on a dataset because its columns are | |
| # typed. Last section reads main.ts: the chord must reach the command, | |
| # and the clip must be snapshotted BEFORE ⌘X clears the selection. | |
| run: node scripts/test-dash-pastespecial.ts | |
| - name: bento/dash text to columns | |
| # The split reuses import.ts's delimiter parser and its type inference, | |
| # so "Smith, John" quoted stays one field and an undecidable date | |
| # column lands as TEXT rather than eleven months out. The overwrite is | |
| # named before it happens. And the outcome is checked in the DOCUMENT: | |
| # patches are committed to a real Store, the source column survives, | |
| # and re-running the recorded step reproduces the same bytes. | |
| run: node scripts/test-dash-tocolumns.ts | |
| - name: bento/dash side panels | |
| run: node scripts/test-dash-panels.ts | |
| - name: bento/dash panel rhythm | |
| # The panel's spacing has been reported broken three times. This pins | |
| # the numbers — one row height, one pitch, one gutter, one radius — and | |
| # refuses a second spelling of any of them, so the next drift is a build | |
| # failure rather than a fourth screenshot. | |
| run: node scripts/test-dash-panelrhythm.ts | |
| - name: bento/dash About dialog | |
| run: node scripts/test-dash-about.ts | |
| - name: bento/dash dialog surfaces | |
| # The About dialog held eight sections and measured 1361px in a 429px | |
| # viewport — 3.2 screens. It is two surfaces now (About = what travels | |
| # in the file; Settings = what this reader's browser remembers) plus the | |
| # password, which moved to the Save menu with the other instructions | |
| # about how the file gets written. This holds the seam AND the size: | |
| # each surface fits a laptop screen without scrolling, measured through | |
| # a box model of about.css calibrated against that 1361px. | |
| run: node scripts/test-dash-surfaces.ts | |
| - name: bento/dash thumbnail preview | |
| # An encrypted workbook NEVER gets a preview — a plaintext title beside | |
| # the ciphertext is the leak the password exists to prevent. | |
| run: node scripts/test-dash-preview.ts | |
| - name: convert engine rigs | |
| # The pptx importer's resolver is where real decks' fidelity lives -- | |
| # 91% of colour and 71% of run properties are indirect, and every hop | |
| # fails silently (a backwards clrMap still renders, just dark-on-dark). | |
| # Seven rigs, 378 checks, negative controls proven against mutated | |
| # implementations; load.ts is the gate that matters: every emitted | |
| # document must pass the REAL slides parseDoc with zero validateDoc | |
| # errors. | |
| run: node scripts/test-convert.ts | |
| - name: bento/dash zip | |
| # Verified against real unzip implementations, not only our own reader: | |
| # a reader and a writer that share a bug agree with each other perfectly. | |
| run: node scripts/test-dash-zip.ts | |
| - name: bento/dash xlsx | |
| # Both 1900 date epochs, including the leap-year bug — getting it wrong | |
| # shifts every date by four years, silently. | |
| run: node scripts/test-dash-xlsx.ts | |
| - name: bento/dash xlsx — what an import carries and what it says it dropped | |
| # The gate that lost a number: `=SUM(RentCells)/B5` has no `!`, no `[` | |
| # and no unknown function, so a bare defined name sailed through and | |
| # the cell painted #NAME? over the 0.61 Excel had computed. Also pins | |
| # the header-under-a-merged-title repair, and the freeze, per-cell | |
| # format and Excel-table totals row that used to be dropped in silence. | |
| # Builds real .xlsx bytes — the gate only ever sees real formula text. | |
| run: node scripts/test-dash-xlsx-carry.ts | |
| - name: bento/dash WebGL | |
| run: node scripts/test-dash-gl.ts | |
| - name: bento/dash 3D scenes | |
| run: node scripts/test-dash-viz3d.ts | |
| - name: bento/dash CRDT convergence | |
| # The big one: randomised concurrent edits across N replicas, asserted | |
| # to converge. It has caught every ordering bug in the engine, and the | |
| # two it did not catch were found by widening it. | |
| run: node scripts/test-dash-sync.ts | |
| - name: dash spreadsheet convergence rig | |
| # The SPREADSHEET kind's own rig, separate from the dataset one because | |
| # it is a different convergence problem: a dataset row has a rid, and a | |
| # spreadsheet cell has only its address. It also pins the snapshot | |
| # FALLBACK — a patch that would move cells must mint zero ops and arm | |
| # the whole-state path, because two replicas shifting addresses | |
| # concurrently renumber each other's writes and no ordering repairs it. | |
| run: node scripts/test-dash-canvassync.ts | |
| - name: relay protocol — one wire format, two clients | |
| # dash's online.ts is a PORT of the transport slides uses (now moving | |
| # into kernel/). The two files are separate text that nobody diffs, and | |
| # ONE DEPLOYED WORKER verifies both: it pins the room id to a key and | |
| # walks an owner→invite→member signature chain. If one client's idea of | |
| # `inv.${pub}.${role}.${exp}` drifts, the symptom is not a red test — it | |
| # is one app's users silently unable to join the other's rooms, on a | |
| # relay that cannot be rolled back independently of the shells already | |
| # in people's files. This diffs only what goes on the wire. | |
| run: node scripts/test-relay-protocol.ts | |
| - name: dash validator rig | |
| # 109 checks over the shapes that read as DATA rather than as damage — | |
| # a column shorter than the sheet has rows, a dictionary index past the | |
| # end of its dict. Nothing here refuses to load a workbook; the point is | |
| # that the user is told, because neither of those looks wrong on screen. | |
| run: node scripts/test-dash-validate.ts | |
| - name: dash comments rig | |
| # A thread anchors to colId + rid, never a position, so it survives a | |
| # sort. The checks that matter are the ORPHAN ones: a deleted row must | |
| # leave the thread dead and saying so, not silently re-pointed at | |
| # whoever inherited the row number. | |
| run: node scripts/test-dash-comments.ts | |
| - name: dash recovery rig | |
| # Every branch of the offer/no-offer decision, with no DOM and no | |
| # IndexedDB in it. A snapshot that cannot be parsed is not offered — a | |
| # Restore button that hands the app a document it will then refuse is a | |
| # dead end wearing a live button. | |
| run: node scripts/test-dash-recovery.ts | |
| - name: dash i18n rig | |
| # A core catalog ships inside every saved workbook and cannot be patched | |
| # without a release, so drift has to fail here rather than in the field: | |
| # a catalog key the source no longer contains is a dead translation, and | |
| # a {placeholder} a translation dropped is a sentence with a hole in it. | |
| run: node scripts/test-dash-i18n.ts | |
| - name: dash sheet-tabs rig | |
| # Sheet order is the tab order and it lives in the DOCUMENT, so a | |
| # reorder is an undoable patch whose inverse carries the ORIGINAL index | |
| # — one undo puts a dragged tab back where it was, not at the end. Also | |
| # guards that deleting the shown sheet steps to another TABLE sheet. | |
| run: node scripts/test-dash-tabs.ts | |
| - name: dash find rig | |
| # The grid is windowed — about 40 rows of a 5,000-row sheet exist in the | |
| # DOM — so the browser's own find reports "not there" for values that | |
| # are. These checks cover the matcher, the view vector (a hit must never | |
| # be a row the filter is hiding) and the replace refusals. | |
| run: node scripts/test-dash-find.ts | |
| - name: dash spreadsheet-cells rig | |
| # The setCanvasCells patch, which writes the SPREADSHEET kind's sparse | |
| # A1 map. Sparseness is not a size optimisation: a cleared cell has to | |
| # be GONE, or an undone sheet is unequal to a fresh one and two | |
| # replicas disagree about whether a cell exists. Also pins the | |
| # collaboration fallback — the CRDT has never heard of this op, and | |
| # localOne's default arm ships a whole-state snapshot rather than | |
| # dropping it. | |
| run: node scripts/test-dash-canvascells.ts | |
| - name: dash step engine | |
| # The relational pipeline the FORMAT has described since commit one and | |
| # nothing executed. Includes a performance floor at 100k rows and a | |
| # HELD-HEAP comparison against a row-shaped pipeline — wall clock does | |
| # not discriminate between the two designs at this size, memory does | |
| # (2.5 MB against 16.8 MB), and memory is what decides whether a | |
| # workbook fits at all. | |
| run: node scripts/test-dash-steps.ts | |
| - name: dash SQL surface | |
| # SQL compiles to Step[] and does not evaluate anything itself — the | |
| # answer is a frame over the same columns, so it stays live rather than | |
| # becoming a copy that is wrong the moment somebody edits a cell. The | |
| # checks that matter are the refusals: a plain JOIN declares card:'one' | |
| # so a non-unique key STOPS the pipeline instead of doubling every | |
| # total, and an edit statement is refused because a query is a question. | |
| run: node scripts/test-dash-sql.ts | |
| - name: dash spreadsheet rig | |
| # Rendering and editing the SPREADSHEET kind — unbounded, sparse, typed | |
| # per cell. The check that matters is that a formula BELOW the data | |
| # lands in a cell that did not exist, without allocating the rows it | |
| # skipped: that gesture is why the kind exists. | |
| run: node scripts/test-dash-canvas.ts | |
| - name: dash promote/flatten rig | |
| # The bridge between the two sheet kinds. The checks that matter are | |
| # the ones about NOT destroying the source: promotion leaves the range | |
| # alone, because a formula under the block and a cross-sheet reference | |
| # into it both point by POSITION and emptying the cells would blank | |
| # them silently. Also that a lifted column formula actually EVALUATES — | |
| # Column.formula is a bare expression, and an "=" in front of it ships | |
| # a column of #VALUE!. | |
| run: node scripts/test-dash-promote.ts | |
| - name: dash fill rig | |
| # ⌘D and the fill handle. This guards a DATA LOSS bug that shipped: | |
| # seeding from the computed value wrote error OBJECTS into stored | |
| # columns and flattened formulas to constants, and ⌘D sharing an | |
| # implementation with the handle made it alternate two seed rows. The | |
| # two gestures are separate here, and a formula fills with its | |
| # references translated. | |
| run: node scripts/test-dash-fill.ts | |
| - name: dash spreadsheet formula rig | |
| # Formulas on the SPREADSHEET kind, including cross-sheet references. | |
| # Sheet1!A1 used to bind to the LOCAL A1 — this sheet's number reported | |
| # under another sheet's name — and ranges over a sparse sheet must cost | |
| # the cells that exist, not the rectangle they span. | |
| run: node scripts/test-dash-canvasformula.ts | |
| - name: dash action-applicability rig | |
| # One table saying which actions run on which sheet kind, and a reason | |
| # for every one that does not. Six toolbar buttons used to throw | |
| # "grid needs a table sheet" on a spreadsheet with nothing shown; the | |
| # rig fails if an action is kind-scoped without a reason string. | |
| run: node scripts/test-dash-actions.ts | |
| - name: dash cell format and type rig | |
| # Per-cell number format, coercion and appearance on a SPREADSHEET | |
| # sheet. The checks that matter are the data-eating ones: 01234 must not | |
| # silently become 1234, 1/2 must never become a date, an ambiguous | |
| # d/m/yyyy is refused rather than guessed, and formatting twenty cells | |
| # is ONE patch and ONE undo. | |
| run: node scripts/test-dash-cellprops.ts | |
| - name: dash theme rig | |
| # A stylesheet has no type system: an undeclared token is a dead rule | |
| # and a light-dark() fed a length is an invalid declaration — both | |
| # silent, and both had SHIPPED. --accent-ink drew nothing and every | |
| # shadow in the app computed to "none". The light palette is also | |
| # diffed against slides' :root so the two cannot drift one grey at a | |
| # time again. | |
| run: node scripts/test-dash-theme.ts | |
| - name: dash file write-back | |
| # The single biggest data-loss risk dash shipped with: an IndexedDB | |
| # snapshot was the only thing catching an hour of typing, and it is | |
| # invisible, uncopyable and cleared by browsers on their own schedule. | |
| # The checks that matter are the bookkeeping ones. A FAILED write must | |
| # not be recorded as a baseline — record the intent and the next cycle | |
| # reads "unchanged", so a permanent failure is reported once and then | |
| # silently skipped forever. Also: two cycles never open concurrent | |
| # writables on one handle, nothing is rethrown out of a setTimeout | |
| # nobody awaits, and the refusals (read-only, template, no-handle) are | |
| # ORDERED, so a read-only workbook reports the permanent reason rather | |
| # than the incidental one. | |
| run: node scripts/test-dash-autosave.ts | |
| - name: dash print | |
| # Every failure mode here is silent: the page that comes out has a | |
| # header, a body and a total, and looks finished whatever is missing | |
| # from it. The grid is windowed at ~46 rows, so the first check builds | |
| # a 5,000-row sheet and counts what reaches the paper — measured in a | |
| # browser too: 3,001 printed rows from a grid holding 46. The rest pin | |
| # that the printout is of the VIEW VECTOR, that the totals row is the | |
| # FILTERED total from the very grid.aggregate the footer calls, that no | |
| # column is ever clipped off the right edge at any wide-sheet setting, | |
| # and that author data reaching markup is escaped — a workbook is | |
| # untrusted input, and this path builds HTML by concatenation. | |
| run: node scripts/test-dash-print.ts | |
| - name: dash grid frontier | |
| # What exists below a dataset's last row: exactly one appender, with | |
| # the ruled lattice stopping at the data. The grid used to LOOK like an | |
| # infinite canvas and behave like a table — pressing "=" below the | |
| # numbers opened the editor on the last DATA cell, so the universal | |
| # spreadsheet gesture silently targeted real data. Typing on the | |
| # frontier must append, and append ONCE: zero times restores the | |
| # original bug, per-keystroke is a different one. Clicking must NOT | |
| # append, since a control that grows the file when you look at it is | |
| # worse than one that does nothing. | |
| run: node scripts/test-dash-frontier.ts | |
| - name: dash grid context menus | |
| # The three menus a right-click produces — cell, row-number gutter, | |
| # column header — asserted from real `contextmenu` events on a mounted | |
| # grid, never from the builders. Two features have shipped complete and | |
| # UNREACHABLE (the conditional-format rules, and the two gutters, which | |
| # did nothing at all on a right-click), and in both cases a rig that | |
| # called the builder would have been green over an app with the feature | |
| # invisible on screen. So the negative control here is a grid with | |
| # `installGridMenus` NOT called: the same right-clicks must produce | |
| # nothing. Also pinned: the labels count the selection and the ops do | |
| # that many; Escape closes a menu and its listener does not outlive it; | |
| # the `+` column appender exists, is absent read-only, and costs | |
| # nothing until its dialog is answered; and finding 11 — a per-cell | |
| # formula is carried into a new row only when the column proves it | |
| # repeats, and the reader is told either way. | |
| run: node scripts/test-dash-menu.ts | |
| - name: dash accessibility | |
| # All of these fail silently to a sighted developer. The load-bearing | |
| # check is aria-rowindex under virtualisation: about forty rows of a | |
| # 5,000-row view exist in the DOM, so a row's position among its | |
| # siblings is meaningless and the index must state its place in the | |
| # FULL view. Get it wrong and a screen reader confidently announces | |
| # "row 3 of 40" in a five-thousand-row sheet, which is worse than | |
| # silence because it is trusted. Also guarded: activeElement after a | |
| # click and after a repaint (every paint replaces innerHTML, so without | |
| # restoration focus drops to BODY), one roving tab stop, and that | |
| # nothing here is a live region. | |
| run: node scripts/test-dash-a11y.ts | |
| - name: dash per-cell appearance | |
| # Appearance on BOTH sheet kinds, which had drifted: the format always | |
| # carried bold/colour on a spreadsheet cell and a dataset cell could | |
| # carry none of it. The check worth naming mounts the REAL grid and | |
| # reads the emitted markup, because the pure-function checks did not | |
| # need the grid to call appearanceCss at all — measured, deleting both | |
| # paint calls left 69 and 123 checks green with the feature invisible | |
| # on screen. This repo shipped that shape once already (--accent-ink | |
| # was a token the stylesheet never declared). Also pins that appearance | |
| # cannot smuggle a value or a type onto a dataset, where the COLUMN | |
| # type is authoritative, and that the CRDT splits content from | |
| # presentation so bolding a cell beside a concurrent retype keeps both. | |
| run: node scripts/test-dash-cellfmt.ts | |
| - name: dash column type vs storage | |
| # Guards a shipped bug that produced a WRONG NUMBER at the end of a path | |
| # dash itself recommends — the worst shape a defect can take: not a | |
| # crash, not an error cell, a confident total that is wrong. Import | |
| # lands a mixed column as text and advises setting the type; doing that | |
| # used to move only `col.type`, so the grid right-aligned and | |
| # number-formatted the values while `aggregate` skipped every one of | |
| # them. Measured on a real .xlsx: footer SUM 0 against a true total of | |
| # 10,308.85, a number dash returns correctly from =SUM() on a | |
| # spreadsheet copy of the same rows. Three properties: converting works; | |
| # undo restores the BYTES and not just the declaration (text→number→text | |
| # is lossy, "1200.50" returns as "1200.5", which a typeof check cannot | |
| # see); and what will not convert is refused rather than zeroed. The | |
| # validator half is checked too, because store.ts can only stop this | |
| # state being created — it still arrives from files saved by the buggy | |
| # build and from hand-edited JSON, which PLATFORM §7 makes a way in. | |
| run: node scripts/test-dash-coltype.ts | |
| - name: dash starter workbook | |
| # The starter ships INSIDE the shell, so it is what bento.page shows and | |
| # what every downloaded copy opens with — the one document in this repo | |
| # a stranger is guaranteed to see — and nothing guarded it. Two families: | |
| # that it is a valid, self-consistent workbook (a starter that trips | |
| # validateDoc ships the app's own warning banner to every new reader on | |
| # first open), and that the NUMBERS are right. The second is the one that | |
| # will catch something: the Scratch sheet reads across into the dataset | |
| # by ADDRESS, so reordering, inserting or hiding a column silently | |
| # re-points two totals on another sheet, and nothing about that is | |
| # visible in a diff. The addresses are pinned by column NAME and the | |
| # totals computed through the shipping code. | |
| run: node scripts/test-dash-starter.ts | |
| - name: dash — the off switch for a live session | |
| # A polish sweep drove the app at 880px with a session running: | |
| # collab.on true, the room live on the relay, and the string "Stop | |
| # sharing" NOWHERE in the document — sync.css hid the toggle outright | |
| # below 900px to buy width in the top bar, and that toggle is the only | |
| # control that stops sharing. The nearest thing left was About's | |
| # "Offline mode", which also kills the signed update check: to stop one | |
| # workbook being shared you had to switch off the app's networking. | |
| # The rule is narrow: a control that REPORTS nothing may stand down at a | |
| # narrow width (the 390px measurement behind those rules is real and | |
| # still honoured), a control that STOPS something already happening may | |
| # not. Checked as CSS text because dash-dom.ts is a parser, not a layout | |
| # engine, and cannot say what a viewport width does to a declaration. | |
| run: node scripts/test-dash-stopsharing.ts | |
| - name: Grid surface rig | |
| # The sheet as an OBJECT ON A SURFACE. An eight-row dataset used to | |
| # fill the top 180px of a 1440x900 window and leave the rest blank in | |
| # the same white as the sheet, so "the table ended here" and "the app | |
| # stopped drawing" looked identical — the strongest unfinished signal | |
| # the app had. The fix is not more rows (test-dash-frontier.ts owns | |
| # that decision and is unchanged): it is a bounded table on a --desk | |
| # ground the spreadsheet kind deliberately does not get, plus the three | |
| # empty states. | |
| # | |
| # Half of it is CSS, which has no type system and no runtime errors, so | |
| # those checks read the stylesheet as text and MEASURE the palette | |
| # rather than merely requiring tokens to be present — a desk 1.02:1 | |
| # from the paper satisfies every structural check and renders as the | |
| # defect. The other half mounts a real Grid and asserts the note is one | |
| # node that survives 5,000 rows. | |
| run: node scripts/test-dash-surface.ts | |
| - name: Data validation rig | |
| # In-cell dropdowns and typed entry rules — Excel's Data Validation, | |
| # which is NOT dash's validate.ts (that one asks whether a workbook | |
| # agrees with itself). Two features, one English word, and a rig that | |
| # confuses them tests neither. | |
| # | |
| # The checks that matter are the three a green suite would otherwise | |
| # hide. (1) The dropdown and the invalid mark are asserted on the | |
| # MARKUP A REAL GRID EMITS, through BOTH paint loops — the pure | |
| # functions were all green with the feature invisible on screen, which | |
| # is precisely the failure this repo shipped once already. (2) `reject` | |
| # refuses AT THE KEYBOARD and nowhere else: a paste, an import, an undo | |
| # and a remote CRDT op all LAND and are marked, because refusing an op | |
| # that has already been committed elsewhere either diverges the | |
| # replicas or silently discards a collaborator's work. Both arms are | |
| # pinned, and a sabotage that leaks the refusal into the paste path | |
| # turns the second one red. (3) A rule never changes existing data — | |
| # what breaks it is marked, never deleted — and the mark is derived at | |
| # paint rather than stored, so it cannot go stale. Also pins the .xlsx | |
| # round trip in both directions, including Excel's INVERTED | |
| # `showDropDown` attribute (1 hides it), which reads correctly in code | |
| # that has it exactly backwards. | |
| run: node scripts/test-dash-datavalid.ts | |
| - name: every rig is actually run | |
| # A rig nobody runs is worse than no rig, and it fails flatteringly: the | |
| # file is there, the checks in it are good, git log shows it being | |
| # maintained, and anyone counting coverage counts it. It just never | |
| # executes, so nothing goes red and nothing says so. Five rigs landed in | |
| # one afternoon — write-back, print, frontier, a11y, appearance — each | |
| # written by a different agent told to write one, none of them knowing | |
| # where this list lives; all five passed on disk and none were here. | |
| # Also checks the reverse, since a step pointing at a deleted script | |
| # fails every push for a reason unrelated to the change that triggered | |
| # it, and prints the deliberate-exemption list on every run so it stays | |
| # something people look at rather than somewhere failures go to hide. | |
| run: node scripts/test-ci-registered.ts | |
| - name: Theme token gate | |
| # Every failure this catches is INVISIBLE in the light theme, which is | |
| # the one the author is looking at. A chrome surface pinned to white | |
| # keeps its light background in dark and puts light text on it — the | |
| # properties panel measured 1.21:1 against a 4.5 floor before this gate | |
| # existed. It also stops a document token being themed (which would | |
| # invert the deck) and the light palette forking from spaces and type. | |
| run: node scripts/test-slides-theme.mjs | |
| - name: Brand palette rig | |
| # themeRefs record where a colour came from so a deck can be re-branded. | |
| # Three properties must hold or the feature is a liability: the | |
| # derivation must be IDEMPOTENT (it runs on the doc event — one that | |
| # never settles is a hang, not a wrong colour), stripping the new fields | |
| # must leave every rendered value identical (every shipped shell reads | |
| # the literals and always will), and an unresolvable reference must | |
| # change nothing rather than writing undefined. | |
| run: node scripts/test-theme.ts | |
| - name: Built-in layout geometry rig | |
| # The built-ins are drawn against 1600x900 while the model default is | |
| # 1280x720, so they used to hang 200px off the right edge of a default | |
| # deck. Nothing structural could see it — the fix is arithmetic, and so | |
| # is the regression. | |
| run: node scripts/test-layouts.ts | |
| - name: slides store rig | |
| # The editor's undo/redo, selection and whole-deck replacement, at the | |
| # store level rather than through the DOM. Bundled first for the same | |
| # reason the spaces rigs are: store.ts imports './model' without an | |
| # extension, which node's strip-only TypeScript loader will not follow, | |
| # so run by hand it fails with ERR_MODULE_NOT_FOUND and looks like a | |
| # broken product. It had never been registered here. | |
| run: | | |
| slides/node_modules/.bin/esbuild scripts/test-slide-store.ts --bundle --platform=node --format=esm \ | |
| --outfile="$RUNNER_TEMP/test-slide-store.mjs" | |
| node "$RUNNER_TEMP/test-slide-store.mjs" | |
| # bento/type: the word processor. Same pipeline, same gate. | |
| - name: Install (type) | |
| working-directory: type | |
| run: npm ci | |
| - name: Typecheck (type) | |
| working-directory: type | |
| run: node_modules/.bin/tsc -b | |
| - name: Build single-file shell (type) | |
| working-directory: type | |
| run: npm run build:single | |
| - name: Splice conformance gate (type) | |
| run: node scripts/shell-gate.mjs type/dist-single/Bento_Type.bento.html | |
| - name: Type inline-formatting rig | |
| # A block stores plain text + marks over character ranges, and the | |
| # round trip render→read must be exact: if it is not, formatting | |
| # silently mutates on every re-render, which surfaces as a signed | |
| # document that stops verifying. Includes a 2,000-case fuzz — it found | |
| # two bugs no hand-written case did. | |
| run: node scripts/test-type-inline.ts | |
| - name: Type model rig | |
| # The load contract (an unreadable file must never become an empty one | |
| # over live data), deterministic id repair, and the rule that marks and | |
| # footnote anchors move together — separating them is what put a | |
| # footnote marker in the middle of a word. | |
| run: node scripts/test-type-model.ts | |
| - name: Type store rig | |
| # A typed word is ONE undo press, and a scoped snapshot really is | |
| # scoped: 200 edits cost ~94KB rather than ~21.7MB. | |
| run: node scripts/test-type-store.ts | |
| - name: Type redline rig | |
| # accept(all) == what they sent back, reject(all) == what you sent. | |
| # Without both, "review these changes" is a lie. | |
| run: node scripts/test-type-redline.ts | |
| - name: Type paginated-output rig | |
| # The printed page count must be the one the editor computed — if print | |
| # re-paginates, "page 14 paragraph 3" stops meaning anything, which is | |
| # the whole reason documents like this are exchanged as PDF. Also pins | |
| # that footnotes reach the page their reference lands on, and that a | |
| # document's own page size is what prints. | |
| run: node scripts/test-type-print.ts | |
| - name: Type theme rig | |
| # Static checks so a theme cannot rot: both themes define the same | |
| # roles, the document surface is never themed, and no chrome rule | |
| # borrows a document token — that last one shipped a select at 1.92:1 | |
| # against a WCAG AA floor of 4.5. | |
| run: node scripts/test-type-theme.mjs | |
| - name: bento/dash CRDT convergence | |
| # The big one: randomised concurrent edits across N replicas, asserted | |
| # to converge. It has caught every ordering bug in the engine, and the | |
| # two it did not catch were found by widening it. | |
| run: node scripts/test-dash-sync.ts | |
| - name: relay protocol — one wire format, two clients | |
| # dash's online.ts is a PORT of the transport slides uses (now in | |
| # kernel/). The two files are separate text that nobody diffs, and ONE | |
| # DEPLOYED WORKER verifies both: it pins the room id to a key and walks | |
| # an owner→invite→member signature chain. If one client's idea of | |
| # `inv.${pub}.${role}.${exp}` drifts, the symptom is not a red test — it | |
| # is one app's users silently unable to join the other's rooms, on a | |
| # relay that cannot be rolled back independently of the shells already | |
| # in people's files. This diffs only what goes on the wire. | |
| # | |
| # Written by the bento/dash work; taken here because the kernel lift is | |
| # the change it guards, and it should not land after the move it exists | |
| # to check. The rig FOLLOWS the twin (kernel first, then slides) and | |
| # FAILS if it finds no transport at all, rather than silently comparing | |
| # two empty lists — which is how its own first version passed. | |
| run: node scripts/test-relay-protocol.ts | |
| - name: Type blocks rig | |
| # Lists and tables are runs of flat blocks assembled at render; | |
| # this pins the grouping, including the level clamp that once opened a | |
| # billion list elements and killed the process. | |
| run: node scripts/test-type-blocks.ts | |
| - name: Type layout rig | |
| # Paragraph spacing, indents and page setup, and that a page break | |
| # is honoured by the paginator rather than merely stored. | |
| run: node scripts/test-type-layout.ts | |
| - name: Type image rig | |
| # Pictures embed or reference, and an atomic block is measured as one | |
| # box so pagination cannot break a page through the middle of it. | |
| run: node scripts/test-type-image.ts | |
| - name: Type link rig | |
| # Link marks round-trip, and the href allow-list holds: a document is | |
| # untrusted input, and javascript: reached the page from one once. | |
| run: node scripts/test-type-link.ts | |
| - name: Type find rig | |
| # Find and replace across blocks, including the offsets that shift | |
| # when a replacement changes a block's length. | |
| run: node scripts/test-type-find.ts | |
| - name: Type toc rig | |
| # Section numbering is DERIVED, never stored, so inserting a heading | |
| # renumbers everything and nothing drifts. | |
| run: node scripts/test-type-toc.ts | |
| - name: Type xref rig | |
| # Captions and cross-references are atoms at an offset, so renumbering | |
| # is invisible to the redline. | |
| run: node scripts/test-type-xref.ts | |
| - name: Type cite rig | |
| # Citations and the bibliography — a citation is an atom, so | |
| # restyling one is not an edit to the sentence around it. | |
| run: node scripts/test-type-cite.ts | |
| - name: Type math rig | |
| # Formulas: the TeX source is the model and inline math is a mark, so | |
| # a formula survives the render→read round trip intact. | |
| run: node scripts/test-type-math.ts | |
| - name: Type comments rig | |
| # Threads keep their anchors through edits, splits and merges, and | |
| # editing or deleting a message never moves the text it points at. | |
| run: node scripts/test-type-comments.ts | |
| - name: Type track rig | |
| # Tracked changes as ins/del marks: accept-all gives the text as | |
| # edited, reject-all the text as it was, and display modes never touch | |
| # the model. | |
| run: node scripts/test-type-track.ts | |
| - name: Type font rig | |
| # Per-selection typeface and size, and the CSS allow-list that keeps a | |
| # font stack from opening a second declaration in a style attribute. | |
| run: node scripts/test-type-font.ts | |
| - name: Type autosave rig | |
| # A crash must not cost the work since the last ⌘S. Pins the content | |
| # key that decides whether a snapshot disagrees with the loaded file, | |
| # and that an encrypted document is NEVER snapshotted — plaintext in | |
| # IndexedDB beside a file whose whole point is that it is encrypted. | |
| run: node scripts/test-type-autosave.ts | |
| - name: Type preview rig | |
| # Thumbnailers run no JavaScript, so a saved file carries a still of | |
| # page one plus a parser-blocking remover. Pins the ordering (nothing | |
| # paintable between host and remover), that a second save REPLACES | |
| # rather than appends, and that an encrypted document gets none. | |
| run: node scripts/test-type-preview.ts | |
| - name: Type i18n rig | |
| # Eight languages in one file: every catalogue holds the same keys, | |
| # every {placeholder} survives translation, no key outlives the string | |
| # it came from, and the packed table matches the catalogues it was | |
| # built from. | |
| run: node scripts/test-type-i18n.ts | |
| - name: Type styles rig | |
| # Named paragraph styles, and the additivity rule: a document with no | |
| # doc.styles must render byte-identically to before the feature. | |
| run: node scripts/test-type-styles.ts | |
| - name: Type move rig | |
| # Reordering moves whole UNITS, so a drag can never tear a table in | |
| # half or drop a paragraph between two bullets. | |
| run: node scripts/test-type-move.ts | |
| - name: Type embed rig | |
| # Embedded Bento artifacts, and the render boundary that refuses a | |
| # script rather than sanitising one. | |
| run: node scripts/test-type-embed.ts | |
| - name: Type chrome rig | |
| # Where controls live: nothing in two menus, no inserts in the | |
| # overflow, and every element the chrome reaches for actually exists — | |
| # a null lookup there has silently broken the boot twice. | |
| run: node scripts/test-type-chrome.ts | |
| - name: Clipboard embedded-font rig | |
| run: | | |
| slides/node_modules/.bin/esbuild scripts/test-clipboard.ts --bundle --platform=node --format=esm --outfile="$RUNNER_TEMP/test-clipboard.mjs" | |
| node "$RUNNER_TEMP/test-clipboard.mjs" | |
| - name: CRDT property-removal rig | |
| # Removing a property travels as a `set` op with `v` ABSENT, and the | |
| # receiving replica threw on it: JSON.stringify(undefined) is undefined, | |
| # and a debug line sliced it. Live in shipped files. The convergence rig | |
| # missed it through 300 seeds because its generator assigns properties | |
| # and never removes one — a property-based rig only explores the | |
| # mutations it was taught. | |
| run: node scripts/test-crdt-delprop.ts | |
| - name: CRDT convergence rig | |
| # 40 seeds locally; deeper on CI where the seconds are free. | |
| run: SEEDS=300 node scripts/test-sync.ts | |
| - name: CRDT equivalence rig (baseline vs candidate, byte for byte) | |
| # Convergence is the weaker property: an engine that converges with | |
| # ITSELF while minting different bytes than the shipped engine passes | |
| # the step above and silently splits every bento/slides file in the | |
| # field. This step asserts byte-identity instead. | |
| # | |
| # The baseline is a FROZEN copy of the engine as shipped | |
| # (scripts/lib/sync-baseline-frozen.ts), so this run is live from here | |
| # on: it compares the working engine against the bytes every file in | |
| # the field was written by. REQUIRE_LIVE=1 fails the build if the | |
| # candidate is ever repointed at something that resolves back to the | |
| # baseline — the quiet way a gate stops gating. | |
| run: REQUIRE_LIVE=1 SEEDS=200 STEPS=60 ACTORS=4 node scripts/test-sync-equiv.ts | |
| - name: CRDT document-shape seam | |
| # The equivalence rig proves a NEGATIVE — that the parameterized engine | |
| # still mints the bytes shipped files were written with — and a | |
| # parameterization that quietly did nothing would pass it. This proves | |
| # the positive: a second shape (pages→blocks) produces an engine that | |
| # actually works, keys nodes on the composite pageId+blockId, and | |
| # restores as itself. Also pins the constructor contract: the shape has | |
| # no default, because a default is how a spaces call site silently ends | |
| # up holding slides' shape. | |
| run: node scripts/test-sync-shape.ts | |
| - name: bento/spaces under the shared engine | |
| # Convergence for the pages/blocks binding, AND a report on whether | |
| # what the replicas converge on is a legal bento/spaces document. | |
| # Those are different questions: page.blocks is flat and in pre-order, | |
| # while the engine merges `parent` as an ordinary register and order as | |
| # a position key — two independent domains describing one tree. The | |
| # format half is REPORTED, not asserted, because the fix is a decision | |
| # nobody has taken yet; STRICT=1 turns it into a gate the day it is. | |
| run: SEEDS=250 STEPS=80 ACTORS=4 node scripts/test-sync-spaces.ts | |
| - name: collaborative text on a parent node | |
| # The token RGA — what lets two people type in one paragraph without | |
| # destroying each other's work — was reachable only on CHILD nodes, | |
| # because both shipped apps put their text there. bento/type does not: | |
| # a block IS the paragraph. Bound naively it would get a | |
| # last-writer-wins register for prose and lose an edit SILENTLY, with a | |
| # document that stays valid and converges. Every other sync rig stayed | |
| # green through that bug, so this one carries a negative control: the | |
| # same scenario with the RGA off MUST lose an edit. | |
| run: node scripts/test-sync-parent-text.ts | |
| - name: a flat document shape | |
| # bento/type is one level deep — `body` holds blocks and nothing sits | |
| # beneath them — so `children` is null and there is no element layer. | |
| # Binds type's real shape and checks both halves: text still merges | |
| # token-by-token, and the document keeps its shape. The second matters | |
| # because an engine that quietly wrote `elements: []` onto every block | |
| # would converge perfectly while corrupting the format. | |
| run: node scripts/test-sync-flat.ts | |
| - name: bento/type under the shared engine | |
| # Convergence for the flat ('body', null, 'text') binding, AND a report | |
| # on whether what the replicas converge on is a legal bento/type | |
| # document. Those are different questions: `text` merges token by token | |
| # through the RGA while `marks` and `notes` — character offsets INTO | |
| # that text — merge as ordinary registers, so two legal concurrent | |
| # edits can converge on offsets that no longer describe the text they | |
| # index. Reported, not asserted, exactly as the spaces tree question | |
| # is: it is a format decision nobody has taken. STRICT=1 makes it a | |
| # gate the day one is. | |
| run: SEEDS=250 STEPS=80 ACTORS=4 node scripts/test-sync-type.ts | |
| - name: the sync session layer | |
| # The session — differ hook, shadow, presence, catch-up, the empty | |
| # document repair — had NO tests before it was moved into the kernel | |
| # and parameterized for three apps. This was written against the | |
| # implementation as it shipped and run unchanged after the move, which | |
| # is the only thing that makes "behaviour is identical" a claim rather | |
| # than a hope. It drives the REAL session over the REAL store through a | |
| # REAL BroadcastChannel; two sessions in one process are two tabs. | |
| # The last four sections bind bento/type to the same kernel session — | |
| # flat document, text on the parent, a store with one listener and no | |
| # dirty flag — so a surviving slides assumption shows up there. | |
| run: node --no-warnings scripts/test-sync-session.ts | |
| - name: the spaces binding of the sync session | |
| # Only what bento/spaces answers DIFFERENTLY, because three of the five | |
| # host answers are not slides' answers and each one is invisible until | |
| # two people are live: a repair id that must be DERIVED (a spare blank | |
| # slide is one click to delete; a spare page is a phantom), a view that | |
| # clamps by page identity to the nearest surviving ANCESTOR rather than | |
| # to home, and a remote op that must not raise this app's 'doc' event — | |
| # which paints "Edited" and would credit a colleague's typing to you. | |
| # The last section is two real sessions over a real BroadcastChannel. | |
| run: node --no-warnings scripts/test-sync-spaces-session.ts |