- cardDirs: course/cards only from Module 5 (spec 6.2a); fixture modules 04/05 and tests - coupling-audit: noun words match as UPPERCASE, PascalCase or a whole string literal; lowercase identifiers like key/flag pass; card names keep every variant - isMain() argv guard replaces import.meta.main in coupling-audit and verify-references; engines.node >=22 - expansion-check: git diff --no-renames so moving engine code into content fails - start-module and other tools: clear error when .course.json is missing - check-links: malformed URLs reported as invalid URL instead of throwing Co-Authored-By: Claude Opus 5.5 <[email protected]>
71 lines
2.8 KiB
JavaScript
71 lines
2.8 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { spawnSync } from "node:child_process";
|
|
import { run, fixtures, toolsDir } from "./helpers.mjs";
|
|
import { join } from "node:path";
|
|
|
|
const env = { COURSE_ROOT: join(fixtures, "course") };
|
|
const audit = (ws) => run("coupling-audit.mjs", [join(fixtures, ws)], env);
|
|
|
|
test("audit_flags_card_name_in_engine", () => {
|
|
const r = audit("ws-coupled");
|
|
assert.equal(r.code, 1);
|
|
assert.match(r.out, /core\.rs:2/);
|
|
assert.match(r.out, /core\.rs:3/);
|
|
assert.match(r.out, /Baron Rivendare/);
|
|
assert.doesNotMatch(r.out, /cards\.rs/);
|
|
});
|
|
|
|
test("audit_ignores_content_dir", () => {
|
|
const r = audit("ws-content-only");
|
|
assert.equal(r.code, 0, r.out);
|
|
});
|
|
|
|
test("audit_ignores_grammar_words", () => {
|
|
const r = audit("ws-grammar");
|
|
assert.equal(r.code, 0, r.out);
|
|
});
|
|
|
|
test("audit_ignores_mechanic_words_in_real_catalog", () => {
|
|
// Uses the real course/catalog.json (no COURSE_ROOT override).
|
|
const r = run("coupling-audit.mjs", [join(fixtures, "ws-mechanics")]);
|
|
assert.equal(r.code, 0, r.out);
|
|
});
|
|
|
|
test("audit_allows_ordinary_lowercase_identifiers", () => {
|
|
// `key`, `flag`, `wall`, `water` are noun words in the real catalog, but as lowercase
|
|
// identifiers they are ordinary code, not content coupling.
|
|
const r = run("coupling-audit.mjs", [join(fixtures, "ws-ordinary-words")]);
|
|
assert.equal(r.code, 0, r.out);
|
|
});
|
|
|
|
test("audit_flags_noun_words_as_names_strings_and_variants", () => {
|
|
// Uses the real catalog: string literal "flag", KEY, Kind::Door and "text:WALL" are coupling;
|
|
// the lowercase identifiers on line 1 are not.
|
|
const r = run("coupling-audit.mjs", [join(fixtures, "ws-noun-coupled")]);
|
|
assert.equal(r.code, 1);
|
|
assert.doesNotMatch(r.out, /core\.rs:1:/);
|
|
assert.match(r.out, /core\.rs:2: names "FLAG"/);
|
|
assert.match(r.out, /core\.rs:3: names "KEY"/);
|
|
assert.match(r.out, /core\.rs:4: names "DOOR"/);
|
|
assert.match(r.out, /core\.rs:5: names "WALL"/);
|
|
});
|
|
|
|
test("tools_do_not_rely_on_import_meta_main", () => {
|
|
// `import.meta.main` is undefined before Node 24.2, so a tool guarded by it exits 0
|
|
// having checked nothing. Tools use isMain() from lib.mjs (an argv comparison).
|
|
for (const f of readdirSync(toolsDir).filter((f) => f.endsWith(".mjs"))) {
|
|
assert.doesNotMatch(readFileSync(join(toolsDir, f), "utf8"), /import\.meta\.main/, f);
|
|
}
|
|
});
|
|
|
|
test("audit_runs_from_a_relative_path", () => {
|
|
const root = join(toolsDir, "..");
|
|
const r = spawnSync("node", ["tools/coupling-audit.mjs", join(fixtures, "ws-coupled")], {
|
|
cwd: root, encoding: "utf8", env: { ...process.env, COURSE_ROOT: join(fixtures, "course") },
|
|
});
|
|
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
assert.match(r.stderr, /Baron Rivendare/);
|
|
});
|