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 { syncGeneratedPagesFromPublicAssets } from './mindspace-page-sync.mjs'; test('syncGeneratedPagesFromPublicAssets registers html files from publish public dir', async () => { const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-publish-')); const publicDir = path.join(publishDir, 'public'); await fs.mkdir(publicDir, { recursive: true }); await fs.writeFile( path.join(publicDir, 'demo.html'), 'Demo PageHi', 'utf8', ); const created = []; const pool = { async query(sql) { if (sql.includes('FROM h5_page_records p')) return [[]]; if (sql.includes('FROM h5_assets a')) return [[]]; return [[]]; }, }; const pageService = { async createFromChat(userId, input, source) { created.push({ userId, input, source }); return { id: 'page-new' }; }, async updatePage() { throw new Error('should not update'); }, }; const result = await syncGeneratedPagesFromPublicAssets({ pool, pageService, assetService: null, userId: 'user-1', publishDir, }); assert.equal(result.created, 1); assert.equal(created.length, 1); assert.equal(created[0].input.title, 'Demo Page'); assert.equal(created[0].source.snapshot.relative_path, 'public/demo.html'); }); test('syncGeneratedPagesFromPublicAssets scopes agent-run sync to current session html', async () => { const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-scoped-')); const publicDir = path.join(publishDir, 'public'); await fs.mkdir(publicDir, { recursive: true }); await fs.writeFile(path.join(publicDir, 'current.html'), 'Current', 'utf8'); await fs.writeFile(path.join(publicDir, 'historical.html'), 'Historical', 'utf8'); const created = []; const pool = { async query(sql) { if (sql.includes('FROM h5_page_records p')) return [[]]; if (sql.includes('FROM h5_assets a')) return [[]]; return [[]]; }, }; const pageService = { async createFromChat(_userId, input, source) { created.push({ input, source }); return { id: `page-${created.length}` }; }, }; await syncGeneratedPagesFromPublicAssets({ pool, pageService, userId: 'user-1', publishDir, onlyRelativePaths: ['public/current.html'], }); assert.deepEqual(created.map((item) => item.source.snapshot.relative_path), ['public/current.html']); await fs.rm(publishDir, { recursive: true, force: true }); }); test('syncGeneratedPagesFromPublicAssets uses file mtime as createdAt', async () => { const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-mtime-')); const publicDir = path.join(publishDir, 'public'); await fs.mkdir(publicDir, { recursive: true }); const htmlPath = path.join(publicDir, 'timed.html'); await fs.writeFile( htmlPath, 'TimedHi', 'utf8', ); const mtimeMs = Date.parse('2026-06-20T10:00:00.000Z'); await fs.utimes(htmlPath, mtimeMs / 1000, mtimeMs / 1000); const created = []; const pool = { async query(sql) { if (sql.includes('FROM h5_page_records p')) return [[]]; if (sql.includes('FROM h5_assets a')) return [[]]; return [[]]; }, }; const pageService = { async createFromChat(userId, input, source) { created.push({ userId, input, source }); return { id: 'page-new' }; }, async updatePage() { throw new Error('should not update'); }, }; await syncGeneratedPagesFromPublicAssets({ pool, pageService, assetService: null, userId: 'user-1', publishDir, }); assert.equal(created.length, 1); assert.equal(created[0].source.createdAt, mtimeMs); }); test('syncGeneratedPagesFromPublicAssets creates pages for unlinked public html assets', async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-')); const htmlPath = path.join(tempDir, 'demo.html'); await fs.writeFile(htmlPath, 'DemoHi', 'utf8'); const created = []; const pool = { async query(sql) { if (sql.includes('FROM h5_page_records p')) return [[]]; if (sql.includes('FROM h5_assets a')) { return [[{ id: 'asset-1', display_name: 'demo.html', original_filename: 'demo.html', updated_at: 100 }]]; } return [[]]; }, }; const pageService = { async createFromChat(userId, input, source) { created.push({ userId, input, source }); return { id: 'page-new' }; }, async updatePage() { throw new Error('should not update'); }, }; const assetService = { async readAsset() { return { path: htmlPath }; }, }; const result = await syncGeneratedPagesFromPublicAssets({ pool, pageService, assetService, userId: 'user-1', }); assert.equal(result.created, 1); assert.equal(created[0].source.assetId, 'asset-1'); }); test('syncGeneratedPagesFromPublicAssets reuses db page when memory index is empty', async () => { const publishDir = await fs.mkdtemp(path.join(os.tmpdir(), 'page-sync-dedupe-')); const publicDir = path.join(publishDir, 'public'); await fs.mkdir(publicDir, { recursive: true }); await fs.writeFile( path.join(publicDir, 'demo.html'), 'Demo PageHi', 'utf8', ); const created = []; const updated = []; const pool = { async query(sql) { if (sql.includes('JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json')) { return [[{ id: 'page-existing', updated_at: 100, version_no: 3 }]]; } if (sql.includes('FROM h5_page_records p')) return [[]]; if (sql.includes('FROM h5_assets a')) return [[]]; return [[]]; }, }; const pageService = { async createFromChat() { created.push(true); return { id: 'page-new' }; }, async updatePage(userId, pageId, input) { updated.push({ userId, pageId, input }); return { id: pageId }; }, }; const result = await syncGeneratedPagesFromPublicAssets({ pool, pageService, assetService: null, userId: 'user-1', publishDir, }); assert.equal(result.created, 0); assert.equal(created.length, 0); assert.equal(updated.length, 1); assert.equal(updated[0].pageId, 'page-existing'); assert.equal(updated[0].input.expectedVersion, 3); });