Files
suaveplan/packages/media/audio/audio-cli/tests/purity.test.ts
2026-05-27 23:47:53 +01:00

92 lines
2.9 KiB
TypeScript

import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const PKG_ROOT = join(__dirname, "..");
const SRC = join(PKG_ROOT, "src");
// Match a *use* of the global as a free identifier (assignment, property access,
// or function call), not occurrences inside compound identifiers like `AudioDocument`.
const BROWSER_GLOBALS = [
"window",
"document",
"navigator",
"AudioContext",
"AudioBuffer",
];
function stripCommentsAndStrings(src: string): string {
// Drop /* ... */, // ..., and "..." / '...' content so we only see code.
return src
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/\/\/[^\n]*/g, " ")
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
}
function refersToGlobal(src: string, name: string): boolean {
const code = stripCommentsAndStrings(src);
// Token must start after a non-identifier char and be followed by a
// property access (.), call site (() or [), assignment (= but not ==),
// semicolon, comma, or close paren.
const re = new RegExp(
`(?:^|[^A-Za-z0-9_.])${name}(?=[.(\\[]|\\s*=[^=])`,
"m"
);
return re.test(code);
}
async function* walk(dir: string): AsyncGenerator<string> {
const entries = await readdir(dir, { withFileTypes: true });
for (const e of entries) {
const full = join(dir, e.name);
if (e.isDirectory()) yield* walk(full);
else if (
e.isFile() &&
e.name.endsWith(".ts") &&
!e.name.endsWith(".test.ts")
)
yield full;
}
}
describe("audio-cli purity", () => {
it("source references no browser-only globals", {
timeout: 5000,
}, async () => {
for await (const path of walk(SRC)) {
const src = await readFile(path, "utf8");
for (const global of BROWSER_GLOBALS) {
expect(
refersToGlobal(src, global),
`${path} references ${global}`
).toBe(false);
}
}
});
it("declared dependency set is limited to the spec list", {
timeout: 5000,
}, async () => {
const raw = await readFile(join(PKG_ROOT, "package.json"), "utf8");
const pkg = JSON.parse(raw) as {
dependencies?: Record<string, string>;
};
const allowed = new Set([
"@suaveplan/audio-codecs",
"@suaveplan/audio-core",
"@suaveplan/audio-engine-server",
"@suaveplan/error",
"@suaveplan/types",
"@opentelemetry/api",
"tslib",
"zod",
]);
for (const dep of Object.keys(pkg.dependencies ?? {})) {
expect(allowed.has(dep), `unexpected runtime dep ${dep}`).toBe(
true
);
}
});
});