import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { createMindSpaceChatSaveService, mindspaceChatSaveServiceInternals, } from './mindspace-chat-save-service.mjs'; const USER_ID = '11111111-1111-4111-8111-111111111111'; test('chat save authority creates localized shared HTML and its public URL inside MindSpace', async () => { const calls = []; const service = createMindSpaceChatSaveService({ pool: { query: async () => [[]] }, h5Root: '/srv/h5', storageRoot: '/srv/storage', resolveMindSpaceUserPublishDirFn: () => '/srv/h5/MindSpace/user-1', async inlinePrivateAssetsInHtmlFn( pool, storageRoot, userId, html, ) { calls.push({ kind: 'inline', pool, storageRoot, userId, html, }); return { html: 'localized' }; }, rewriteWorkspacePublicAssetReferencesFn( html, relativePath, ) { calls.push({ kind: 'rewrite', html, relativePath, }); return 'shared'; }, async mkdirFn(directory, options) { calls.push({ kind: 'mkdir', directory, options }); }, async writeFileFn(...args) { calls.push({ kind: 'write', args }); }, randomUUIDFn: () => '12345678-1234-4123-8123-123456789abc', buildMindSpacePublicUrlForUserFn(input) { calls.push({ kind: 'url', input }); return 'https://example.com/MindSpace/user-1/public/shared/report-12345678.html'; }, }); const result = await service.createSharedHtml({ userId: 'user-1', html: 'private', sourceFilename: '../unsafe report.html', }); assert.equal( result.relativePath, 'public/shared/unsafe-report-12345678.html', ); assert.equal( result.publicUrl, 'https://example.com/MindSpace/user-1/public/shared/report-12345678.html', ); assert.equal(calls[0].kind, 'inline'); assert.equal(calls[1].kind, 'rewrite'); assert.equal(calls[2].kind, 'mkdir'); assert.equal(calls[3].kind, 'write'); assert.equal( calls[3].args[0], '/srv/h5/MindSpace/user-1/public/shared/unsafe-report-12345678.html', ); assert.equal(calls[3].args[1], 'shared'); assert.equal(calls[4].kind, 'url'); }); test('chat save authority validates and reads workspace HTML inside MindSpace', async () => { const calls = []; const service = createMindSpaceChatSaveService({ pool: { query: async () => [[]] }, h5Root: '/srv/h5', storageRoot: '/srv/storage', resolvePublishHtmlAbsolutePathFn( h5Root, userId, relativePath, ) { calls.push({ kind: 'resolve', h5Root, userId, relativePath, }); return '/srv/h5/MindSpace/user-1/public/page.html'; }, async readFileFn(...args) { calls.push({ kind: 'read', args }); return 'Page'; }, }); const result = await service.readWorkspaceHtml({ userId: 'user-1', relativePath: 'public/page.html', }); assert.equal( result.html, 'Page', ); assert.equal(result.relativePath, 'public/page.html'); assert.equal(calls[0].kind, 'resolve'); assert.equal(calls[1].kind, 'read'); }); test('chat save authority repairs, materializes, and writes workspace HTML inside MindSpace', async () => { const h5Root = await fs.mkdtemp( path.join(os.tmpdir(), 'mindspace-chat-save-authority-'), ); const storageRoot = path.join(h5Root, 'data', 'mindspace'); const storageKey = 'users/u1/assets/asset-good/source.png'; const htmlPath = path.join( h5Root, 'MindSpace', USER_ID, 'public', 'report.html', ); await fs.mkdir(path.dirname(htmlPath), { recursive: true }); await fs.mkdir( path.dirname(path.join(storageRoot, storageKey)), { recursive: true }, ); await fs.writeFile(htmlPath, 'old', 'utf8'); await fs.writeFile( path.join(storageRoot, storageKey), Buffer.from('png payload'), ); const pool = { async query(sql) { if (sql.includes('SELECT id FROM h5_assets')) { return [[{ id: 'asset-good' }]]; } if (sql.includes('JOIN h5_asset_versions')) { return [[{ id: 'asset-good', mime_type: 'image/png', original_filename: 'source.png', storage_key: storageKey, }]]; } throw new Error(`Unexpected SQL: ${sql}`); }, }; let writeCount = 0; const service = createMindSpaceChatSaveService({ pool, h5Root, storageRoot, writeFileFn: async (...args) => { writeCount += 1; return fs.writeFile(...args); }, }); const sourceContent = `/api/mindspace/v1/assets/asset-good/download`; const html = ''; const first = await service.materializeWorkspaceHtml({ userId: USER_ID, sourceContent, html, relativePath: 'public/report.html', }); assert.equal(first.changed, true); assert.equal(first.repairedAssetReferences, true); assert.equal(first.materializedAssetCount, 1); assert.equal(first.relativePath, 'public/report.html'); assert.match(first.html, /\.tmp-images\/asset-good\.png/); assert.doesNotMatch(first.html, /\/api\/mindspace\/v1\/assets\//); assert.equal(await fs.readFile(htmlPath, 'utf8'), first.html); assert.equal( ( await fs.readFile( path.join( h5Root, 'MindSpace', USER_ID, 'public', '.tmp-images', 'asset-good.png', ), ) ).toString(), 'png payload', ); const second = await service.materializeWorkspaceHtml({ userId: USER_ID, sourceContent, html: first.html, relativePath: 'public/report.html', }); assert.equal(second.changed, false); assert.equal(writeCount, 1); }); test('chat save authority renders preview thumbnail svg inside MindSpace', async () => { const h5Root = await fs.mkdtemp( path.join(os.tmpdir(), 'mindspace-chat-save-thumb-'), ); const storageRoot = path.join(h5Root, 'data', 'mindspace'); const htmlPath = path.join( h5Root, 'MindSpace', USER_ID, 'public', 'report.html', ); await fs.mkdir(path.dirname(htmlPath), { recursive: true }); await fs.writeFile(htmlPath, 'Report', 'utf8'); const pool = { query: async () => [[]] }; const service = createMindSpaceChatSaveService({ pool, h5Root, storageRoot, }); const rendered = await service.renderPreviewThumbnailSvg({ userId: USER_ID, relativePath: 'public/report.html', html: 'Report', title: 'Report', subtitle: 'Summary', force: true, }); assert.match(rendered.svg, / { const h5Root = await fs.mkdtemp( path.join(os.tmpdir(), 'mindspace-chat-save-ensure-'), ); const storageRoot = path.join(h5Root, 'data', 'mindspace'); const htmlPath = path.join( h5Root, 'MindSpace', USER_ID, 'public', 'report.html', ); await fs.mkdir(path.dirname(htmlPath), { recursive: true }); await fs.writeFile(htmlPath, 'Report', 'utf8'); const service = createMindSpaceChatSaveService({ pool: { query: async () => [[]] }, h5Root, storageRoot, }); const result = await service.ensurePreviewThumbnail({ userId: USER_ID, relativePath: 'public/report.html', html: 'Report', title: 'Report', subtitle: 'Summary', force: true, }); assert.equal(result.ready, true); assert.equal(result.relativePath, 'public/report.html'); await fs.access( path.join( h5Root, 'MindSpace', USER_ID, 'public', 'report.thumbnail.svg', ), ); }); test('chat save authority rejects paths outside the public HTML zone', async () => { const service = createMindSpaceChatSaveService({ pool: { query: async () => [[]] }, h5Root: '/tmp/h5', storageRoot: '/tmp/storage', }); await assert.rejects( () => service.materializeWorkspaceHtml({ userId: USER_ID, html: 'unsafe', relativePath: '../private/report.html', }), (error) => error?.code === 'invalid_page_path', ); assert.throws( () => mindspaceChatSaveServiceInternals .normalizePublicHtmlRelativePath('oa/report.html'), (error) => error?.code === 'invalid_page_path', ); }); test('chat save authority preserves best-effort repair behavior', async () => { let writeCalled = false; const service = createMindSpaceChatSaveService({ pool: { query: async () => [[]] }, h5Root: '/tmp/h5', storageRoot: '/tmp/storage', repairMissingHtmlAssetReferencesFn: async () => { throw new Error('repair unavailable'); }, materializePrivateAssetsInWorkspaceHtmlFn: async () => { throw new Error('storage unavailable'); }, writeFileFn: async () => { writeCalled = true; }, }); const html = 'unchanged'; const result = await service.materializeWorkspaceHtml({ userId: USER_ID, sourceContent: 'source', html, relativePath: 'report.html', }); assert.equal(result.html, html); assert.equal(result.relativePath, 'public/report.html'); assert.equal(result.changed, false); assert.equal(writeCalled, false); });