meat/tests/layering.test.ts

60 lines
2.3 KiB
TypeScript

import { expect, test } from "bun:test"
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
import path from "node:path"
// The engine is the reusable, content-agnostic layer: it must import nothing from
// game (content) or app (browser glue). game -> engine and app -> game -> engine are
// fine; the reverse is the violation. This freezes the Stage-3 seam so a stray import
// can't quietly re-couple the layers.
function tsFiles(dir: string): string[] {
const out: string[] = []
for (const name of readdirSync(dir)) {
const p = path.join(dir, name)
if (statSync(p).isDirectory()) {
out.push(...tsFiles(p))
} else if (p.endsWith(".ts")) {
out.push(p)
}
}
return out
}
function repositoryRoot(): string {
const parent = new URL("..", import.meta.url).pathname
return existsSync(path.join(parent, "app/renderer.ts")) ? parent : path.join(parent, "..")
}
function importsLayer(source: string, layers: string): boolean {
return new RegExp(`(?:from\\s+|import\\s*(?:\\(\\s*)?)["'][^"']*/(?:${layers})/`).test(source)
}
test("engine imports nothing from game or app", () => {
const engineDir = path.join(repositoryRoot(), "engine")
const offenders = tsFiles(engineDir).filter((file) => importsLayer(readFileSync(file, "utf8"), "game|app"))
expect(offenders).toEqual([])
})
test("game imports nothing from app", () => {
const gameDir = path.join(repositoryRoot(), "game")
const offenders = tsFiles(gameDir).filter((file) => importsLayer(readFileSync(file, "utf8"), "app"))
expect(offenders).toEqual([])
})
test("render driver and worker know no game content", () => {
const root = repositoryRoot()
const files = [path.join(root, "app/renderer.ts"), path.join(root, "app/render-worker.ts")]
const offenders = files.filter((file) => importsLayer(readFileSync(file, "utf8"), "game"))
expect(offenders).toEqual([])
})
test("game exports no closed content-kind registries", () => {
const gameDir = path.join(repositoryRoot(), "game")
const offenders = tsFiles(gameDir).filter((file) => {
const source = readFileSync(file, "utf8")
return (
/\b(?:export\s+)?(?:type|enum)\s+\w+Kind\b/.test(source) ||
/\b(?:const|let|var)\s+\w*_(?:KINDS|REGISTRY)\b/.test(source)
)
})
expect(offenders).toEqual([])
})