test(app): add bun test infra and first unit tests

bun test as the runner — already the toolchain, zero new deps beyond
@types/bun. 54 tests across 5 files covering the already-pure logic:
FastAPI error normalization (extracted verbatim to lib/api/errors.ts),
clip trim clamping (extracted from StoryTrackEditor to lib/utils/trim.ts,
magic 100 now MIN_CLIP_DURATION_MS), duration/size/engine formatters,
engine-language map consistency, and changelog parsing. Wired into CI
after typecheck.

Known issues surfaced by the tests, left as-is for now: parseChangelog's
heading regex uses \s* which matches newlines, so a dateless heading
followed directly by a bullet swallows that bullet; formatFileSize has
no TB unit; ENGINE_DISPLAY_NAMES lacks tada/kokoro.
This commit is contained in:
Jamie Pine
2026-07-26 23:17:58 -07:00
parent 000c13b6b9
commit cb5a800445
14 changed files with 475 additions and 49 deletions
+70
View File
@@ -0,0 +1,70 @@
import { describe, expect, test } from 'bun:test';
import {
ALL_LANGUAGES,
ENGINE_LANGUAGES,
LANGUAGE_CODES,
LANGUAGE_OPTIONS,
getLanguageOptionsForEngine,
} from './languages';
describe('ENGINE_LANGUAGES', () => {
test('every engine maps only to codes defined in ALL_LANGUAGES', () => {
for (const [engine, codes] of Object.entries(ENGINE_LANGUAGES)) {
for (const code of codes) {
expect(ALL_LANGUAGES[code], `${engine} references unknown code "${code}"`).toBeDefined();
}
}
});
test('no engine lists a language twice', () => {
for (const [engine, codes] of Object.entries(ENGINE_LANGUAGES)) {
expect(new Set(codes).size, `${engine} has duplicate codes`).toBe(codes.length);
}
});
test('every engine supports at least English', () => {
for (const codes of Object.values(ENGINE_LANGUAGES)) {
expect(codes).toContain('en');
}
});
test('English-only engines list exactly one language', () => {
expect(ENGINE_LANGUAGES.luxtts).toEqual(['en']);
expect(ENGINE_LANGUAGES.chatterbox_turbo).toEqual(['en']);
});
test('qwen and qwen_custom_voice support the same languages', () => {
expect(ENGINE_LANGUAGES.qwen_custom_voice).toEqual(ENGINE_LANGUAGES.qwen);
});
});
describe('getLanguageOptionsForEngine', () => {
test('builds value/label pairs from ALL_LANGUAGES', () => {
expect(getLanguageOptionsForEngine('luxtts')).toEqual([{ value: 'en', label: 'English' }]);
});
test('preserves the engine declaration order', () => {
const values = getLanguageOptionsForEngine('qwen').map((o) => o.value);
expect(values).toEqual([...ENGINE_LANGUAGES.qwen]);
});
test('falls back to qwen languages for unknown engines', () => {
expect(getLanguageOptionsForEngine('does-not-exist')).toEqual(
getLanguageOptionsForEngine('qwen'),
);
});
});
describe('language option exports', () => {
test('LANGUAGE_CODES covers every ALL_LANGUAGES key exactly once', () => {
const codes: string[] = [...LANGUAGE_CODES].sort();
expect(codes).toEqual(Object.keys(ALL_LANGUAGES).sort());
expect(new Set(LANGUAGE_CODES).size).toBe(LANGUAGE_CODES.length);
});
test('LANGUAGE_OPTIONS labels match ALL_LANGUAGES', () => {
for (const option of LANGUAGE_OPTIONS) {
expect(option.label).toBe(ALL_LANGUAGES[option.value]);
}
});
});