const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const { JSDOM } = require('jsdom'); const { webcrypto, createHash } = require('node:crypto'); const read = file => fs.readFileSync('public/js/' + file, 'utf8'); const tick = () => new Promise(resolve => setImmediate(resolve)); const deferred = () => { let resolve, reject; const promise = new Promise((a, b) => { resolve = a; reject = b; }); return { promise, resolve, reject }; }; const id = '12345678-1234-1234-1234-123456789abc'; const src = '/api/generated-images/' + id; const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aL9sAAAAASUVORK5CYII=', 'base64'); const image = 'data:image/png;base64,' + png.toString('base64'); function ui(t, { privateAsset = true, inline = true } = {}) { const dom = new JSDOM('
', { url: 'https://synthetic.test', runScripts: 'outside-only' }); const w = dom.window; Object.defineProperty(w, 'crypto', { value: webcrypto }); w.Blob = Blob; w.TextEncoder = TextEncoder; const observers = []; const Observer = w.MutationObserver; w.MutationObserver = class extends Observer { constructor(callback) { super(callback); observers.push(this); } }; // Actual account-boundary implementation with disposable in-memory identity/storage. w.eval(read('accountBoundary.js')); assert.equal(w.AccountBoundary.enter({ id: '101' }, true), true); w.getAuthHeaders = () => ({ Authorization: 'Bearer synthetic-only' }); const calls = { writes: [], shares: [], prints: [], toasts: [], fetches: [], focus: [] }; const timers = []; w.setTimeout = fn => { timers.push(fn); return timers.length; }; w.clearTimeout = timer => { timers[timer - 1] = () => {}; }; w.matchMedia = () => ({ matches: inline }); w.showToast = (...args) => calls.toasts.push(args); w.print = () => calls.prints.push('browser'); w.FileReader = class { readAsDataURL(blob) { blob.arrayBuffer().then(bytes => { this.result = 'data:' + blob.type + ';base64,' + Buffer.from(bytes).toString('base64'); this.onload(); }); } }; function response(url) { if (url.includes('/jobs/')) return new Response(JSON.stringify({ success: true, status: 'done', jobId: id, imageUrl: src, context: { includedTurns: 1, totalTurns: 2, used: 40, limit: 32000, unit: 'UTF-16 code units' } })); return new Response(png, { headers: { 'content-type': 'image/png', 'content-length': String(png.length), 'x-image-owner': '101', 'x-image-sha256': createHash('sha256').update(png).digest('hex') } }); } w.fetch = async (url, options) => { calls.fetches.push({ url, options }); return response(url); }; for (const file of ['generatedImages.js', 'assistant/citations.js', 'assistant/sharing.js', 'assistant/export.js']) { // Allows the failing-first run against the frozen image input without this core helper. if (file.endsWith('sharing.js') && !fs.existsSync('public/js/' + file)) continue; vm.runInContext(read(file).replace(/^import[^;]+;\s*/gm, '').replace(/^export /gm, ''), dom.getInternalVMContext()); } const exporter = w.createAssistantExporter({ showToast: w.showToast }); const state = { lastAnswer: 'Exact body [3].', lastSources: [{ number: 3, title: 'Synthetic source', page: 19 }], lastGeneratedImageSrc: privateAsset ? src : image, generatedImageJobs: privateAsset ? [{ jobId: id }] : [], messages: [{ role: 'assistant', content: 'Exact body [3].', ...(privateAsset ? { imageJobs: [{ jobId: id }] } : {}) }] }; async function exported() { await exporter.exportAnswerPdf(state); if (!privateAsset) await exporter.exportAnswerPdf(state); // Exercise the legacy cache-hit branch too. return w.document.querySelector('#assistant-export-modal'); } function plugins() { w.Capacitor = { isNativePlatform: () => true, Plugins: { Filesystem: { writeFile: async args => { calls.writes.push(args); return { uri: 'file://' + args.path }; } }, Share: { share: async args => { calls.shares.push(args); } } } }; return w.Capacitor.Plugins; } function replace({ freeze = true, sameId = false } = {}) { if (freeze) w.AccountBoundary.freeze(); const b = new JSDOM('', { url: 'https://synthetic.test', runScripts: 'outside-only' }); b.window.eval(read('accountBoundary.js')); assert.equal(b.window.AccountBoundary.enter({ id: sameId ? '101' : '102' }, true), true); w.AccountBoundary = b.window.AccountBoundary; t.after(() => b.window.close()); } function popup() { const p = new JSDOM('', { url: 'https://synthetic.test', runScripts: 'outside-only' }); let closed = false; const target = { document: p.window.document, get closed() { return closed; }, focus: () => calls.focus.push('popup'), print: () => calls.prints.push('popup'), close() { closed = true; } }; w.open = () => target; t.after(() => p.window.close()); return target; } t.after(() => { observers.forEach(observer => observer.disconnect()); w.close(); }); return { w, context: dom.getInternalVMContext(), calls, timers, state, exporter, exported, plugins, replace, popup, response }; } for (const privateAsset of [true, false]) { const branch = privateAsset ? 'private assets' : 'legacy cache'; for (const freeze of [true, false]) for (const stage of ['filesystem', 'share', 'native resolve', 'native reject']) { test(`${branch}: deferred ${stage} -> ${freeze ? 'freeze' : 'same-ID object replacement without freeze'} -> resolve/reject has no later effects or UI writes`, async t => { const app = ui(t, { privateAsset }); const p = app.plugins(); const pending = deferred(); if (stage === 'filesystem') p.Filesystem.writeFile = args => { app.calls.writes.push(args); return pending.promise; }; if (stage === 'share') p.Share.share = args => { app.calls.shares.push(args); return pending.promise; }; if (stage.startsWith('native')) app.w.NativePrint = { printHtml(...args) { app.calls.prints.push(args); pending.promise.catch(() => {}); return pending.promise; } }; const modal = await app.exported(); const print = modal.querySelector('#assistant-export-print'); print.click(); await tick(); assert.equal(stage.startsWith('native') ? app.calls.prints.length : app.calls.writes.length, 1, 'first OS operation admitted'); assert.equal(print.disabled, true, 'await the admitted operation before updating UI'); app.replace({ freeze, sameId: !freeze }); assert.equal(modal.isConnected, !freeze); const before = JSON.stringify(app.calls); const text = print.textContent; const disabled = print.disabled; if (stage === 'native reject') pending.reject(new Error('Synthetic native capability failure')); else pending.resolve({ uri: 'file://synthetic-owned' }); await tick(); assert.equal(JSON.stringify(app.calls), before, 'no follow-up native calls, browser fallback, fetches or toasts'); assert.equal(print.textContent, text, 'no late finally text reset'); assert.equal(print.disabled, disabled, 'no late finally enable'); }); } for (const replacement of ['freeze', 'same-id object']) { test(`${branch}: original Print callback rejects ${replacement} replacement and cannot close newer preview`, async t => { const app = ui(t, { privateAsset }); app.plugins(); const old = await app.exported(); const print = old.querySelector('#assistant-export-print'); app.replace({ freeze: replacement === 'freeze', sameId: true }); const newer = await app.exported(); const before = JSON.stringify(app.calls); print.click(); await tick(); assert.equal(JSON.stringify(app.calls), before); assert.equal(newer.isConnected, true); assert.equal(app.w.document.body.classList.contains('assistant-export-open'), true); assert.ok(app.w.document.querySelector('#unrelated')); }); } for (const stage of ['native sync failure', 'browser fallback', 'native AbortError', 'share AbortError']) { test(`${branch}: ${stage} does not become a capability fallback after abort`, async t => { const app = ui(t, { privateAsset }); const p = app.plugins(); if (stage === 'native sync failure') app.w.NativePrint = { printHtml() { app.replace(); throw new Error('Unavailable'); } }; if (stage === 'native AbortError') app.w.NativePrint = { printHtml() { throw new app.w.DOMException('Policy abort', 'AbortError'); } }; if (stage === 'share AbortError') p.Share.share = async args => { app.calls.shares.push(args); throw new app.w.DOMException('Policy abort', 'AbortError'); }; if (stage === 'browser fallback') app.w.Capacitor.Plugins = {}; const modal = await app.exported(); modal.querySelector('#assistant-export-print').click(); if (stage === 'browser fallback') app.replace(); await tick(); assert.equal(app.calls.writes.length, stage === 'share AbortError' ? 1 : 0); assert.deepEqual(app.calls.prints, []); assert.deepEqual(app.calls.toasts, []); }); } for (const mode of ['native', 'filesystem', 'browser', 'native failure fallback', 'share cancel']) { test(`${branch}: same owner ${mode} still succeeds with exact embedded bytes and source/page`, async t => { const app = ui(t, { privateAsset }); const p = app.plugins(); if (mode === 'native') app.w.NativePrint = { printHtml: (...args) => app.calls.prints.push(args) }; if (mode === 'native failure fallback') app.w.NativePrint = { printHtml: async () => { throw new Error('Unavailable'); } }; if (mode === 'browser') app.w.Capacitor.Plugins = {}; if (mode === 'share cancel') p.Share.share = async args => { app.calls.shares.push(args); throw new Error('User canceled'); }; const original = JSON.stringify(app.state); const modal = await app.exported(); modal.querySelector('#assistant-export-print').click(); await tick(); assert.equal(app.calls.prints.length + app.calls.shares.length, 1); const encoded = mode === 'native' ? app.calls.prints[0][1] : app.calls.writes[0]?.data; const html = encoded ? Buffer.from(encoded, 'base64').toString() : modal.innerHTML; assert.ok(html.includes(image)); assert.match(html, /Exact body \[3\]\./); assert.match(html, /Synthetic source, page 19/); assert.equal(JSON.stringify(app.state), original); assert.equal(modal.querySelector('#assistant-export-print').disabled, false); if (privateAsset) assert.equal(app.calls.fetches[0].options.headers.Authorization, 'Bearer synthetic-only'); }); } test(`${branch}: delayed desktop print and original popup button keep their owner`, async t => { const app = ui(t, { privateAsset, inline: false }); const target = app.popup(); await app.exporter.exportAnswerPdf(app.state); const print = target.document.querySelector('#assistant-export-print'); const delayed = app.timers.slice(); app.replace(); print.click(); delayed.forEach(fn => fn()); assert.equal(target.closed, true); assert.deepEqual(app.calls.focus, []); assert.deepEqual(app.calls.prints, []); assert.deepEqual(app.calls.toasts, []); }); test(`${branch}: same-owner desktop print works`, async t => { const app = ui(t, { privateAsset, inline: false }); const target = app.popup(); await app.exporter.exportAnswerPdf(app.state); app.timers.forEach(fn => fn()); target.document.querySelector('#assistant-export-print').click(); assert.deepEqual(app.calls.prints, ['popup', 'popup']); assert.equal(target.closed, false); }); } for (const freeze of [true, false]) for (const stage of ['job resolve', 'job reject', 'asset resolve']) { for (const inline of [true, false]) { test(`private prepare ${stage} after boundary replacement is silent (${inline ? 'inline' : 'desktop'})${freeze ? '' : ' without freeze'}`, async t => { const app = ui(t, { inline }); const target = inline ? null : app.popup(); const pending = deferred(); let admitted; app.w.fetch = (url, options) => { app.calls.fetches.push({ url, options }); if (!admitted && ((stage.startsWith('job') && url.includes('/jobs/')) || (stage === 'asset resolve' && url === src))) { admitted = url; return pending.promise; } return Promise.resolve(app.response(url)); }; const job = app.exporter.exportAnswerPdf(app.state); await tick(); assert.ok(admitted); app.replace({ freeze, sameId: true }); const before = JSON.stringify(app.calls); if (stage.endsWith('reject')) pending.reject(new Error('Synthetic transport failure')); else pending.resolve(app.response(admitted)); await job; await tick(); assert.equal(JSON.stringify(app.calls), before); assert.equal(app.w.document.querySelector('#assistant-export-modal'), null); if (target) assert.equal(target.closed, true); }); } } test('late filesystem completion has a unique owned path and cannot close or update a newer export', async t => { const app = ui(t, { privateAsset: false }); const p = app.plugins(); const pending = deferred(); app.w.Date = class extends app.w.Date { constructor() { super('2026-01-02T03:04:05Z'); } }; p.Filesystem.writeFile = args => { app.calls.writes.push(args); return app.calls.writes.length === 1 ? pending.promise : Promise.resolve({ uri: 'file://' + args.path }); }; const old = await app.exported(); old.querySelector('#assistant-export-print').click(); await tick(); app.replace(); const newer = await app.exported(); newer.querySelector('#assistant-export-print').click(); await tick(); assert.equal(app.calls.writes.length, 2); assert.notEqual(app.calls.writes[0].path, app.calls.writes[1].path); const before = JSON.stringify(app.calls); pending.resolve({ uri: 'file://' + app.calls.writes[0].path }); await tick(); assert.equal(JSON.stringify(app.calls), before); assert.equal(newer.isConnected, true); }); test('export invocation fails closed without a verified boundary, before even an empty-answer toast', async t => { const app = ui(t); for (const boundary of [undefined, {}, { capture: () => '101' }]) { app.w.AccountBoundary = boundary; await app.exporter.exportAnswerPdf({}); assert.equal(app.w.document.querySelector('#assistant-export-modal'), null); assert.deepEqual(app.calls.toasts, []); } }); for (const freeze of [true, false]) for (const stage of ['fetch', 'body', 'reader', 'filesystem', 'share']) { test(`private image download ${stage} retains original object/ticket/signal (${freeze ? 'freeze' : 'same-ID replacement without freeze'})`, { timeout: 5000 }, async t => { const app = ui(t); const pending = deferred(); const admitted = deferred(); const p = app.plugins(); app.calls.native = []; app.calls.anchors = []; app.w.HTMLAnchorElement.prototype.click = function() { app.calls.anchors.push(this.download); }; app.w.NativeFiles = { saveImage(...args) { app.calls.native.push(args); return 'saved:synthetic'; } }; const originalSignal = app.w.AccountBoundary.signal(); if (stage === 'fetch' || stage === 'body') app.w.fetch = (url, options) => { app.calls.fetches.push({ url, options }); if (stage === 'fetch') { admitted.resolve(); return pending.promise; } const response = app.response(url); return Promise.resolve({ ok: true, headers: response.headers, body: { getReader: () => ({ read: () => { admitted.resolve(); return pending.promise; } }) } }); }; if (stage === 'reader') app.w.FileReader = class { readAsDataURL() { admitted.resolve(); pending.promise.then(() => { this.result = image; this.onload(); }); } }; if (stage === 'filesystem' || stage === 'share') delete app.w.NativeFiles; if (stage === 'filesystem') p.Filesystem.writeFile = args => { app.calls.writes.push(args); admitted.resolve(); return pending.promise; }; if (stage === 'share') p.Share.share = args => { app.calls.shares.push(args); admitted.resolve(); return pending.promise; }; vm.runInContext(read('assistant/images.js').replace(/^import[^;]+;\s*/gm, '').replace(/^export /gm, ''), app.context); const store = app.w.createAssistantImageStore(); store.renderGeneratedImage(src); const job = store.downloadImage('img-1'); await admitted.promise; assert.equal(app.calls.fetches.length, 1); assert.equal(app.calls.fetches[0].options.signal, originalSignal); assert.equal(app.calls.fetches[0].options.redirect, 'error'); if (stage === 'filesystem' || stage === 'share') assert.equal(app.calls.writes.length, 1, 'paid bytes admitted only under the original owner'); if (stage === 'share') assert.equal(app.calls.shares.length, 1); app.replace({ freeze, sameId: true }); const before = JSON.stringify(app.calls); pending.resolve(stage === 'fetch' ? app.response(src) : stage === 'body' ? { done: false, value: new Uint8Array(png) } : { uri: 'file://original-only' }); await job; await tick(); assert.equal(JSON.stringify(app.calls), before, 'no native, Filesystem, Share, anchor or toast after replacement'); }); } test('late private preview load and detached close cannot hydrate or close a newer same-ID preview', async t => { const app = ui(t); const pending = deferred(); app.w.fetch = () => pending.promise; vm.runInContext(read('assistant/images.js').replace(/^import[^;]+;\s*/gm, '').replace(/^export /gm, ''), app.context); const store = app.w.createAssistantImageStore(); store.renderGeneratedImage(src); store.openImagePreview('img-1'); const old = app.w.document.querySelector('.assistant-image-modal'); assert.ok(old); assert.equal(old.querySelector('img').getAttribute('src'), null); const close = old.querySelector('.assistant-image-modal-close'); app.replace({ freeze: false, sameId: true }); store.renderGeneratedImage(image); store.openImagePreview('img-2'); const newer = app.w.document.querySelector('.assistant-image-modal'); pending.resolve(app.response(src)); await tick(); await tick(); close.click(); assert.equal(newer.isConnected, true); assert.equal(newer.querySelector('img').getAttribute('src'), image); assert.equal(app.w.document.body.classList.contains('assistant-image-preview-open'), true); assert.equal(old.isConnected, false); assert.deepEqual(app.calls.toasts, []); }); test('a superseded boundary abort cleans up only its export, never a newer same-ID export', async t => { const app = ui(t); const originalBoundary = app.w.AccountBoundary; const old = await app.exported(); app.replace({ freeze: false, sameId: true }); const newer = await app.exported(); originalBoundary.freeze(); assert.equal(old.isConnected, false); assert.equal(newer.isConnected, true); assert.equal(app.w.document.body.classList.contains('assistant-export-open'), true); }); test('private transient cleanup revokes only the obsolete owner URLs and preserves newer and unrelated blobs', async t => { const app = ui(t); const revoked = []; let seq = 0; app.w.URL.createObjectURL = () => 'blob:synthetic-' + ++seq; app.w.URL.revokeObjectURL = url => revoked.push(url); const originalBoundary = app.w.AccountBoundary; const oldUrl = app.w.transientImageUrl(new Blob([png])); app.replace({ freeze: false, sameId: true }); const newUrl = app.w.transientImageUrl(new Blob([png])); app.w.document.body.insertAdjacentHTML('beforeend', '