CI / Tool tests and sync-check (push) Successful in 13s
CI / Verify references (all modules, Rust and TS) (push) Successful in 1m32s
CI / Verify Expansion Drop solutions (all modules, Rust and TS) (push) Successful in 1m35s
CI / Coupling audit on references (push) Successful in 13s
CI / Site build (push) Successful in 25s
CI / Naive engine tests (Module 0) (push) Successful in 32s
- verify-references --expansion overlays expansion/solution on each reference and runs the cumulative tests plus the drop test; refuses a solution that touches src/engine/ - CI job verify-expansions - README: workspace-template + git init step, tool list, repo disclaimer link - Cargo.lock for every Rust reference (staged with the reference, pins serde for rustc 1.85.1) Co-Authored-By: Claude Opus 5.5 <[email protected]>
132 lines
6.1 KiB
JavaScript
132 lines
6.1 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { join } from "node:path";
|
|
import { run, fixtures } from "./helpers.mjs";
|
|
import { planVerification } from "../verify-references.mjs";
|
|
import { moduleDirs } from "../lib.mjs";
|
|
|
|
const root = join(fixtures, "course");
|
|
|
|
test("verify_plan_skips_modules_without_reference", () => {
|
|
process.env.COURSE_ROOT = root;
|
|
const plan = planVerification(moduleDirs(), 3);
|
|
assert.deepEqual(plan.map((p) => p.num), ["01", "02", "03"]);
|
|
assert.deepEqual(plan[2].testModules.map((m) => m.num), ["01", "02", "03"]);
|
|
assert.deepEqual(plan[2].langs, ["rust", "ts"]);
|
|
});
|
|
|
|
test("verify_runs_with_copied_reference_and_tests", () => {
|
|
const r = run("verify-references.mjs", ["02"], {
|
|
COURSE_ROOT: root,
|
|
VERIFY_CMD_RUST: "test -f tests/m01.rs && test -f Cargo.toml && ! test -f tests/m03.rs",
|
|
VERIFY_CMD_TS: "test -f tests/m01.test.ts && test -f package.json",
|
|
});
|
|
assert.equal(r.code, 0, r.out);
|
|
assert.match(r.out, /01.*ok/s);
|
|
assert.match(r.out, /02.*ok/s);
|
|
});
|
|
|
|
test("verify_reports_failure", () => {
|
|
const r = run("verify-references.mjs", ["01"], {
|
|
COURSE_ROOT: root,
|
|
VERIFY_CMD_RUST: "false",
|
|
VERIFY_CMD_TS: "true",
|
|
});
|
|
assert.equal(r.code, 1);
|
|
assert.match(r.out, /FAIL/);
|
|
});
|
|
|
|
test("verify_stage_copies_all_earlier_module_tests", async () => {
|
|
process.env.COURSE_ROOT = root;
|
|
const { planVerification, stage } = await import("../verify-references.mjs");
|
|
const { existsSync } = await import("node:fs");
|
|
const plan = planVerification(moduleDirs(), 3)[2];
|
|
const dir = stage(plan, "rust");
|
|
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_stage_copies_course_cards_from_module_5", async () => {
|
|
// From Module 5 the references load card data from <crate or package root>/cards;
|
|
// Modules 1-4 keep their cards in code and get no course/cards (spec §6.2a).
|
|
process.env.COURSE_ROOT = root;
|
|
const { planVerification, stage } = await import("../verify-references.mjs");
|
|
const { existsSync } = await import("node:fs");
|
|
const plans = planVerification(moduleDirs(), 5);
|
|
for (const lang of ["rust", "ts"]) {
|
|
assert.ok(!existsSync(join(stage(plans[0], lang), "cards", "test-fixture.json")), `01 ${lang}`);
|
|
assert.ok(!existsSync(join(stage(plans[3], lang), "cards", "test-fixture.json")), `04 ${lang}`);
|
|
assert.ok(existsSync(join(stage(plans[4], lang), "cards", "test-fixture.json")), `05 ${lang}`);
|
|
}
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
test("verify_stage_copies_module_levels", async () => {
|
|
// From Module 10 the tests load puzzle levels from <crate or package root>/levels.
|
|
process.env.COURSE_ROOT = root;
|
|
const { planVerification, stage } = await import("../verify-references.mjs");
|
|
const { existsSync } = await import("node:fs");
|
|
const plans = planVerification(moduleDirs(), 3);
|
|
assert.ok(existsSync(join(stage(plans[2], "ts"), "levels", "fixture-level.json")));
|
|
assert.ok(!existsSync(join(stage(plans[1], "ts"), "levels")));
|
|
});
|
|
|
|
test("verify_stage_copies_module_cards_up_to_the_module", async () => {
|
|
// Module-local cards (Module 10's SpellGrid cards) reach that module and later ones only.
|
|
process.env.COURSE_ROOT = root;
|
|
const { planVerification, stage } = await import("../verify-references.mjs");
|
|
const { existsSync } = await import("node:fs");
|
|
const plans = planVerification(moduleDirs(), 3);
|
|
for (const lang of ["rust", "ts"]) {
|
|
assert.ok(!existsSync(join(stage(plans[0], lang), "cards", "module-fixture.json")), lang);
|
|
assert.ok(existsSync(join(stage(plans[1], lang), "cards", "module-fixture.json")), lang);
|
|
const later = stage(plans[2], lang);
|
|
assert.ok(existsSync(join(later, "cards", "module-fixture.json")), lang);
|
|
}
|
|
});
|
|
|
|
test("verify_expansion_overlays_solution_and_drop_test", async () => {
|
|
// --expansion: the reference, the cumulative tests, then the module's drop solution
|
|
// (code over the root, cards/ into cards/, words.json into src/content/) and drop test.
|
|
process.env.COURSE_ROOT = root;
|
|
const { planVerification, stage, overlayExpansion, hasExpansion } = await import("../verify-references.mjs");
|
|
const { existsSync } = await import("node:fs");
|
|
const plan = planVerification(moduleDirs(), 1)[0];
|
|
assert.ok(hasExpansion(plan, "rust"));
|
|
assert.ok(!hasExpansion(planVerification(moduleDirs(), 2)[1], "rust"));
|
|
const dir = overlayExpansion(plan, "rust", stage(plan, "rust"));
|
|
for (const f of ["Cargo.toml", "tests/m01.rs", "tests/m01x.rs", "src/content/drop.rs", "src/content/words.json", "cards/test-drop.json"]) {
|
|
assert.ok(existsSync(join(dir, f)), f);
|
|
}
|
|
});
|
|
|
|
test("verify_expansion_rejects_solution_that_changes_engine", async () => {
|
|
const { overlayExpansion } = await import("../verify-references.mjs");
|
|
const { mkdirSync, mkdtempSync, writeFileSync } = await import("node:fs");
|
|
const { tmpdir } = await import("node:os");
|
|
const mod = mkdtempSync(join(tmpdir(), "sdc-exp-"));
|
|
mkdirSync(join(mod, "expansion/solution/ts/src/engine"), { recursive: true });
|
|
writeFileSync(join(mod, "expansion/solution/ts/src/engine/core.ts"), "");
|
|
assert.throws(() => overlayExpansion({ num: "09", path: mod }, "ts", mkdtempSync(join(tmpdir(), "sdc-st-"))), /changes the engine/);
|
|
});
|
|
|
|
test("verify_expansion_cli_runs_only_modules_with_a_solution", () => {
|
|
const r = run("verify-references.mjs", ["02", "--expansion"], {
|
|
COURSE_ROOT: root,
|
|
VERIFY_CMD_RUST: "test -f tests/m01x.rs && test -f src/content/drop.rs",
|
|
VERIFY_CMD_TS: "true",
|
|
});
|
|
assert.equal(r.code, 0, r.out);
|
|
assert.match(r.out, /module 01-m01 \[rust\] expansion: ok/);
|
|
assert.match(r.out, /module 02-m02 \[rust\] expansion: skipped/);
|
|
});
|