import { describe, it, expect } from 'vitest'; import { PROMPT_WORD_CAP, smartTrimPrompt, buildFilmTitles, parseCreditLines, subtitleEnableField, buildCharacterRefs, buildCastLoras, characterRefFromStudioMember, resolveCastMember, formatPreflightResult, gatePreflight, matchBackend, pickAutoMotionBackend, pickAutoBind, buildCastContextBlock, buildStoryboardPayload, isRenderCommand, } from './lib.mjs'; // Real unit tests over the pure helpers extracted from bot.mjs. No Discord client, no network, // no env: each asserts the exact transform contract these functions promise to the render path. const words = (n: number) => Array.from({ length: n }, (_, i) => `w${i}`).join(' '); describe('smartTrimPrompt', () => { it('leaves a prompt at or under the cap untouched', () => { const r = smartTrimPrompt(words(PROMPT_WORD_CAP)); expect(r.trimmed).toBe(false); expect(r.text.split(' ').length).toBe(PROMPT_WORD_CAP); }); it('normalizes whitespace and trims edges without flagging a trim', () => { expect(smartTrimPrompt(' a b\tc ')).toEqual({ text: 'a b\tc', trimmed: false }); }); it('clamps to the cap and flags a trim when over', () => { const r = smartTrimPrompt(words(PROMPT_WORD_CAP + 20)); expect(r.trimmed).toBe(true); expect(r.text.split(/\s+/).length).toBe(PROMPT_WORD_CAP); }); it('keeps the leading sentence whole, then tops up from the rest', () => { const head = 'A hero runs fast.'; // 4 words, motion-critical clause const tail = words(PROMPT_WORD_CAP + 30); const r = smartTrimPrompt(`${head} ${tail}`); expect(r.trimmed).toBe(true); expect(r.text.startsWith('A hero runs fast.')).toBe(true); expect(r.text.split(/\s+/).length).toBe(PROMPT_WORD_CAP); }); it('hard-slices when the leading sentence alone exceeds the cap', () => { const longSentence = words(PROMPT_WORD_CAP + 10) + '.'; const r = smartTrimPrompt(longSentence + ' ' + words(5)); expect(r.trimmed).toBe(true); expect(r.text.split(/\s+/).length).toBe(PROMPT_WORD_CAP); // Falls back to a plain head slice: first PROMPT_WORD_CAP tokens of the whole text. expect(r.text.startsWith('w0 w1 w2')).toBe(true); }); it('treats null/undefined/empty as an empty untrimmed prompt', () => { expect(smartTrimPrompt(undefined)).toEqual({ text: '', trimmed: false }); expect(smartTrimPrompt(null)).toEqual({ text: '', trimmed: false }); expect(smartTrimPrompt('')).toEqual({ text: '', trimmed: false }); }); }); describe('buildFilmTitles', () => { it('returns undefined for a null/empty render-settings', () => { expect(buildFilmTitles(null)).toBeUndefined(); expect(buildFilmTitles(undefined)).toBeUndefined(); expect(buildFilmTitles({})).toBeUndefined(); }); it('emits a title card from title text', () => { expect(buildFilmTitles({ titles: { text: ' The Reel ' } })).toEqual({ title: { text: 'The Reel' } }); }); it('includes a subtitle only when the title text is present', () => { expect(buildFilmTitles({ titles: { text: 'The Reel', subtitle: ' A short ' } })) .toEqual({ title: { text: 'The Reel', subtitle: 'A short' } }); // Subtitle alone (no title text) is dropped -- the card requires text. expect(buildFilmTitles({ titles: { subtitle: 'orphan' } })).toBeUndefined(); }); it('strips blank credit lines and omits credits when all blank', () => { expect(buildFilmTitles({ credits: { lines: ['Dir: X', ' ', '', 'DP: Y'] } })) .toEqual({ credits: { lines: ['Dir: X', 'DP: Y'] } }); expect(buildFilmTitles({ credits: { lines: [' ', ''] } })).toBeUndefined(); }); it('combines title and credits', () => { expect(buildFilmTitles({ titles: { text: 'T' }, credits: { lines: ['a'] } })) .toEqual({ title: { text: 'T' }, credits: { lines: ['a'] } }); }); it('ignores a non-array credits.lines', () => { expect(buildFilmTitles({ credits: { lines: 'not-an-array' } })).toBeUndefined(); }); }); describe('parseCreditLines', () => { it('splits on pipe, semicolon, and newline', () => { expect(parseCreditLines('a | b ; c\nd')).toEqual(['a', 'b', 'c', 'd']); }); it('trims each line and drops blanks', () => { expect(parseCreditLines(' a || ; \n b ')).toEqual(['a', 'b']); }); it('returns an empty array for null/undefined/empty', () => { expect(parseCreditLines(null)).toEqual([]); expect(parseCreditLines(undefined)).toEqual([]); expect(parseCreditLines('')).toEqual([]); }); }); describe('subtitleEnableField', () => { it('prefers a bool key that looks like an enable switch', () => { const mod = { config_schema: { quality: { type: 'string' }, burn_in: { type: 'bool' } } }; expect(subtitleEnableField(mod)).toBe('burn_in'); }); it('matches enable/on/subtitle/caption name shapes', () => { expect(subtitleEnableField({ config_schema: { on: { type: 'bool' } } })).toBe('on'); expect(subtitleEnableField({ config_schema: { captions: { type: 'bool' } } })).toBe('captions'); }); it('falls back to the first bool field when none match the enable shape', () => { const mod = { config_schema: { alpha: { type: 'bool' }, beta: { type: 'bool' } } }; expect(subtitleEnableField(mod)).toBe('alpha'); }); it('falls back to "enabled" when there are no bool fields or no schema', () => { expect(subtitleEnableField({ config_schema: { size: { type: 'string' } } })).toBe('enabled'); expect(subtitleEnableField({})).toBe('enabled'); expect(subtitleEnableField(null)).toBe('enabled'); }); }); describe('buildCharacterRefs', () => { it('includes only cast with both a studio castId and an uploaded portraitKey (inline path)', () => { const brief = { cast: [ { slot: 'A', name: 'Ada', castId: 'c1', portraitKey: 'k1' }, { slot: 'B', name: 'Ben', castId: 'c2' }, { slot: 'C', name: 'Cy', portraitKey: 'k3' }, ], }; expect(buildCharacterRefs(brief)).toEqual({ A: { name: 'Ada', portrait: { key: 'k1' }, trainingImages: [{ key: 'k1' }] }, }); }); it('builds refs from bound studio cast members (portrait + ref_keys)', () => { const brief = { cast_bindings: { A: 'cast-uuid' }, cast: [{ slot: 'A', name: 'Wren', prompt: 'a pilot' }], }; const catalog = [{ id: 'cast-uuid', name: 'Wren', bible: 'brave pilot', portrait_key: 'p1', ref_keys: ['r1', 'r2'], }]; expect(buildCharacterRefs(brief, catalog)).toEqual({ A: { name: 'Wren', prompt: 'brave pilot', portrait: { key: 'p1' }, trainingImages: [{ key: 'p1' }, { key: 'r1' }, { key: 'r2' }], }, }); }); it('returns an empty map when no cast qualifies', () => { expect(buildCharacterRefs({ cast: [] })).toEqual({}); expect(buildCharacterRefs({ cast: [{ slot: 'A', name: 'Ada' }] })).toEqual({}); }); }); describe('buildCastLoras', () => { it('maps slot bindings to cast ids', () => { expect(buildCastLoras({ A: 'id-a', B: 'id-b' })).toEqual({ A: 'id-a', B: 'id-b' }); expect(buildCastLoras({})).toEqual({}); }); }); describe('resolveCastMember', () => { const catalog = [{ id: 'uuid-wren', name: 'Wren' }, { id: 'uuid-mara', name: 'Mara' }]; it('matches by id or name', () => { expect(resolveCastMember(catalog, 'uuid-wren')).toEqual(catalog[0]); expect(resolveCastMember(catalog, 'wren')).toEqual(catalog[0]); }); }); describe('formatPreflightResult', () => { it('summarizes ok and blocked runs', () => { expect(formatPreflightResult({ ok: true, counts: { error: 0, warning: 1, info: 0 }, issues: [] })) .toContain('OK'); expect(formatPreflightResult({ ok: false, counts: { error: 1, warning: 0, info: 0 }, issues: [{ level: 'error', message: 'LoRA not ready' }], })).toContain('blocked'); }); }); describe('matchBackend', () => { const names = ['CogVideoX', 'LTX', 'Wan22']; it('maps auto/default/empty to a null value (omit on submit)', () => { expect(matchBackend(names, 'auto')).toEqual({ value: null }); expect(matchBackend(names, 'default')).toEqual({ value: null }); expect(matchBackend(names, '')).toEqual({ value: null }); expect(matchBackend(names, ' ')).toEqual({ value: null }); expect(matchBackend(names, undefined)).toEqual({ value: null }); }); it('matches a name case-insensitively (exact wins)', () => { expect(matchBackend(names, 'ltx')).toEqual({ value: 'LTX' }); expect(matchBackend(names, 'CogVideoX')).toEqual({ value: 'CogVideoX' }); }); it('falls back to a substring match', () => { expect(matchBackend(names, 'cog')).toEqual({ value: 'CogVideoX' }); expect(matchBackend(names, 'wan')).toEqual({ value: 'Wan22' }); }); it('returns an error with the valid options for an unknown backend', () => { const r = matchBackend(names, 'nope'); expect(r.value).toBeUndefined(); expect(r.error).toContain('Unknown backend `nope`'); expect(r.error).toContain('auto, CogVideoX, LTX, Wan22'); }); it('reports "(none reported)" when the registry list is empty', () => { expect(matchBackend([], 'x').error).toContain('(none reported)'); expect(matchBackend(undefined, 'x').error).toContain('(none reported)'); }); }); describe('pickAutoMotionBackend', () => { // A GET /api/modules payload: `hooks['motion.backend']` is the serving list the studio already // sorts by ui.order then name; `modules[].ui.locality` classifies each (undeclared = cloud). const reg = (order, localities) => ({ hooks: { 'motion.backend': order }, modules: order.map((name) => localities[name] === undefined ? { name } : { name, ui: { locality: localities[name] } }, ), }); it('prefers a cloud module over an order-first local gpu-door (the #58 bug)', () => { const r = pickAutoMotionBackend(reg(['local-gpu', 'alibaba-wan'], { 'local-gpu': 'local', 'alibaba-wan': 'cloud' })); expect(r).toEqual({ value: 'alibaba-wan' }); }); it('prefers the operator own-gpu (byo) over a local door when no cloud serves', () => { const r = pickAutoMotionBackend(reg(['local-gpu', 'own-gpu'], { 'local-gpu': 'local', 'own-gpu': 'byo' })); expect(r).toEqual({ value: 'own-gpu' }); }); it('prefers cloud over byo', () => { const r = pickAutoMotionBackend(reg(['own-gpu', 'alibaba-wan'], { 'own-gpu': 'byo', 'alibaba-wan': 'cloud' })); expect(r).toEqual({ value: 'alibaba-wan' }); }); it('within a class, respects the studio ui.order (first serving name wins)', () => { const r = pickAutoMotionBackend(reg(['alibaba-wan', 'google-veo'], { 'alibaba-wan': 'cloud', 'google-veo': 'cloud' })); expect(r).toEqual({ value: 'alibaba-wan' }); }); it('treats an undeclared locality as cloud', () => { const r = pickAutoMotionBackend(reg(['own-gpu', 'mystery'], { 'own-gpu': 'byo', 'mystery': undefined })); expect(r).toEqual({ value: 'mystery' }); }); it('sends a lone local door when it is the only thing serving (pure self-host)', () => { const r = pickAutoMotionBackend(reg(['local-gpu'], { 'local-gpu': 'local' })); expect(r).toEqual({ value: 'local-gpu' }); }); it('classifies as cloud when the modules array is absent', () => { const r = pickAutoMotionBackend({ hooks: { 'motion.backend': ['x'] } }); expect(r).toEqual({ value: 'x' }); }); it('errors (never omits) when nothing serves motion.backend', () => { expect(pickAutoMotionBackend(reg([], {})).error).toContain('no motion backend'); expect(pickAutoMotionBackend({}).error).toContain('no motion backend'); expect(pickAutoMotionBackend({ hooks: {} }).error).toContain('no motion backend'); }); }); describe('characterRefFromStudioMember (slate #88: ref_keys are {key,mime} objects)', () => { it('extracts the R2 key STRING from object ref_keys, never pushes [object Object]', () => { const ref = characterRefFromStudioMember({ name: 'Wren', bible: 'a botanist', portrait_key: 'cast/4/portrait.jpg', ref_keys: [{ key: 'cast-gen/4/ref_01.jpg', mime: 'image/jpeg' }, { key: 'cast-gen/4/ref_02.jpg', mime: 'image/jpeg' }], }, 'A'); const keys = ref.trainingImages.map((t) => t.key); expect(keys).toEqual(['cast/4/portrait.jpg', 'cast-gen/4/ref_01.jpg', 'cast-gen/4/ref_02.jpg']); expect(keys.every((k) => typeof k === 'string')).toBe(true); expect(JSON.stringify(ref)).not.toContain('[object Object]'); }); it('still accepts bare-string ref_keys (defensive)', () => { const ref = characterRefFromStudioMember({ name: 'X', ref_keys: ['cast-gen/9/ref_01.jpg'] }, 'A'); expect(ref.trainingImages.map((t) => t.key)).toEqual(['cast-gen/9/ref_01.jpg']); }); }); describe("pickAutoBind (#84 auto-reuse of trained studio cast)", () => { const catalog = [ { id: "wren-id", name: "Wren", lora_status: "ready" }, { id: "riff-a", name: "Riff", lora_status: "ready" }, { id: "riff-b", name: "Riff", lora_status: "failed" }, ]; it("binds on a unique exact case-insensitive name match", () => { const r = pickAutoBind(catalog, "wren"); expect(r.status).toBe("bound"); expect(r.member.id).toBe("wren-id"); }); it("is ambiguous when >1 member shares the name -- never auto-picks", () => { const r = pickAutoBind(catalog, "Riff"); expect(r.status).toBe("ambiguous"); expect(r.matches).toHaveLength(2); }); it("returns none when no member has the name", () => { expect(pickAutoBind(catalog, "Nobody").status).toBe("none"); }); it("skips a generic placeholder slot name", () => { expect(pickAutoBind(catalog, "Character A").status).toBe("skip"); expect(pickAutoBind(catalog, "").status).toBe("skip"); }); }); describe("buildCastContextBlock (#84 cast-blind brain fix)", () => { const catalog = [ { id: "w", name: "Wren", lora_status: "ready", voice_id: "asteria" }, { id: "s", name: "Salvage Robot", lora_status: "ready" }, ]; it("lists the studio roster with LoRA readiness", () => { const out = buildCastContextBlock({ cast: [], cast_bindings: {} }, catalog); expect(out).toContain("Wren -- LoRA ready"); expect(out).toContain("Salvage Robot -- LoRA ready"); expect(out).toContain("voice asteria"); }); it("marks a bound slot as BOUND and an inline slot as auto-bind", () => { const brief = { cast: [{ slot: "A", name: "Wren" }, { slot: "B", name: "Nova" }], cast_bindings: { A: "w" }, }; const out = buildCastContextBlock(brief, catalog); expect(out).toMatch(/A: Wren -- BOUND/); expect(out).toMatch(/B: Nova -- inline \(auto-binds/); }); it("returns empty string when there is nothing to show", () => { expect(buildCastContextBlock({ cast: [], cast_bindings: {} }, [])).toBe(""); }); it("tells the brain it can see the cast (anti-confabulation)", () => { expect(buildCastContextBlock({ cast: [], cast_bindings: {} }, catalog)) .toContain("NEVER tell the user to run a command"); }); }); // The studio storyboard validator (vivijure storyboard-validate.ts) accepts scene.dialogue ONLY as // { slot, text }; a bare string 400s the bundle. These pin the shape slate puts on the wire so a // future edit that re-introduces a string dialogue (the audited silent-failure regression) fails CI. describe("buildStoryboardPayload dialogue shape (studio contract)", () => { const refs = { A: { name: "Wren" }, B: { name: "Nova" } }; it("never emits a bare string dialogue for any scene shape", () => { const brief = { title: "T", scenes: [ { id: "s1", prompt: "p", character_slots: ["A"], dialogue: "Hello there" }, { id: "s2", prompt: "p", character_slots: ["A", "B"], dialogue: "Who spoke?" }, { id: "s3", prompt: "p", character_slots: [], dialogue: "Nobody here" }, { id: "s4", prompt: "p", character_slots: ["A"] }, ], }; const out = buildStoryboardPayload(brief, refs); for (const sc of out.scenes) { expect(typeof sc.dialogue).not.toBe("string"); if (sc.dialogue !== undefined) { expect(sc.dialogue).toMatchObject({ slot: expect.any(String), text: expect.any(String) }); } } }); it("attributes a spoken line to the single speaking slot as { slot, text }", () => { const brief = { title: "T", scenes: [{ id: "s1", prompt: "p", character_slots: ["A"], dialogue: " Hello " }] }; const out = buildStoryboardPayload(brief, refs); expect(out.scenes[0].dialogue).toEqual({ slot: "A", text: "Hello" }); }); it("omits dialogue (no string, no wrong speaker) when the speaker is ambiguous or absent", () => { const brief = { title: "T", scenes: [ { id: "s1", prompt: "p", character_slots: ["A", "B"], dialogue: "ambiguous" }, { id: "s2", prompt: "p", character_slots: [], dialogue: "no slots" }, ], }; const out = buildStoryboardPayload(brief, refs); expect(out.scenes[0].dialogue).toBeUndefined(); expect(out.scenes[1].dialogue).toBeUndefined(); }); it("carries the top-level authored fields the studio StoryboardInput reads", () => { const brief = { title: "My Film", full_prompt: "fp", style_prefix: "sp", style_category: "Noir", duration_seconds: 30, clip_seconds: 5, scenes: [{ id: "s1", prompt: "p", character_slots: ["A"], act: "I", target_seconds: 4 }], }; const out = buildStoryboardPayload(brief, refs); expect(out).toMatchObject({ title: "My Film", full_prompt: "fp", style_prefix: "sp", style_category: "Noir", duration_seconds: 30, clip_seconds: 5 }); expect(out.use_characters).toEqual(["A"]); expect(out.scenes[0]).toMatchObject({ id: "s1", prompt: "p", act: "I", character_slots: ["A"], target_seconds: 4 }); }); }); describe("isRenderCommand", () => { it("matches the bare command and the command followed by whitespace-delimited args", () => { expect(isRenderCommand("!render")).toBe(true); expect(isRenderCommand("!render now")).toBe(true); expect(isRenderCommand("!render draft")).toBe(true); expect(isRenderCommand("!render\tfinal")).toBe(true); }); it("does not match sibling commands that merely share the !render prefix", () => { for (const t of ["!renders", "!renders 5", "!rendertags", "!renderpoll abc", "!render-plan", "!render-keyframes now", "!render-keyframes"]) { expect(isRenderCommand(t), t).toBe(false); } }); it("does not match unrelated text", () => { expect(isRenderCommand("render")).toBe(false); expect(isRenderCommand("!ship")).toBe(false); expect(isRenderCommand("")).toBe(false); }); }); describe('gatePreflight (paid-render guard, slate#183)', () => { const okResult = { ok: true, counts: { error: 0, warning: 0, info: 0 }, issues: [] }; const noResult = { ok: false, counts: { error: 1, warning: 0, info: 0 }, issues: [{ level: 'error', message: 'LoRA not ready' }] }; it('case 1: preflight ran and said NO -> refuse with the issues', async () => { const r = await gatePreflight(async () => ({ ok: true, result: noResult })); expect(r?.ok).toBe(false); expect(r?.error).toContain('blocked'); expect(r?.preflight).toEqual(noResult); }); it('case 2 (control): preflight ran and said YES -> proceed', async () => { expect(await gatePreflight(async () => ({ ok: true, result: okResult }))).toBeNull(); }); it('case 3a: preflight call threw -> refuse', async () => { const r = await gatePreflight(async () => { throw new Error('connect ECONNREFUSED'); }); expect(r?.ok).toBe(false); expect(r?.error).toContain('did not start'); }); it('case 3b: preflight timed out (TimeoutError rejection) -> refuse', async () => { const r = await gatePreflight(() => Promise.reject(Object.assign(new Error('The operation timed out'), { name: 'TimeoutError' }))); expect(r?.ok).toBe(false); expect(r?.error).toContain('did not start'); }); it('case 3c: studio answered with an error (5xx/401 shape) -> refuse with that error', async () => { const r = await gatePreflight(async () => ({ ok: false, error: 'the studio hit a server error trying to run preflight (503).' })); expect(r?.ok).toBe(false); expect(r?.error).toContain('server error'); }); it('case 3d: preflight returned nothing or no verdict -> refuse', async () => { expect((await gatePreflight(async () => null))?.ok).toBe(false); expect((await gatePreflight(async () => undefined))?.ok).toBe(false); expect((await gatePreflight(async () => ({ ok: true, result: {} })))?.ok).toBe(false); expect((await gatePreflight(async () => ({ ok: true })))?.ok).toBe(false); }); });