From 1453a00b9ca7849b3da82eff24b60e97079e960f Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 29 Oct 2026 00:12:48 +0100 Subject: [PATCH] fix: verify-references installs TS deps; sync-check covers expansion tests; template gitignore and passWithNoTests verify-references ran a bare `npx vitest run` in the staged reference, which fetched an unrelated vitest that could not load the reference config. It now installs the reference's devDependencies first. sync-check also checks each module's expansion/tests against ### headings in expansion/README.md. Co-Authored-By: Claude Opus 5.5 --- course/00-rewrite-trap/lesson.md | 4 ++-- tools/lib.mjs | 6 ++--- tools/sync-check.mjs | 23 ++++++++++++------- .../course-bad/01-a/expansion/README.md | 1 + .../course-bad/01-a/expansion/tests/rust/c.rs | 2 ++ .../course/01-m01/expansion/README.md | 3 +++ .../01-m01/expansion/tests/rust/m01x.rs | 2 ++ .../01-m01/expansion/tests/ts/m01x.test.ts | 2 ++ tools/test/sync-check.test.mjs | 6 +++++ tools/test/verify-references.test.mjs | 10 ++++++++ tools/verify-references.mjs | 14 +++++++---- workspace-template/rust/.gitignore | 1 + workspace-template/ts/.gitignore | 1 + workspace-template/ts/vitest.config.ts | 2 +- 14 files changed, 59 insertions(+), 18 deletions(-) create mode 100644 tools/test/fixtures/course-bad/01-a/expansion/README.md create mode 100644 tools/test/fixtures/course-bad/01-a/expansion/tests/rust/c.rs create mode 100644 tools/test/fixtures/course/01-m01/expansion/README.md create mode 100644 tools/test/fixtures/course/01-m01/expansion/tests/rust/m01x.rs create mode 100644 tools/test/fixtures/course/01-m01/expansion/tests/ts/m01x.test.ts create mode 100644 workspace-template/rust/.gitignore create mode 100644 workspace-template/ts/.gitignore diff --git a/course/00-rewrite-trap/lesson.md b/course/00-rewrite-trap/lesson.md index 13f4a30..8636625 100644 --- a/course/00-rewrite-trap/lesson.md +++ b/course/00-rewrite-trap/lesson.md @@ -244,10 +244,10 @@ cp -r workspace-template/ts ~/my-engine cd ~/my-engine git init && git add -A && git commit -m "start from template" npm install -npm test -- --passWithNoTests +npm test ``` -Copy the whole directory with `cp -r` so the hidden `.course.json` file (which records your track) comes along. With no tests yet, `cargo test` reports 0 tests, and `npm test` needs `--passWithNoTests` to exit cleanly. +Copy the whole directory with `cp -r` so the hidden `.course.json` file (which records your track) comes along. With no tests yet, both `cargo test` and `npm test` report 0 tests and exit cleanly. **Read the template's README.** Your engine goes in `src/engine/`, your cards in `src/content/`, and `src/harness` is the thin adapter the course's tests call. diff --git a/tools/lib.mjs b/tools/lib.mjs index a8ff1c3..cb47d6c 100644 --- a/tools/lib.mjs +++ b/tools/lib.mjs @@ -39,9 +39,9 @@ export function testNames(path, lang) { return [...names].sort(); } -/** `### test_…` headings in a module's scenarios.md. */ -export function scenarioNames(path) { - const f = join(path, "scenarios.md"); +/** `### test_…` headings in a module's scenarios.md (or another file in `path`). */ +export function scenarioNames(path, file = "scenarios.md") { + const f = join(path, file); if (!existsSync(f)) return []; const names = new Set(); for (const m of readFileSync(f, "utf8").matchAll(/^### (test_\w+)/gm)) names.add(m[1]); diff --git a/tools/sync-check.mjs b/tools/sync-check.mjs index 02f0566..bfee577 100644 --- a/tools/sync-check.mjs +++ b/tools/sync-check.mjs @@ -1,24 +1,31 @@ #!/usr/bin/env node // Verifies Rust tests, TS tests and scenarios.md list identical test names per module. +// A module's Expansion Drop (expansion/tests/{rust,ts}, scenarios as `### test_…` +// headings in expansion/README.md) is checked the same way, as a separate set. +import { join } from "node:path"; import { moduleDirs, testNames, scenarioNames } from "./lib.mjs"; const problems = []; -for (const mod of moduleDirs()) { +function check(label, path, scenarioFile) { const sets = { - rust: testNames(mod.path, "rust"), - ts: testNames(mod.path, "ts"), - scenarios: scenarioNames(mod.path), + rust: testNames(path, "rust"), + ts: testNames(path, "ts"), + scenarios: scenarioNames(path, scenarioFile), }; - if (Object.values(sets).every((s) => s.length === 0)) continue; + if (Object.values(sets).every((s) => s.length === 0)) return; const all = new Set(Object.values(sets).flat()); for (const name of [...all].sort()) { for (const [where, names] of Object.entries(sets)) { - if (!names.includes(name)) { - problems.push(`module ${mod.num}-${mod.slug}: ${name} missing from ${where}`); - } + if (!names.includes(name)) problems.push(`${label}: ${name} missing from ${where}`); } } } + +for (const mod of moduleDirs()) { + const label = `module ${mod.num}-${mod.slug}`; + check(label, mod.path, "scenarios.md"); + check(`${label} expansion`, join(mod.path, "expansion"), "README.md"); +} if (problems.length) { console.error("sync-check: mismatches\n" + problems.map((p) => " " + p).join("\n")); process.exit(1); diff --git a/tools/test/fixtures/course-bad/01-a/expansion/README.md b/tools/test/fixtures/course-bad/01-a/expansion/README.md new file mode 100644 index 0000000..935cb90 --- /dev/null +++ b/tools/test/fixtures/course-bad/01-a/expansion/README.md @@ -0,0 +1 @@ +# Expansion diff --git a/tools/test/fixtures/course-bad/01-a/expansion/tests/rust/c.rs b/tools/test/fixtures/course-bad/01-a/expansion/tests/rust/c.rs new file mode 100644 index 0000000..4717094 --- /dev/null +++ b/tools/test/fixtures/course-bad/01-a/expansion/tests/rust/c.rs @@ -0,0 +1,2 @@ +#[test] +fn test_01x_c() {} diff --git a/tools/test/fixtures/course/01-m01/expansion/README.md b/tools/test/fixtures/course/01-m01/expansion/README.md new file mode 100644 index 0000000..0165f5b --- /dev/null +++ b/tools/test/fixtures/course/01-m01/expansion/README.md @@ -0,0 +1,3 @@ +# Expansion + +### test_01x_a diff --git a/tools/test/fixtures/course/01-m01/expansion/tests/rust/m01x.rs b/tools/test/fixtures/course/01-m01/expansion/tests/rust/m01x.rs new file mode 100644 index 0000000..7936295 --- /dev/null +++ b/tools/test/fixtures/course/01-m01/expansion/tests/rust/m01x.rs @@ -0,0 +1,2 @@ +#[test] +fn test_01x_a() {} diff --git a/tools/test/fixtures/course/01-m01/expansion/tests/ts/m01x.test.ts b/tools/test/fixtures/course/01-m01/expansion/tests/ts/m01x.test.ts new file mode 100644 index 0000000..51f5ae0 --- /dev/null +++ b/tools/test/fixtures/course/01-m01/expansion/tests/ts/m01x.test.ts @@ -0,0 +1,2 @@ +import { test } from "vitest"; +test("test_01x_a", () => {}); diff --git a/tools/test/sync-check.test.mjs b/tools/test/sync-check.test.mjs index 9c478f3..e0548b5 100644 --- a/tools/test/sync-check.test.mjs +++ b/tools/test/sync-check.test.mjs @@ -14,3 +14,9 @@ test("sync_passes_consistent_course", () => { const r = run("sync-check.mjs", [], { COURSE_ROOT: join(fixtures, "course") }); assert.equal(r.code, 0, r.out); }); + +test("sync_checks_expansion_tests_against_readme", () => { + const r = run("sync-check.mjs", [], { COURSE_ROOT: join(fixtures, "course-bad") }); + assert.match(r.out, /01-a expansion: test_01x_c missing from ts/); + assert.match(r.out, /01-a expansion: test_01x_c missing from scenarios/); +}); diff --git a/tools/test/verify-references.test.mjs b/tools/test/verify-references.test.mjs index dcf89a1..854dd4f 100644 --- a/tools/test/verify-references.test.mjs +++ b/tools/test/verify-references.test.mjs @@ -45,3 +45,13 @@ test("verify_stage_copies_all_earlier_module_tests", async () => { for (const f of ["m01.rs", "m02.rs", "m03.rs"]) assert.ok(existsSync(join(dir, "tests", f))); assert.ok(existsSync(join(dir, "Cargo.toml"))); }); + +test("verify_ts_default_installs_dependencies_first", async () => { + // A staged reference has no node_modules; a bare `npx vitest` fetches the + // latest vitest, which cannot load the reference's vitest.config.ts. + const { commandFor } = await import("../verify-references.mjs"); + const cmd = commandFor("ts", {}); + assert.match(cmd, /^npm (ci|install)\b.*&& npx vitest run$/); + assert.equal(commandFor("ts", { VERIFY_CMD_TS: "true" }), "true"); + assert.equal(commandFor("rust", {}), "cargo test"); +}); diff --git a/tools/verify-references.mjs b/tools/verify-references.mjs index 402a712..3c8288f 100644 --- a/tools/verify-references.mjs +++ b/tools/verify-references.mjs @@ -6,11 +6,17 @@ import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { moduleDirs, testFiles, fail } from "./lib.mjs"; -const COMMANDS = { - rust: () => process.env.VERIFY_CMD_RUST || "cargo test", - ts: () => process.env.VERIFY_CMD_TS || "npx vitest run", +// A staged reference has no node_modules, so TS installs the reference's own +// pinned devDependencies first (a bare `npx vitest` would fetch an unrelated version). +const DEFAULTS = { + rust: "cargo test", + ts: "npm install --no-audit --no-fund --loglevel=error && npx vitest run", }; +export function commandFor(lang, env = process.env) { + return env[`VERIFY_CMD_${lang.toUpperCase()}`] || DEFAULTS[lang]; +} + export function planVerification(mods, upTo) { return mods .filter((m) => Number(m.num) >= 1 && Number(m.num) <= upTo) @@ -41,7 +47,7 @@ if (import.meta.main) { for (const p of plans) { for (const lang of p.langs) { const dir = stage(p, lang); - const r = spawnSync(COMMANDS[lang](), { cwd: dir, shell: true, encoding: "utf8" }); + const r = spawnSync(commandFor(lang), { cwd: dir, shell: true, encoding: "utf8" }); const ok = r.status === 0; failed ||= !ok; console.log(`module ${p.num}-${p.slug} [${lang}]: ${ok ? "ok" : "FAIL"}`); diff --git a/workspace-template/rust/.gitignore b/workspace-template/rust/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/workspace-template/rust/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/workspace-template/ts/.gitignore b/workspace-template/ts/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/workspace-template/ts/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/workspace-template/ts/vitest.config.ts b/workspace-template/ts/vitest.config.ts index 00988cf..a6f65eb 100644 --- a/workspace-template/ts/vitest.config.ts +++ b/workspace-template/ts/vitest.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ - test: { include: ["tests/**/*.test.ts"] }, + test: { include: ["tests/**/*.test.ts"], passWithNoTests: true }, });