import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync, writeFileSync, unlinkSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { pathToFileURL } from 'node:url' import ts from 'typescript' const src = readFileSync(new URL('../utils/promptSequences.ts', import.meta.url), 'utf8') const js = ts.transpileModule(src, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText const tmp = join(tmpdir(), `prompt-sequences-${process.pid}.mjs`) writeFileSync(tmp, js) const { assembleSceneText, assembleSequenceText, formatCharacterLine, emptyScene, emptyCharacter, emptyOutfit, emptySet, emptyFraming, cloneSceneForNext, normalizePromptSequence, ensureSceneStatesForCharacters } = await import(pathToFileURL(tmp).href) unlinkSync(tmp) test('formatCharacterLine covers name/description and outfit', () => { assert.equal(formatCharacterLine({ name: 'Ada', description: 'tall' }), 'Ada: tall') assert.equal( formatCharacterLine({ name: 'Ada', description: 'tall' }, { name: 'Work', description: 'blue jacket' }), 'Ada: tall. Outfit: blue jacket' ) }) test('selecting a framing copies only that framing text; labels never appear', () => { const wide = { ...emptyFraming('wide'), id: 'f-wide', text: 'open courtyard at dawn' } const medium = { ...emptyFraming('medium'), id: 'f-med', text: 'courtyard from the arch' } const narrow = { ...emptyFraming('narrow'), id: 'f-nar', text: 'door latch detail' } const set = { ...emptySet(), id: 's1', name: 'Courtyard', framings: [wide, medium, narrow] } const base = { style: 'style', settings: 'settings', characterStates: [], prompt: 'prompt' } const mediumText = assembleSceneText({ ...base, setStates: [{ setId: 's1', on: true, framingId: 'f-med' }] }, [], [set]) assert.equal(mediumText, 'style\n\nsettings\n\ncourtyard from the arch\n\nprompt') assert.ok(!mediumText.includes('open courtyard')) assert.ok(!mediumText.includes('door latch')) assert.ok(!mediumText.includes('Courtyard')) assert.ok(!mediumText.includes('wide')) assert.ok(!mediumText.includes('medium')) assert.ok(!mediumText.includes('narrow')) const narrowText = assembleSceneText({ ...base, setStates: [{ setId: 's1', on: true, framingId: 'f-nar' }] }, [], [set]) assert.equal(narrowText, 'style\n\nsettings\n\ndoor latch detail\n\nprompt') assert.ok(!narrowText.includes('open courtyard')) assert.ok(!narrowText.includes('from the arch')) const noneText = assembleSceneText({ ...base, setStates: [{ setId: 's1', on: true, framingId: null }] }, [], [set]) assert.equal(noneText, 'style\n\nsettings\n\nprompt') }) test('off set is omitted; sequence join is double blank', () => { const framing = { ...emptyFraming('wide'), id: 'f1', text: 'set text' } const set = { ...emptySet(), id: 's1', name: 'Place', framings: [framing] } const scene1 = { style: 'a', settings: '', setStates: [{ setId: 's1', on: true, framingId: 'f1' }], characterStates: [], prompt: 'p1' } const scene2 = { style: 'b', settings: '', setStates: [{ setId: 's1', on: false, framingId: 'f1' }], characterStates: [], prompt: 'p2' } assert.equal(assembleSceneText(scene2, [], [set]), 'b\n\np2') const full = assembleSequenceText({ sets: [set], scenes: [scene1, scene2] }) assert.equal(full, 'a\n\nset text\n\np1\n\n\nb\n\np2') assert.ok(!full.includes('Place')) assert.ok(!full.includes('wide')) }) test('legacy beat-only set still opens as one wide framing', () => { const legacy = normalizePromptSequence({ id: 'seq1', name: 'Old beats', sets: [{ id: 's1', name: 'Hall', description: 'stone walls', beats: [ { id: 'b1', name: 'Beat1', text: 'first beat', framings: ['wide'] }, { id: 'b2', name: 'Beat2', text: 'second beat', framings: ['medium'] } ] }], scenes: [{ id: 'sc1', title: 'Scene 1', style: 'oil', settings: '', shotSize: 'wide', setStates: [{ setId: 's1', on: true }], prompt: 'walk', characters: [] }], characters: [{ id: 'x', name: 'Ada', description: 'tall', outfits: [] }], createdAt: 1, updatedAt: 1 }) assert.equal(legacy.sets.length, 1) assert.equal(legacy.sets[0].framings.length, 1) assert.equal(legacy.sets[0].framings[0].label, 'wide') assert.ok(legacy.sets[0].framings[0].text.includes('stone walls')) assert.ok(legacy.sets[0].framings[0].text.includes('first beat')) assert.ok(legacy.sets[0].framings[0].text.includes('second beat')) assert.equal(legacy.scenes[0].setStates[0].framingId, legacy.sets[0].framings[0].id) const text = assembleSequenceText(legacy) assert.ok(text.includes('stone walls')) assert.ok(text.includes('first beat')) assert.ok(!text.includes('Hall')) assert.ok(!text.includes('Beat1')) }) test('legacy sequence without sets still assembles', () => { const legacy = normalizePromptSequence({ id: 'seq1', name: 'Old', scenes: [ { id: 'sc1', title: 'Scene 1', style: 'oil', settings: '', prompt: 'walk', characters: [{ id: 'x', name: 'Ada', description: 'tall' }] } ], createdAt: 1, updatedAt: 1 }) assert.deepEqual(legacy.sets, []) assert.equal(assembleSequenceText(legacy), 'oil\n\nAda: tall\n\nwalk') }) test('cloneSceneForNext copies setStates including framingId', () => { const sets = [emptySet()] const first = emptyScene(1, [], sets) first.setStates[0].on = false first.setStates[0].framingId = sets[0].framings[0].id const next = cloneSceneForNext(first, 2) assert.equal(next.setStates[0].on, false) assert.equal(next.setStates[0].framingId, sets[0].framings[0].id) assert.equal(next.setStates[0].setId, sets[0].id) }) test('character outfit and off still work with sets', () => { const ada = { ...emptyCharacter(), id: 'c1', name: 'Ada', description: 'tall', outfits: [{ ...emptyOutfit(), id: 'o1', description: 'red dress' }] } const text = assembleSceneText({ style: '', settings: '', setStates: [], characterStates: [{ characterId: 'c1', on: true, outfitId: 'o1' }], prompt: 'hi' }, [ada], []) assert.equal(text, 'Ada: tall. Outfit: red dress\n\nhi') }) test('ensureSceneStatesForCharacters turns new character on only for current scene', () => { const a = emptyCharacter() const scenes = [emptyScene(1, [a]), emptyScene(2, [a])] const b = emptyCharacter() ensureSceneStatesForCharacters(scenes, [a, b], { newCharacterId: b.id, turnOnSceneIndex: 1 }) assert.equal(scenes[0].characterStates.find(s => s.characterId === b.id).on, false) assert.equal(scenes[1].characterStates.find(s => s.characterId === b.id).on, true) })