- Rename course/11-break-your-engine to 13-break-your-engine and update every link and mention; start-module explains that Module 13 has no tests to start. - tools/diff-tracks.mjs (npm run diff:tracks): stages both references of a module, generates a seeded random SpellGrid level and script per seed, pipes it to the Rust and TypeScript CLIs and diffs their output; crashes on one or both sides fail. stagePlay takes a module number. - register_assembled: the TypeScript track compared only the text of an existing Assembled card, so a base re-registered with a new cost (or a different split) silently rewrote the earlier card. It now compares every field the engine reads, as Rust does (Modules 5-10). New test_05_assemble_never_rewrites_an_earlier_card. - SpellGrid CLIs: coordinates and chooser numbers are 1-9 plain digits in both tracks (Rust accepted signs, TypeScript accepted "+2", "0x2", "1e0"); Rust quotes card names verbatim instead of with Debug escapes; Rust joins the minion list with "; " instead of rewriting ", ". Two new CLI tests per track. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.5 KiB
JavaScript
113 lines
4.5 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { run, copyFixture, fixtures } from "./helpers.mjs";
|
|
|
|
const env = { COURSE_ROOT: join(fixtures, "course") };
|
|
|
|
test("start_module_copies_tests", () => {
|
|
const ws = copyFixture("ws-start");
|
|
const r = run("start-module.mjs", ["01", ws], env);
|
|
assert.equal(r.code, 0, r.out);
|
|
assert.ok(existsSync(join(ws, "tests/m01.rs")));
|
|
assert.match(r.out, /newGame, summon, play/);
|
|
});
|
|
|
|
test("start_module_copies_course_cards_from_module_5", () => {
|
|
// Spec §6.2a: Modules 1-4 keep their cards in the learner's code; course/cards
|
|
// (with the M3/M4 drop answers and the M5 data format) arrives with Module 5.
|
|
for (const [num, prev] of [["01", null], ["04", "m03.rs"]]) {
|
|
const ws = copyFixture("ws-start");
|
|
if (prev) {
|
|
mkdirSync(join(ws, "tests"), { recursive: true });
|
|
writeFileSync(join(ws, "tests", prev), "");
|
|
}
|
|
const r = run("start-module.mjs", [num, ws], env);
|
|
assert.equal(r.code, 0, r.out);
|
|
assert.ok(!existsSync(join(ws, "cards/test-fixture.json")), `module ${num} gets no course cards`);
|
|
}
|
|
const ws = copyFixture("ws-start");
|
|
mkdirSync(join(ws, "tests"), { recursive: true });
|
|
writeFileSync(join(ws, "tests/m04.rs"), "");
|
|
const r = run("start-module.mjs", ["05", ws], env);
|
|
assert.equal(r.code, 0, r.out);
|
|
assert.ok(existsSync(join(ws, "cards/test-fixture.json")));
|
|
});
|
|
|
|
test("start_module_copies_module_cards_up_to_the_module", () => {
|
|
const early = copyFixture("ws-start");
|
|
assert.equal(run("start-module.mjs", ["01", early], env).code, 0);
|
|
assert.ok(!existsSync(join(early, "cards/module-fixture.json")));
|
|
const later = copyFixture("ws-start");
|
|
mkdirSync(join(later, "tests"), { recursive: true });
|
|
writeFileSync(join(later, "tests/m02.rs"), "");
|
|
const r = run("start-module.mjs", ["03", later], env);
|
|
assert.equal(r.code, 0, r.out);
|
|
assert.ok(!existsSync(join(later, "cards/test-fixture.json")));
|
|
assert.ok(existsSync(join(later, "cards/module-fixture.json")));
|
|
});
|
|
|
|
test("start_module_explains_missing_course_json", () => {
|
|
const ws = copyFixture("ws-content-only");
|
|
const r = run("start-module.mjs", ["01", join(ws, "no-such-dir")], env);
|
|
assert.equal(r.code, 1);
|
|
assert.match(r.out, /No \.course\.json/);
|
|
assert.match(r.out, /workspace-template/);
|
|
assert.doesNotMatch(r.out, /ENOENT|at .*\.mjs:\d+/);
|
|
});
|
|
|
|
test("start_module_copies_module_levels", () => {
|
|
const ws = copyFixture("ws-start");
|
|
mkdirSync(join(ws, "tests"), { recursive: true });
|
|
writeFileSync(join(ws, "tests/m02.rs"), "");
|
|
const r = run("start-module.mjs", ["03", ws], env);
|
|
assert.equal(r.code, 0, r.out);
|
|
assert.ok(existsSync(join(ws, "levels/fixture-level.json")));
|
|
});
|
|
|
|
test("start_module_uses_track", () => {
|
|
const ws = copyFixture("ws-start-ts");
|
|
assert.equal(run("start-module.mjs", ["01", ws], env).code, 0);
|
|
assert.ok(existsSync(join(ws, "tests/m01.test.ts")));
|
|
assert.ok(!existsSync(join(ws, "tests/m01.rs")));
|
|
});
|
|
|
|
test("start_module_accepts_when_previous_present", () => {
|
|
const ws = copyFixture("ws-start");
|
|
mkdirSync(join(ws, "tests"), { recursive: true });
|
|
writeFileSync(join(ws, "tests/m02.rs"), "");
|
|
const r = run("start-module.mjs", ["03", ws], env);
|
|
assert.equal(r.code, 0, r.out);
|
|
assert.ok(existsSync(join(ws, "tests/m03.rs")));
|
|
});
|
|
|
|
test("start_module_refuses_skipping", () => {
|
|
const ws = copyFixture("ws-start");
|
|
mkdirSync(join(ws, "tests"), { recursive: true });
|
|
writeFileSync(join(ws, "tests/m01.rs"), "");
|
|
const r = run("start-module.mjs", ["03", ws], env);
|
|
assert.equal(r.code, 1);
|
|
assert.match(r.out, /finish module 02 first/);
|
|
assert.ok(!existsSync(join(ws, "tests/m03.rs")));
|
|
});
|
|
|
|
test("start_module_00_not_startable", () => {
|
|
const ws = copyFixture("ws-start");
|
|
const r = run("start-module.mjs", ["00", ws], env);
|
|
assert.notEqual(r.code, 0);
|
|
assert.match(r.out, /naive-engine/);
|
|
});
|
|
|
|
test("start_module_explains_a_module_with_no_tests", () => {
|
|
// A module made only of reading and exercises (Module 13): no tests for either track.
|
|
const course = copyFixture("course");
|
|
mkdirSync(join(course, "06-no-tests"));
|
|
writeFileSync(join(course, "06-no-tests", "lesson.md"), "# Module 6\n");
|
|
const ws = copyFixture("ws-start");
|
|
const r = run("start-module.mjs", ["06", ws], { COURSE_ROOT: course });
|
|
assert.equal(r.code, 1);
|
|
assert.match(r.out, /Module 06 has no tests to start/);
|
|
assert.doesNotMatch(r.out, /finish module/);
|
|
});
|