Files
suaveplan/packages/media/audio-ir/audio-ir-loader/src/error-codes/error-codes.test.ts
2026-05-27 23:47:53 +01:00

332 lines
9.9 KiB
TypeScript

import type { IrManifestEntry } from "@suaveplan/audio-ir-core";
import { createIrRegistry } from "@suaveplan/audio-ir-core";
import {
createStorageUriResolver,
type UriPlugin,
} from "@suaveplan/storage-uri";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { ImpulseResponseCache } from "../cache/cache.js";
import { type AudioDecoder, createIrLoader } from "../loader/loader.js";
import { runPreload } from "../loader/preload.js";
import { resetInstruments } from "../otel/otel.js";
import {
AUDIO_IR_LOADER_CODES,
AudioIrLoaderError,
type AudioIrLoaderErrorCode,
} from "./error-codes.js";
const ENTRY: IrManifestEntry = Object.freeze({
id: "club-club1",
family: "room",
uri: "bundle://x/club1.wav",
channels: 1,
sampleRate: 44100,
gainDb: 0,
license: "MIT",
source: "demo",
}) as IrManifestEntry;
const FLAC_ENTRY: IrManifestEntry = Object.freeze({
...ENTRY,
id: "club-club2",
uri: "bundle://x/club2.flac",
}) as IrManifestEntry;
function buildResolver(reads: Record<string, Uint8Array>): {
resolver: ReturnType<typeof createStorageUriResolver>;
plugin: UriPlugin;
} {
const plugin: UriPlugin = {
scheme: "bundle",
adapter: {
async get(key: string) {
return reads[key];
},
async set() {
return;
},
async delete() {
return true;
},
async exists(key) {
return key in reads;
},
async list() {
return { keys: [], total: 0, hasMore: false };
},
async clear() {
return;
},
},
parse(uri: string) {
const noScheme = uri.replace(/^bundle:\/\//, "");
return { key: noScheme };
},
};
return { resolver: createStorageUriResolver([plugin]), plugin };
}
const noopWavBytes = new Uint8Array([1, 2, 3]);
function decoderThrowing(code: string): AudioDecoder {
return async () => {
const err = new Error(`fixture decoder threw ${code}`);
throw err;
};
}
async function exerciseDecoderUnavailable(): Promise<
AudioIrLoaderError | undefined
> {
resetInstruments();
const registry = createIrRegistry();
registry.register(FLAC_ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club2.flac": noopWavBytes });
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoderThrowing("nope") },
});
try {
await loader.load("club-club2");
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseManifestMismatch(): Promise<
AudioIrLoaderError | undefined
> {
resetInstruments();
const registry = createIrRegistry();
registry.register(ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club1.wav": noopWavBytes });
const decoder: AudioDecoder = async () => ({
channelData: [new Float32Array([0.1])],
channels: 1,
sampleRate: 22050,
});
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoder },
});
try {
await loader.load("club-club1");
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseAborted(): Promise<AudioIrLoaderError | undefined> {
resetInstruments();
const registry = createIrRegistry();
registry.register(ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club1.wav": noopWavBytes });
const decoder: AudioDecoder = async () => ({
channelData: [new Float32Array([0])],
channels: 1,
sampleRate: 44100,
});
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoder },
});
const controller = new AbortController();
controller.abort(new Error("preflight"));
try {
await loader.load("club-club1", { signal: controller.signal });
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseCacheOverflow(): Promise<
AudioIrLoaderError | undefined
> {
const cache = new ImpulseResponseCache(64);
const big = Object.freeze({
id: "big",
channelData: Object.freeze([new Float32Array(1024)]),
channels: 1,
sampleRate: 44100,
lengthFrames: 1024,
durationSec: 1024 / 44100,
gainDb: 0,
family: "room" as const,
manifest: ENTRY,
});
try {
cache.put("big", big as unknown as Parameters<typeof cache.put>[1]);
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseDecodeFailed(): Promise<AudioIrLoaderError | undefined> {
resetInstruments();
const registry = createIrRegistry();
registry.register(ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club1.wav": noopWavBytes });
const decoder: AudioDecoder = async () => ({
channelData: [new Float32Array([0])],
channels: 99,
sampleRate: 44100,
});
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoder },
});
try {
await runPreload(
(id, opts) => loader.load(id, opts),
["club-club1"],
undefined
);
} catch (err) {
if (err instanceof AudioIrLoaderError) return err;
}
return undefined;
}
async function exerciseResolveFailed(): Promise<
AudioIrLoaderError | undefined
> {
resetInstruments();
const registry = createIrRegistry();
registry.register(ENTRY, {
manifestSlug: "club",
manifestFamily: "room",
});
const { resolver } = buildResolver({ "x/club1.wav": noopWavBytes });
const decoder: AudioDecoder = async () => ({
channelData: [new Float32Array([0])],
channels: 1,
sampleRate: 44100,
});
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decoder },
});
// Build a fake load fn that throws a non-SuaveplanError to trigger
// the RESOLVE_FAILED wrapper inside runPreload.
const badLoader = async () => {
throw new TypeError("plain error");
};
try {
await runPreload(
badLoader as unknown as (id: string) => Promise<never>,
["x"],
undefined
);
} catch (err) {
// top-level err is DECODE_FAILED whose `errors[0].error` is the RESOLVE_FAILED
if (err instanceof AudioIrLoaderError && err.errors !== undefined) {
const first = err.errors[0]?.error;
if (first instanceof AudioIrLoaderError) return first;
}
}
// touch loader to avoid unused warning
void loader;
return undefined;
}
function siteFor(
code: AudioIrLoaderErrorCode
): () => Promise<AudioIrLoaderError | undefined> {
if (code === AUDIO_IR_LOADER_CODES.DECODER_UNAVAILABLE)
return exerciseDecoderUnavailable;
if (code === AUDIO_IR_LOADER_CODES.MANIFEST_MISMATCH)
return exerciseManifestMismatch;
if (code === AUDIO_IR_LOADER_CODES.ABORTED) return exerciseAborted;
if (code === AUDIO_IR_LOADER_CODES.CACHE_OVERFLOW)
return exerciseCacheOverflow;
if (code === AUDIO_IR_LOADER_CODES.DECODE_FAILED)
return exerciseDecodeFailed;
if (code === AUDIO_IR_LOADER_CODES.RESOLVE_FAILED)
return exerciseResolveFailed;
return async () => undefined;
}
describe("AUDIO_IR_LOADER_CODES exhaustion", () => {
it("every AUDIO_IR_LOADER code reachable from a real call site", {
timeout: 30000,
}, async () => {
const all = Object.values(AUDIO_IR_LOADER_CODES);
expect(all.length).toBe(6);
for (const code of all) {
const err = await siteFor(code)();
expect(err, `no real call site exercises ${code}`).toBeDefined();
expect(err?.code).toBe(code);
}
});
it("registry exposes exactly six codes", { timeout: 5000 }, () => {
const names = Object.keys(AUDIO_IR_LOADER_CODES).sort();
expect(names).toEqual([
"ABORTED",
"CACHE_OVERFLOW",
"DECODER_UNAVAILABLE",
"DECODE_FAILED",
"MANIFEST_MISMATCH",
"RESOLVE_FAILED",
]);
});
it("AUDIO_IR_LOADER_CODES registry is frozen", { timeout: 5000 }, () => {
expect(Object.isFrozen(AUDIO_IR_LOADER_CODES)).toBe(true);
});
it("AudioIrLoaderError surfaces typed discriminants", {
timeout: 5000,
}, () => {
const err = new AudioIrLoaderError(
AUDIO_IR_LOADER_CODES.MANIFEST_MISMATCH,
"x",
{
id: "club-a",
field: "channels",
manifestValue: 2,
decodedValue: 1,
}
);
expect(err.name).toBe("AudioIrLoaderError");
expect(err.id).toBe("club-a");
expect(err.field).toBe("channels");
expect(err.manifestValue).toBe(2);
expect(err.decodedValue).toBe(1);
});
it("AudioIrLoaderError with no options leaves discriminants undefined", {
timeout: 5000,
}, () => {
const err = new AudioIrLoaderError(AUDIO_IR_LOADER_CODES.ABORTED, "x");
expect(err.id).toBeUndefined();
expect(err.extension).toBeUndefined();
expect(err.field).toBeUndefined();
expect(err.manifestValue).toBeUndefined();
expect(err.decodedValue).toBeUndefined();
expect(err.maxBytes).toBeUndefined();
expect(err.decodedBytes).toBeUndefined();
expect(err.errors).toBeUndefined();
});
});