36ade35240
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>
299 lines
13 KiB
YAML
299 lines
13 KiB
YAML
name: CI
|
|
|
|
on:
|
|
# `push` unrestricted plus `pull_request` ran every job TWICE on any branch
|
|
# with an open PR — visible as the (push) / (pull_request) pairs in the checks
|
|
# list. Restricting push to main means a PR branch is covered once by
|
|
# pull_request, and main once by push, which is also what pages-deploy needs.
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
|
|
jobs:
|
|
test:
|
|
name: Test and lint (host)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
components: clippy, rustfmt
|
|
- uses: Swatinem/rust-cache@v2
|
|
- run: cargo fmt --all -- --check
|
|
- run: cargo clippy --workspace --all-targets -- -D warnings
|
|
- run: cargo test --workspace
|
|
# The comment cap and the test-evidence keys (improvise-9c8c); without
|
|
# this step the rule is advisory. See context/design-principles.md §6.
|
|
- name: The comment checker catches what it has been fixed for
|
|
run: python3 scripts/check-comment-runs.py --self-test
|
|
- name: Comment runs capped at two lines
|
|
run: python3 scripts/check-comment-runs.py
|
|
- name: Test-evidence headings name real tests
|
|
run: python3 scripts/check-comment-runs.py --check-evidence
|
|
|
|
# Too slow on GitHub Actions
|
|
#
|
|
# nix-test:
|
|
# name: Nix Test
|
|
# runs-on: ubuntu-latest
|
|
# steps:
|
|
# - name: Free up disk space
|
|
# run: |
|
|
# sudo rm -rf /usr/share/dotnet \
|
|
# /usr/local/lib/android
|
|
# sudo df -h
|
|
|
|
# - uses: actions/checkout@v7
|
|
# - name: Install Nix
|
|
# uses: DeterminateSystems/nix-installer-action@v14
|
|
|
|
# - name: Magic Nix Cache
|
|
# uses: DeterminateSystems/magic-nix-cache-action@v8
|
|
|
|
# - name: Check Nix health
|
|
# run: nix doctor
|
|
|
|
# - run: nix build
|
|
|
|
wasm:
|
|
name: wasm32 check (browser bundle crates)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: wasm32-unknown-unknown
|
|
- uses: Swatinem/rust-cache@v2
|
|
# Only the crates that go into the browser bundle. The root `improvise`
|
|
# crate is the terminal binary and depends on crossterm, which does not
|
|
# build for wasm — that is correct, not a gap (improvise-ywd).
|
|
- name: improvise-core
|
|
run: cargo check -p improvise-core --target wasm32-unknown-unknown
|
|
- name: improvise-formula
|
|
run: cargo check -p improvise-formula --target wasm32-unknown-unknown
|
|
- name: improvise-protocol
|
|
run: cargo check -p improvise-protocol --target wasm32-unknown-unknown
|
|
- name: improvise-io
|
|
run: cargo check -p improvise-io --target wasm32-unknown-unknown
|
|
- name: improvise-command
|
|
run: cargo check -p improvise-command --target wasm32-unknown-unknown
|
|
# The browser client itself. This is the one crate for which a wasm
|
|
# build is the *point* rather than a property worth keeping, so a
|
|
# failure here is not a portability regression but a broken deliverable
|
|
# (improvise-gsw).
|
|
- name: improvise-wasm-client
|
|
run: cargo check -p improvise-wasm-client --target wasm32-unknown-unknown
|
|
# The worker half of the standalone deployment (improvise-9x6). Same
|
|
# terms as the client: a wasm build is the point, so a failure here is a
|
|
# broken deliverable rather than a portability regression.
|
|
- name: improvise-worker-server
|
|
run: cargo check -p improvise-worker-server --target wasm32-unknown-unknown
|
|
# Not in the browser bundle yet — nothing depends on improvise-gp. Gated
|
|
# anyway because it is a candidate (fitting a tree in the browser) and
|
|
# staying wasm-clean is free now, expensive to retrofit (improvise-z5eg).
|
|
- name: improvise-gp
|
|
run: cargo check -p improvise-gp --target wasm32-unknown-unknown
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The browser JavaScript's unit tests. Its own job, deliberately: a failure
|
|
# here is a decision `web/shell.js` or `web/serial.js` got wrong, and
|
|
# reporting that as "Browser harness" would put it behind an 80 MB browser
|
|
# download and two wasm-pack builds. It needs neither — `node --test` is in
|
|
# the runtime, and this job installs nothing at all.
|
|
js:
|
|
name: Browser JS unit tests (node --test)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
# The glob is the SHELL's, not node's: `web/tests/bundle/` needs a built
|
|
# wasm bundle and runs in the browser job instead.
|
|
- name: What the page decides between its boundaries
|
|
run: node --test web/tests/*.test.js
|
|
|
|
kb:
|
|
name: Knowledge base consistency
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: cachix/install-nix-action@v31
|
|
# kb.pl is executable knowledge, so it gets checked like code: rules
|
|
# must reject what they should, domains must close, and functional keys
|
|
# must not carry conflicting values. See kb_report/0 in the file.
|
|
- name: kb_consistent
|
|
run: |
|
|
nix shell nixpkgs#trealla --command tpl prolog-sessions/kb.pl \
|
|
-g "(kb_consistent -> halt(0) ; (kb_report, halt(1)))"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The browser harness (improvise-vvl3, absorbing improvise-xf1).
|
|
#
|
|
# This is the only job that RUNS any of the browser half. `OpfsFs`,
|
|
# `web/app.js` and `web/worker.js` were previously compile-checked,
|
|
# syntax-checked and read — never executed — so every bug in them was found
|
|
# by reading or by a person pressing keys, neither of which is repeatable.
|
|
#
|
|
# It can fail the build, deliberately: a harness nobody runs is worse than
|
|
# none, because it reads as coverage. Playwright's browser download is ~80 MB
|
|
# and takes well under a minute on ubuntu-latest, which is affordable next to
|
|
# the two wasm-pack builds this job already needs.
|
|
browser:
|
|
name: Browser harness (OPFS contract + shell)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: wasm32-unknown-unknown
|
|
# Named rather than assumed: this job lints harness/opfs-contract,
|
|
# which the `test` job's workspace-wide fmt and clippy cannot reach.
|
|
components: clippy, rustfmt
|
|
- uses: Swatinem/rust-cache@v2
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 22
|
|
- name: Install wasm-pack
|
|
env:
|
|
WASM_PACK_VERSION: v0.15.0
|
|
run: |
|
|
set -eu
|
|
url="https://github.com/rustwasm/wasm-pack/releases/download/${WASM_PACK_VERSION}/wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl.tar.gz"
|
|
curl -sSfL "$url" | tar -xz --strip-components=1 -C /usr/local/bin \
|
|
"wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl/wasm-pack"
|
|
wasm-pack --version
|
|
# Not a workspace member (it only builds for wasm32), so the `test` job's
|
|
# workspace-wide fmt and clippy do not reach it.
|
|
- name: Lint the harness crate
|
|
working-directory: harness/opfs-contract
|
|
run: |
|
|
cargo fmt --all -- --check
|
|
cargo clippy --target wasm32-unknown-unknown --all-targets -- -D warnings
|
|
- name: Install the harness
|
|
working-directory: harness
|
|
run: npm ci
|
|
# All three, because improvise-xf1's acceptance criterion names Chrome,
|
|
# Firefox and Safari and `createSyncAccessHandle` is implemented
|
|
# separately by each engine. WebKit is installed even though its OPFS is
|
|
# absent today: the spec skips on a runtime probe, so the day that build
|
|
# gains OPFS the check starts running without a CI change.
|
|
- name: Install browsers
|
|
working-directory: harness
|
|
run: npx playwright install --with-deps chromium firefox webkit
|
|
# Builds both site bundles, the harness's own wasm bundle, and assembles
|
|
# `harness/serve` — the same tree `pages-build` publishes, plus /opfs/.
|
|
- name: Build the harness
|
|
run: ./harness/build.sh
|
|
# The JS↔wasm boundary: every call web/app.js makes is declared by the
|
|
# bundle, with that arity. It needs the bundle and nothing else, so it
|
|
# runs here rather than in the `js` job, and before Playwright — a
|
|
# renamed export is cheaper to read here than as a blank page.
|
|
- name: The page's calls match the bundle it calls
|
|
run: node --test web/tests/bundle/*.test.js
|
|
- name: Run the harness
|
|
working-directory: harness
|
|
run: npx playwright test
|
|
- if: failure()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: playwright-traces
|
|
path: harness/test-results
|
|
retention-days: 7
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GitHub Pages delivery (improvise-d31).
|
|
#
|
|
# Two jobs on purpose. `pages-build` runs on every push and pull request, so
|
|
# a change that breaks `web/build.sh` or the bundle fails on the PR that made
|
|
# it — the build is a gate. `pages-deploy` runs only on a push to main and
|
|
# `needs` the check jobs above, so nothing that failed `cargo test`, the
|
|
# wasm32 gate or the browser harness can reach the published site. The deploy
|
|
# depends on those checks rather than repeating them (improvise-d31 AC 2).
|
|
#
|
|
# Not path-filtered, deliberately. The bundles are `improvise-wasm-client` and
|
|
# `improvise-worker-server` plus, between them, most of the workspace — the
|
|
# worker bundle is the whole App: core, formula, io, protocol and command. A
|
|
# filter
|
|
# that meant "skip when the bundle cannot have changed" would have to name
|
|
# most of the workspace, and the failure mode of getting it wrong is a stale
|
|
# site with no signal that it is stale. A redundant build is the cheaper error.
|
|
pages-build:
|
|
name: Browser bundle + static site
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- uses: dtolnay/rust-toolchain@stable
|
|
with:
|
|
targets: wasm32-unknown-unknown
|
|
- uses: Swatinem/rust-cache@v2
|
|
# Pinned release binary rather than `cargo install` (a three-minute
|
|
# source build) or a third-party action. wasm-pack runs its own bundled
|
|
# `wasm-opt`, which is why improvise-wasm-client's Cargo.toml names
|
|
# `--enable-bulk-memory`: without it the optimizer rejects the
|
|
# `memory.copy` LLVM emits regardless.
|
|
# v0.15.0 is what `nix develop` supplies, and the bundle was built and
|
|
# measured against it locally (346 KB, 120 KB gzipped). Pinning the same
|
|
# version is what makes that local run evidence about this one.
|
|
- name: Install wasm-pack
|
|
env:
|
|
WASM_PACK_VERSION: v0.15.0
|
|
run: |
|
|
set -eu
|
|
url="https://github.com/rustwasm/wasm-pack/releases/download/${WASM_PACK_VERSION}/wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl.tar.gz"
|
|
curl -sSfL "$url" | tar -xz --strip-components=1 -C /usr/local/bin \
|
|
"wasm-pack-${WASM_PACK_VERSION}-x86_64-unknown-linux-musl/wasm-pack"
|
|
wasm-pack --version
|
|
|
|
# The same script a developer runs locally, so there is one answer to
|
|
# "how is the bundle built" rather than two that can drift.
|
|
- name: Build the wasm bundle
|
|
run: ./web/build.sh
|
|
|
|
# `web/assemble.sh`, not an inline copy of it: the browser harness job
|
|
# below assembles the same tree, and a harness that runs against a
|
|
# different arrangement of files is testing something nobody deploys.
|
|
- name: Assemble the site
|
|
run: ./web/assemble.sh site "$GITHUB_SHA"
|
|
|
|
- run: find site -type f | sort
|
|
- run: du -sh site
|
|
|
|
- uses: actions/upload-pages-artifact@v5
|
|
with:
|
|
path: site
|
|
|
|
pages-deploy:
|
|
name: Deploy to GitHub Pages
|
|
needs: [test, wasm, js, browser, pages-build]
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
pages: write
|
|
id-token: write
|
|
# A superseded deploy is allowed to finish rather than being cancelled:
|
|
# `deploy-pages` cancelled midway can leave the Pages deployment in a
|
|
# half-applied state, and the next push republishes anyway.
|
|
concurrency:
|
|
group: pages
|
|
cancel-in-progress: false
|
|
environment:
|
|
name: github-pages
|
|
url: ${{ steps.deployment.outputs.page_url }}
|
|
steps:
|
|
# `enablement: false` (the default) configures the environment URL
|
|
# without attempting to create or modify the Pages site via the API.
|
|
# GitHub Pages must be enabled once in the repository settings
|
|
# (Settings → Pages → Source: GitHub Actions) before this job will
|
|
# produce a live URL. Using `enablement: true` fails when the workflow
|
|
# token lacks admin permission to create the site (improvise-3gy).
|
|
- uses: actions/configure-pages@v6
|
|
with:
|
|
enablement: false
|
|
- id: deployment
|
|
uses: actions/deploy-pages@v5
|