The solver's position key reads stackAt (each tile oldest first). New tool tools/verify-levels.mjs checks course/10-spellgrid/levels/proofs.json: each level's shortest solution, and that it exhausts without its idea (neverRule) or on the naive engine. Levels 4-10, one course idea each, with test_10_level_N_solution in both tracks, scenarios and Lesson 10's list; l1 no longer matches l10, and proofs.json is not listed as a level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
2.6 KiB
JavaScript
50 lines
2.6 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { run, toolsDir } from "./helpers.mjs";
|
|
import { levelNames, parseArgs, resolveLevel } from "../play-spellgrid.mjs";
|
|
|
|
test("play_spellgrid_parses_args", () => {
|
|
assert.deepEqual(parseArgs(["rust"]), { lang: "rust", level: "l1", dir: null, stageOnly: false });
|
|
assert.deepEqual(parseArgs(["ts", "l2", "--dir", "x", "--stage-only"]), { lang: "ts", level: "l2", dir: "x", stageOnly: true });
|
|
});
|
|
|
|
test("play_spellgrid_stages_reference_cards_and_levels", () => {
|
|
// Uses the real course: the Module 10 Rust reference with every card it reads and the levels.
|
|
const dir = join(mkdtempSync(join(tmpdir(), "sdc-play-")), "ws");
|
|
const r = run("play-spellgrid.mjs", ["rust", "l3", "--dir", dir, "--stage-only"]);
|
|
assert.equal(r.code, 0, r.out);
|
|
for (const f of ["Cargo.toml", "src/cli.rs", "cards/whirlwind.json", "cards/powder-keg.json", "levels/l1-double-tap.json"]) {
|
|
assert.ok(existsSync(join(dir, f)), f);
|
|
}
|
|
assert.match(r.out, /cargo run --quiet --bin spellgrid -- levels\/l3-simultaneous-paradox\.json/);
|
|
// Re-staging the same directory is allowed.
|
|
assert.equal(run("play-spellgrid.mjs", ["rust", "--dir", dir, "--stage-only"]).code, 0);
|
|
});
|
|
|
|
test("play_spellgrid_refuses_unknown_level_and_foreign_dir", () => {
|
|
const foreign = mkdtempSync(join(tmpdir(), "sdc-play-"));
|
|
writeFileSync(join(foreign, "mine.txt"), "keep me");
|
|
const r = run("play-spellgrid.mjs", ["rust", "--dir", foreign, "--stage-only"]);
|
|
assert.equal(r.code, 1);
|
|
assert.match(r.out, /not empty/);
|
|
const bad = run("play-spellgrid.mjs", ["rust", "l99", "--dir", join(foreign, "ws"), "--stage-only"]);
|
|
assert.equal(bad.code, 1);
|
|
assert.match(
|
|
bad.out,
|
|
/no level "l99"\. Levels: l1-double-tap, l2-spliced-corpse, l3-simultaneous-paradox, l4-crossed-out, l5-sealed-keg, l6-melt-before-defeat, l7-clay-key, l8-nothing-dies, l9-skull-river, l10-friendly-fire\n/,
|
|
);
|
|
assert.equal(run("play-spellgrid.mjs", ["java"]).code, 1);
|
|
});
|
|
|
|
test("play_spellgrid_resolves_levels_by_number", () => {
|
|
// l1 is level 1, not level 10; proofs.json beside the levels is not a level.
|
|
const levels = join(toolsDir, "..", "course", "10-spellgrid", "levels");
|
|
assert.equal(resolveLevel(levels, "l1").path, join("levels", "l1-double-tap.json"));
|
|
assert.equal(resolveLevel(levels, "l10").path, join("levels", "l10-friendly-fire.json"));
|
|
assert.equal(resolveLevel(levels, "proofs"), null);
|
|
assert.ok(!levelNames(levels).includes("proofs"));
|
|
});
|