Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
import * as rootBarrel from "../src/index.js";
describe("audio-cli subpath exports", () => {
it("every advertised subpath has an exports entry in package.json", {
timeout: 5000,
}, async () => {
const raw = await readFile(
join(__dirname, "..", "package.json"),
"utf8"
);
const pkg = JSON.parse(raw) as { exports: Record<string, unknown> };
for (const sub of [
".",
"./package.json",
"./cli",
"./error-codes",
"./pipeline/parser",
"./commands/process",
"./commands/batch",
"./commands/analyze",
"./commands/convert",
"./commands/info",
"./commands/play",
"./commands/record",
]) {
expect(
Object.hasOwn(pkg.exports, sub),
`missing export ${sub}`
).toBe(true);
}
});
it("root barrel exports AudioCLI, parsePipeline, error codes, every runX", {
timeout: 5000,
}, () => {
const expected = [
"AudioCLI",
"parsePipeline",
"validatePipeline",
"AUDIO_CLI_CODES",
"AudioCliError",
"EXIT_CODES",
"exitCodeFor",
"runProcess",
"runBatch",
"runAnalyze",
"runConvert",
"runInfo",
"runPlay",
"runRecord",
"createSilentMicrophone",
"noopSpeaker",
"createDefaultCodecRegistry",
"detectCodecForOutput",
];
for (const name of expected) {
expect(name in rootBarrel, `root barrel missing ${name}`).toBe(
true
);
}
});
});

View File

@@ -0,0 +1,27 @@
import { writeFile } from "node:fs/promises";
import { WavCodec } from "@suaveplan/audio-codecs";
/** Write a minimal mono 44.1 kHz WAV file for fixture purposes. */
export async function writeWav(path: string, samples = 256): Promise<void> {
const bytes = await WavCodec.encode([new Float32Array(samples)], {
sampleRate: 44_100,
bitDepth: 16,
format: "pcm",
});
await writeFile(path, bytes);
}
/** Write a noisy 44.1 kHz mono WAV file so loudness analyses report a value. */
export async function writeNoisyWav(
path: string,
samples = 44_100
): Promise<void> {
const buf = new Float32Array(samples);
for (let i = 0; i < samples; i++) buf[i] = Math.sin(i * 0.01) * 0.5;
const bytes = await WavCodec.encode([buf], {
sampleRate: 44_100,
bitDepth: 16,
format: "pcm",
});
await writeFile(path, bytes);
}

View File

@@ -0,0 +1,73 @@
import { mkdtemp, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { AudioCLI } from "../src/cli/cli.js";
import { writeWav } from "./helpers.js";
describe("audio-cli integration", () => {
let dir: string;
let cli: AudioCLI;
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), "audio-cli-int-"));
cli = new AudioCLI();
});
afterEach(async () => {
await rm(dir, { recursive: true, force: true });
});
it("process → analyze → convert round-trip via dispatch", {
timeout: 30_000,
}, async () => {
const a = join(dir, "in.wav");
await writeWav(a, 256);
// process
expect(
await cli.dispatch([
"process",
a,
join(dir, "out.wav"),
"--pipeline",
"normalize",
])
).toBe(0);
expect((await stat(join(dir, "out.wav"))).size).toBeGreaterThan(0);
// analyze with --output
const report = join(dir, "report.json");
expect(await cli.dispatch(["analyze", a, "--output", report])).toBe(0);
expect((await stat(report)).size).toBeGreaterThan(0);
// convert
expect(await cli.dispatch(["convert", a, join(dir, "conv.wav")])).toBe(
0
);
});
it("batch dispatch over a comma list returns a JSON report", {
timeout: 30_000,
}, async () => {
const a = join(dir, "a.wav");
const b = join(dir, "b.wav");
await writeWav(a, 32);
await writeWav(b, 32);
expect(
await cli.dispatch([
"batch",
"--input",
`${a},${b}`,
"--output",
dir,
"--pipeline",
"",
])
).toBe(0);
});
});

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { parsePipeline } from "../src/pipeline/parser.js";
/**
* Property-style tests: the same op chain produced via the pipe-form string
* and via the positional bare-op tokens MUST yield the same internal edit
* chain. This is the parser invariant that lets the bare-op CLI form and
* `batch --pipeline` share a single downstream code path.
*/
describe("parsePipeline determinism", () => {
const cases: Array<{ str: string; tokens: readonly string[] }> = [
{ str: "trim 30s", tokens: ["trim", "30s"] },
{ str: "normalize -16lufs", tokens: ["normalize", "-16lufs"] },
{
str: "trim 30s | normalize -16lufs",
tokens: ["trim", "30s", "normalize", "-16lufs"],
},
{
str: "trim 30s | normalize | fade 0.5s 1s",
tokens: ["trim", "30s", "normalize", "fade", "0.5s", "1s"],
},
{ str: "gain 6db", tokens: ["gain", "6db"] },
];
for (const { str, tokens } of cases) {
it(`pipe-form and bare-op produce the same chain for: ${str}`, {
timeout: 5000,
}, () => {
expect(parsePipeline(tokens)).toEqual(parsePipeline(str));
});
}
it("idempotent: parse(parse(x)) is the identity for chained pipe forms", {
timeout: 5000,
}, () => {
const once = parsePipeline("trim 30s | normalize -16lufs");
const reSerialised = once
.map(([name, opts]) => {
const args = Object.entries(opts).map(([k, v]) => {
if (k === "start") return `${v}s`;
if (k === "lufs") return `${v}lufs`;
return `${k}=${v}`;
});
return [name, ...args].join(" ");
})
.join(" | ");
const twice = parsePipeline(reSerialised);
expect(twice).toEqual(once);
});
});

View File

@@ -0,0 +1,91 @@
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
);
}
});
});