import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import test from 'node:test'; import vm from 'node:vm'; import ts from 'typescript'; function loadTsModule(relativePath) { const filename = resolve(relativePath); const source = readFileSync(filename, 'utf8'); const { outputText } = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, }, fileName: filename, }); const module = { exports: {} }; const sandbox = { exports: module.exports, module, require(specifier) { throw new Error(`Unexpected require from ${relativePath}: ${specifier}`); }, }; vm.runInNewContext(outputText, sandbox, { filename }); return module.exports; } test('revision resets stream content tracking so polish failures can fall back', () => { const { createAssistantStreamState } = loadTsModule('src/lib/aiChatStreamState.ts'); const state = createAssistantStreamState(); assert.equal(state.shouldUseFallback(), true); state.markContentReceived(); assert.equal(state.shouldUseFallback(), false); state.resetForRevision(); assert.equal(state.shouldUseFallback(), true); state.markContentReceived(); assert.equal(state.shouldUseFallback(), false); }); test('error finalization does not mark the currently streaming document ready', () => { const { finalizeAssistantMessage } = loadTsModule('src/lib/aiChatStreamState.ts'); const message = { status: 'streaming', stage: { id: 'polish', label: 'Polishing' }, docs: [ { id: 'first', title: 'First pass', html: '

complete

', status: 'ready' }, { id: 'polish', title: 'Polished', html: '

partial', status: 'streaming' }, ], }; finalizeAssistantMessage(message, 'error'); assert.equal(message.status, 'error'); assert.equal(message.stage, undefined); assert.equal(message.docs[0].status, 'ready'); assert.equal(message.docs[1].status, 'error'); }); test('successful finalization marks open documents ready', () => { const { finalizeAssistantMessage } = loadTsModule('src/lib/aiChatStreamState.ts'); const message = { status: 'streaming', docs: [ { id: 'doc', title: 'Draft', html: '

complete

', status: 'streaming' }, ], }; finalizeAssistantMessage(message, 'done'); assert.equal(message.status, 'done'); assert.equal(message.docs[0].status, 'ready'); }); test('persisted interrupted messages recover as errors, not ready documents', () => { const { recoverInterruptedAssistantMessage } = loadTsModule('src/lib/aiChatStreamState.ts'); const message = { status: 'streaming', docs: [ { id: 'closed', title: 'Closed', html: '

done

', status: 'ready' }, { id: 'open', title: 'Open', html: '

partial', status: 'streaming' }, ], }; recoverInterruptedAssistantMessage(message); assert.equal(message.status, 'error'); assert.equal(message.docs[0].status, 'ready'); assert.equal(message.docs[1].status, 'error'); }); test('parser can flush an incomplete document without closing it as ready', () => { const { createVensuiteDocParser } = loadTsModule('src/lib/vensuiteDocStream.ts'); const events = []; const parser = createVensuiteDocParser({ onText: (text) => events.push(['text', text]), onDocStart: (title) => events.push(['start', title]), onDocChunk: (html) => events.push(['chunk', html]), onDocEnd: () => events.push(['end']), }); parser.push('

half'); parser.flush({ completeOpenDocument: false }); assert.deepEqual(events, [ ['start', 'Partial'], ['chunk', '

half'], ]); });