Skip to content

PRO — 9/10: results, report, view and model tools #1539

PRO — 9/10: results, report, view and model tools

PRO — 9/10: results, report, view and model tools #1539

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
backend-test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
- name: Backend tests
run: cargo test --locked -p dedaliano-backend
lint:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: clippy, rust-src
# Noninteractive and needrestart-proof: an apt prompt or a stalled mirror
# here hangs the job until the 6 h default timeout (seen on main and on
# PR #154, 2026-08-19 — the job sat in this step for over an hour). The
# step timeout turns the hang into a fast failure.
- name: Install mold linker
run: |
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y mold
timeout-minutes: 5
- uses: Swatinem/rust-cache@v2
with:
workspaces: engine
- name: Clippy
working-directory: engine
run: cargo clippy --lib -- -W clippy::all -A clippy::erasing_op
# Named correctness gates. Every gate's tests are EXCLUDED from the suite
# job below — each test must run exactly once per CI run. If you add a gate
# step here, add its filter to the suite exclusion as well (verified with:
# cargo nextest list --profile ci -E '<filter>' | wc -l — suite + gates must
# equal the old combined filter's count).
test:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
# Noninteractive and needrestart-proof: an apt prompt or a stalled mirror
# here hangs the job until the 6 h default timeout (seen on main and on
# PR #154, 2026-08-19 — the job sat in this step for over an hour). The
# step timeout turns the hang into a fast failure.
- name: Install mold linker
run: |
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y mold
timeout-minutes: 5
- uses: Swatinem/rust-cache@v2
with:
workspaces: engine
- uses: taiki-e/install-action@nextest
- name: Shell benchmarks gate
working-directory: engine
run: cargo nextest run --profile ci -E 'test(/benchmark_shell/) + test(/benchmark_beam_shell/) + test(/benchmark_plate/) + test(/benchmark_navier/) + test(/benchmark_scordelis/) + test(/benchmark_cantilever/) + test(/benchmark_pinched/) + test(/test_quad_thin_plate/)'
- name: Shell acceptance gate
working-directory: engine
run: cargo nextest run --profile ci -E 'test(/acceptance_.*shell/) + test(/acceptance_.*diaphragm/)'
- name: Constraint benchmarks gate
working-directory: engine
run: cargo nextest run --profile ci -E 'test(/benchmark_.*constraint/)'
- name: Sparse shell gates
working-directory: engine
run: cargo nextest run --profile ci --test sparse_shell_gates
- name: Solver invariants gate
working-directory: engine
run: cargo nextest run --profile ci --test solver_invariants
- name: Solver CI coverage gate
working-directory: engine
run: cargo nextest run --profile ci --test solver_ci_coverage
- name: Conditioning adversarial gate
working-directory: engine
run: cargo nextest run --profile ci --test conditioning_adversarial
# Wall-clock timing gates: run single-threaded so elapsed time reflects
# CPU work, not contention from co-scheduled tests on shared 2-4 vCPU
# runners. Measuring wall time under parallel execution made these flaky.
- name: Performance regression gates
working-directory: engine
run: cargo nextest run --profile ci --test perf_regression_gates --test-threads 1
- name: Advanced perf gates
working-directory: engine
run: cargo nextest run --profile ci --test perf_regression_advanced --test-threads 1
- name: k_full overbuild gates
working-directory: engine
run: cargo nextest run --profile ci --test kfull_overbuild_gates
# The remaining ~6700 tests, sharded 2-way. Excludes the named gates above
# (they run in the `test` job) and the wall-clock timing benchmarks/perf
# binaries (validated in their dedicated single-threaded gate steps).
suite:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
# Noninteractive and needrestart-proof: an apt prompt or a stalled mirror
# here hangs the job until the 6 h default timeout (seen on main and on
# PR #154, 2026-08-19 — the job sat in this step for over an hour). The
# step timeout turns the hang into a fast failure.
- name: Install mold linker
run: |
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y mold
timeout-minutes: 5
- uses: Swatinem/rust-cache@v2
with:
workspaces: engine
- uses: taiki-e/install-action@nextest
- name: Run test suite (shard ${{ matrix.shard }}/2)
working-directory: engine
run: cargo nextest run --profile ci -E 'all() - test(harmonic_phase_breakdown) - test(harmonic_modal_vs_direct_timing) - binary(perf_regression_advanced) - binary(perf_regression_gates) - (test(/benchmark_shell/) + test(/benchmark_beam_shell/) + test(/benchmark_plate/) + test(/benchmark_navier/) + test(/benchmark_scordelis/) + test(/benchmark_cantilever/) + test(/benchmark_pinched/) + test(/test_quad_thin_plate/)) - (test(/acceptance_.*shell/) + test(/acceptance_.*diaphragm/)) - test(/benchmark_.*constraint/) - binary(sparse_shell_gates) - binary(solver_invariants) - binary(solver_ci_coverage) - binary(conditioning_adversarial) - binary(kfull_overbuild_gates)' --partition count:${{ matrix.shard }}/2
timeout-minutes: 20
# Criterion reports are informational (continue-on-error): they must not gate
# PRs, so they only run on main. Artifact cadence is unchanged (every main push).
bench:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
# Noninteractive and needrestart-proof: an apt prompt or a stalled mirror
# here hangs the job until the 6 h default timeout (seen on main and on
# PR #154, 2026-08-19 — the job sat in this step for over an hour). The
# step timeout turns the hang into a fast failure.
- name: Install mold linker
run: |
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y mold
timeout-minutes: 5
- uses: Swatinem/rust-cache@v2
with:
workspaces: engine
- name: Benchmarks compile
working-directory: engine
run: cargo bench --no-run
# Informational only: criterion reports are uploaded as artifacts below.
# Benchmark wall time on shared runners must not gate CI (same rationale
# as the single-threaded perf gates above) — slow runners exceeded the
# timeout with no correctness signal in the failure.
- name: Run criterion benchmarks (quick)
working-directory: engine
run: cargo bench --bench solver_bench --bench assembly_bench --bench workflow_bench -- --sample-size 10 --warm-up-time 1 --measurement-time 3
timeout-minutes: 15
continue-on-error: true
- name: Upload criterion reports
if: always()
uses: actions/upload-artifact@v4
with:
name: criterion-reports
path: engine/target/criterion
retention-days: 30
if-no-files-found: warn
# The web suite runs as two jobs in parallel. It used to be one job that built
# the app and then ran both Vitest passes back to back: 11–16 min, most of it
# the unit pass waiting on its longest files. The unit pass needs the WASM
# engine and nothing else, so it shards two ways here; the production-build
# tests need Chromium and the built app, so they stay with the build.
web-unit:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- uses: Swatinem/rust-cache@v2
with:
workspaces: engine
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: web/package-lock.json
- name: Build WASM engine
working-directory: engine
run: wasm-pack build --target web --out-dir ../web/src/lib/wasm --no-opt
- name: Install dependencies
working-directory: web
run: npm ci
- name: Unit + integration tests (shard ${{ matrix.shard }}/2)
working-directory: web
run: npx vitest run --project unit --shard=${{ matrix.shard }}/2
web-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- uses: Swatinem/rust-cache@v2
with:
workspaces: engine
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: web/package-lock.json
- name: Build WASM engine
working-directory: engine
run: wasm-pack build --target web --out-dir ../web/src/lib/wasm --no-opt
- name: Install dependencies
working-directory: web
run: npm ci
# `npm run build` prerenders the public pages by driving the app in a
# browser (web/scripts/prerender.ts). Chromium is a build input now.
- name: Install Chromium for prerendering
working-directory: web
run: npx playwright install --with-deps chromium
- name: Build
working-directory: web
run: npm run build
- name: Production-build tests
working-directory: web
run: npx vitest run --project build
# Five shards, each running whole spec files chosen by measured duration
# (web/scripts/e2e-shard.mjs). One runner took 40 min for the smoke suite:
# 569 tests of ~3 s each, serially — `workers: 1` stays, for the WebGL
# determinism the config explains, so the parallelism is across runners.
# `playwright --shard` splits by test count in file order and put the three
# heaviest files in one shard (12.6 vs 7.8 min); by duration it is 7.5 each.
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- uses: Swatinem/rust-cache@v2
with:
workspaces: engine
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: web/package-lock.json
# The WASM glue is gitignored, so it must be built here. Without it Vite
# substitutes a stub solver and every design assertion would pass vacuously —
# the e2e readiness gate (`__stabileo.solverReady()`) fails loudly if that
# ever happens.
- name: Build WASM engine
working-directory: engine
run: wasm-pack build --target web --out-dir ../web/src/lib/wasm --no-opt
- name: Install dependencies
working-directory: web
run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: pw-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}
- name: Install Chromium
working-directory: web
run: npx playwright install --with-deps chromium
# BLOCKING: functional DOM + hook assertions.
#
# `@landing` was added to this grep on 2026-08-19 and taken out the same
# day. The whole landing suite failed here in a way it does not fail
# locally: the twelve blog cases passed, the first landing case passed,
# and from the second onwards EVERY test timed out at 60 s "while setting
# up context" — `browser.newContext` never returning, one wedged browser
# rather than any assertion about the page. With `workers: 1` the landing
# spec runs after ~190 heavier cases, and the leading suspicion is the
# hero's continuous rAF animation under software GL on a 2-core runner,
# not anything the spec asserts. Diagnosing that belongs to whoever owns
# this harness; guessing at it through 30-minute CI iterations does not.
#
# The blog spec is tagged `@smoke` directly, so the public pages are not
# entirely unguarded here. `npx playwright test --grep @landing` is the
# local command for the rest.
- name: E2E smoke suite (shard ${{ matrix.shard }}/5)
working-directory: web
run: npx playwright test --grep @smoke $(node scripts/e2e-shard.mjs ${{ matrix.shard }} 5)
# Heavier suite (408-member model): main + opt-in label only.
# The slow and visual suites are small; they run once, in shard 1.
- name: E2E slow suite
if: matrix.shard == 1 && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run-e2e'))
working-directory: web
run: npx playwright test --grep "@slow" --grep-invert "visual baselines"
# NON-BLOCKING on this first landing (approved decision O7): pixel gates that
# block on day one are how e2e suites end up disabled. Promote once the Linux
# baselines have proven stable across a few runs.
#
# LINUX BASELINES: now committed under web/e2e/__screenshots__/linux/, so this
# step COMPARES against them. It must not pass --update-snapshots: that would
# overwrite the baselines every run and leave the pixel gate permanently inert,
# silently accepting any visual regression.
#
# Still continue-on-error and the two assertions are still expect.soft, so a
# mismatch is informational for this first landing. Promote to blocking once the
# baselines have proven stable. To intentionally refresh them, run the job once
# with UPDATE_SNAPSHOTS=1.
- name: E2E visual baselines (non-blocking comparison)
if: matrix.shard == 1 && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run-e2e'))
continue-on-error: true
working-directory: web
run: npx playwright test --grep "visual baselines" ${{ vars.UPDATE_SNAPSHOTS == '1' && '--update-snapshots' || '' }}
# Uploaded for diagnostics: on a mismatch this carries the actual/diff images.
- name: Upload Linux screenshot baselines / diffs
if: always() && matrix.shard == 1
uses: actions/upload-artifact@v4
with:
name: linux-screenshot-baselines
path: web/e2e/__screenshots__/linux
retention-days: 30
if-no-files-found: ignore
# Every e2e failure since this landed has been undiagnosable, and this step is
# why: both paths are DOT-directories, and `upload-artifact@v4` excludes hidden
# files unless told otherwise. The run log says so plainly —
# include-hidden-files: false
# No files were found with the provided path: web/e2e/.report
# — and `if-no-files-found: ignore` turned that into silence rather than a
# warning. So the traces, videos, failure screenshots and the HTML report were
# produced on every run and thrown away on every run, which is how one canvas
# test stayed red across three unrelated PRs with nobody able to see what it
# saw. `warn` restores the signal if the paths ever go stale again.
- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts-${{ matrix.shard }}
path: |
web/e2e/.report
web/e2e/.artifacts
include-hidden-files: true
retention-days: 14
if-no-files-found: warn