ci: Phase 1d-tests I + K + L — workflow hardening + nightly library-compile

K (frontend-tests.yml reinforced):
  • Matrix node-version: [20, 22] — catches Node-version-specific bugs
  • Cache the 24 MB ngspice WASM by hash — saves ~10s/run
  • `npm run tsc` step (continue-on-error: pre-existing strict errors
    in unrelated test files; tracked but not blocking)
  • `npm run build` — Vite production build smoke catches Rollup/
    Vite-only failures that vitest doesn't see (manualChunks wiring,
    dynamic import paths, asset resolution)
  • `npm run test:coverage` + upload as artifact (Node 22 only)

L (package.json scripts):
  • `tsc` → `tsc -b`
  • `test:libraries` → `RUN_LIBRARY_TESTS=1 vitest run
    src/__tests__/library-compile.integration.test.ts`

I (library-compile nightly):
  • New `.github/workflows/library-compile.yml` — 5 AM UTC cron +
    workflow_dispatch. Not on PRs (slow + external deps).
  • Sets up arduino-cli + caches `~/.arduino15` cores (avr, esp32,
    rp2040 — ~500 MB).
  • New `library-compile.integration.test.ts` — iterates every
    example with `code` + `libraries` + a known FQBN.  For each:
    arduino-cli lib install → write .ino → arduino-cli compile.
    7 examples currently match (epaper-displays).
  • Gated behind RUN_LIBRARY_TESTS=1; default vitest skips the file.

Final tally: 1853 tests pass (was 1461 before Phase 1d-tests — +392
new sub-tests across 8 new test files + 1 new workflow).  Vite build
green (2.68 MB main chunk, unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
davidmonterocrespo24 2026-05-15 23:19:11 +02:00
parent 68c19a6663
commit 07552b5d9e
4 changed files with 245 additions and 5 deletions

View File

@ -9,27 +9,39 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: ['20', '22']
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js 22
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: '22'
node-version: ${{ matrix.node-version }}
# The metadata generator scans wokwi-elements/src/, which only ships in
# the upstream repo (not on npm). Clone it once for the freshness check.
- name: Clone wokwi-elements (for metadata regeneration only)
run: git clone --depth=1 https://github.com/wokwi/wokwi-elements.git third-party/wokwi-elements
# Cache the vendored ngspice WASM (24 MB). Keyed by the file hash —
# invalidates only when the WASM blob itself changes (rare). Saves
# ~10s of disk write per CI run.
- name: Cache ngspice WASM
uses: actions/cache@v4
with:
path: frontend/public/wasm/ngspice-interactive
key: ngspice-wasm-${{ hashFiles('frontend/public/wasm/ngspice-interactive/ngspice-lib.wasm') }}
# No node_modules cache. Lock files are gitignored (see .gitignore) so
# a cache key tied to package-lock.json never invalidates and ends up
# restoring stale links to old `file:` deps from previous commits
# (e.g. wokwi-elements pre-npm migration). Fresh install every run is
# ~30s slower but actually correct.
- name: Install frontend dependencies
run: cd frontend && npm install --no-audit --no-fund --include=optional
@ -49,5 +61,31 @@ jobs:
exit 1
fi
- name: TypeScript build
run: cd frontend && npm run tsc
continue-on-error: true # tsc -b has pre-existing strict errors in unrelated test files; tracking separately
- name: Run tests
run: cd frontend && npm test
# Production build smoke — catches Vite/Rollup-only failures that
# vitest doesn't see (chunk wiring, dynamic imports, manualChunks
# config, asset resolution).
- name: Vite production build
run: cd frontend && npm run build
# Upload coverage as an artifact for download / inspection. Skip
# codecov for now (no org account). Run only on Node 22 to keep the
# artifact list deduped.
- name: Coverage report
if: matrix.node-version == '22'
run: cd frontend && npm run test:coverage
continue-on-error: true
- name: Upload coverage artifact
if: matrix.node-version == '22' && always()
uses: actions/upload-artifact@v4
with:
name: coverage-lcov
path: frontend/coverage/
if-no-files-found: ignore

74
.github/workflows/library-compile.yml vendored Normal file
View File

@ -0,0 +1,74 @@
name: Library compile (nightly)
# Phase 1d-tests I. Runs arduino-cli against every example that
# declares Arduino libraries, ensuring sketches still compile after
# upstream library bumps. Slow (~10 min) and external-dependent
# (arduino-cli + cores + libs from the index), so it's kept OUT of
# the PR workflow.
#
# Runs on a 5 AM UTC cron + on-demand via workflow_dispatch.
on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
jobs:
compile:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup arduino-cli
uses: arduino/setup-arduino-cli@v2
with:
version: 1.0.4
# Cache the Arduino core downloads (~500 MB total for avr + esp32 +
# rp2040). Invalidate when the upstream `arduino-cli core list`
# output changes — proxy via the lockfile alternative below.
- name: Cache Arduino cores
uses: actions/cache@v4
with:
path: |
~/.arduino15
~/Arduino
key: arduino-cores-v3-${{ runner.os }}
- name: Update arduino-cli core index
run: |
arduino-cli config init --overwrite
arduino-cli config add board_manager.additional_urls \
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json \
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
arduino-cli core update-index
- name: Install board cores
run: |
arduino-cli core install arduino:avr
arduino-cli core install esp32:esp32
arduino-cli core install rp2040:rp2040
- name: Install frontend dependencies
run: cd frontend && npm install --no-audit --no-fund --include=optional
- name: Compile every example that declares libraries
run: cd frontend && npm run test:libraries
env:
RUN_LIBRARY_TESTS: '1'
- name: Upload arduino-cli logs (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: arduino-cli-logs
path: /tmp/arduino-cli-*.log
if-no-files-found: ignore

View File

@ -24,7 +24,9 @@
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
"test:ui": "vitest --ui",
"tsc": "tsc -b",
"test:libraries": "RUN_LIBRARY_TESTS=1 vitest run src/__tests__/library-compile.integration.test.ts"
},
"dependencies": {
"@google/genai": "^0.6.0",
@ -75,4 +77,4 @@
"vite": "^7.3.1",
"vitest": "^4.0.18"
}
}
}

View File

@ -0,0 +1,126 @@
/**
* Library compile integration test (Phase 1d-tests I nightly).
*
* For every gallery example that declares external Arduino
* `libraries`, install them via arduino-cli + compile the example's
* sketch. Detects:
* Library API drift in upstream releases (a new Adafruit GFX
* major breaks every dependent sketch).
* Missing libraries in the index.
* Sketch syntax regressions across our codebase.
*
* Gated behind `RUN_LIBRARY_TESTS=1` so the default `npm test` skips
* the entire file arduino-cli isn't usually available in dev
* environments and the compile loop takes ~10 min. The nightly
* workflow `.github/workflows/library-compile.yml` sets the env var.
*
* Fidelity rule: imports the example arrays from the real source-of-
* truth modules. Adding a new example with `libraries` automatically
* extends this test.
*/
import { describe, it, expect } from 'vitest';
import { execSync } from 'node:child_process';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { analogExamples } from '../data/examples-analog';
import { digitalExamples } from '../data/examples-digital';
import { hundredDaysExamples } from '../data/examples-100-days';
import { epaperExamples } from '../data/examples-displays-epaper';
import { picowWifiExamples } from '../data/examples-picow-wifi';
import { circuitExamples } from '../data/examples-circuits';
import type { ExampleProject } from '../data/examples';
import { BOARD_KIND_FQBN, type BoardKind } from '../types/board';
const RUN_LIBRARY_TESTS = process.env.RUN_LIBRARY_TESTS === '1';
const ALL_EXAMPLES: ExampleProject[] = [
...analogExamples,
...digitalExamples,
...hundredDaysExamples,
...epaperExamples,
...picowWifiExamples,
...circuitExamples,
];
interface CompilableExample {
example: ExampleProject;
fqbn: string;
libraries: string[];
}
/**
* Filter to examples that:
* Have a `code` field (Arduino sketch)
* Have a non-empty `libraries` array (otherwise no value-add over
* a vanilla compile that already runs in dev)
* Map to a board with a known FQBN (no Pi 3B different toolchain)
*/
function eligibleExamples(): CompilableExample[] {
const out: CompilableExample[] = [];
for (const ex of ALL_EXAMPLES) {
if (!ex.code) continue;
if (!ex.libraries || ex.libraries.length === 0) continue;
const boardKind: BoardKind = (ex.boardType ?? 'arduino-uno') as BoardKind;
const fqbn = BOARD_KIND_FQBN[boardKind];
if (!fqbn) continue;
out.push({ example: ex, fqbn, libraries: ex.libraries });
}
return out;
}
const COMPILABLE = eligibleExamples();
describe.skipIf(!RUN_LIBRARY_TESTS)(
`arduino-cli library compile (${COMPILABLE.length} examples)`,
() => {
it.each(COMPILABLE.map((c) => [c.example.id, c] as const))(
'%s compiles with its declared libraries',
{ timeout: 300_000 },
(_id, compilable) => {
const { example, fqbn, libraries } = compilable;
// Install libs (idempotent — arduino-cli skips already-installed).
for (const lib of libraries) {
try {
execSync(`arduino-cli lib install "${lib}"`, {
stdio: 'pipe',
encoding: 'utf8',
timeout: 60_000,
});
} catch (err) {
throw new Error(
`[${example.id}] arduino-cli lib install "${lib}" failed: ${
err instanceof Error ? err.message : String(err)
}`,
);
}
}
// Sketch dir + .ino file.
const dir = mkdtempSync(path.join(tmpdir(), `velxio-${example.id}-`));
const inoPath = path.join(dir, `${path.basename(dir)}.ino`);
writeFileSync(inoPath, example.code ?? '', 'utf8');
try {
execSync(`arduino-cli compile --fqbn ${fqbn} "${dir}"`, {
stdio: 'pipe',
encoding: 'utf8',
timeout: 240_000,
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`[${example.id}] arduino-cli compile failed:\n${msg}`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
expect(true).toBe(true);
},
);
},
);
// Placeholder when disabled so the test runner shows "skipped" rather
// than "no tests".
describe.skipIf(RUN_LIBRARY_TESTS)('library-compile (set RUN_LIBRARY_TESTS=1 to enable)', () => {
it('placeholder', () => {
expect(RUN_LIBRARY_TESTS).toBe(false);
});
});