import assert from 'node:assert/strict'; import test from 'node:test'; import { attachPortalMindSpaceChatShareRoutes } from './portal-mindspace-chat-share-routes.mjs'; function createRouterRecorder() { const routes = new Map(); return { routes, get(path, handler) { routes.set(`GET ${path}`, handler); }, post(path, handler) { routes.set(`POST ${path}`, handler); }, }; } function createResponseRecorder() { return { statusCode: 200, body: undefined, status(code) { this.statusCode = code; return this; }, json(body) { this.body = body; return this; }, }; } function createRequest(overrides = {}) { return { body: {}, query: {}, currentUser: { id: 'user-1', username: 'alice' }, ...overrides, }; } function createBundle() { return { resolvedHtml: { content: 'Page', filename: 'page.html', }, }; } function createDependencies(overrides = {}) { const services = { pages: { id: 'pages' }, publications: { id: 'publications' }, plazaPosts: { id: 'plaza-posts' }, }; const calls = { data: [], errors: [], plazaErrors: [], }; return { calls, services, dependencies: { h5Root: '/workspace', env: {}, getMindSpacePages: () => services.pages, getMindSpacePublications: () => services.publications, getPlazaPosts: () => services.plazaPosts, resolveChatSaveBundle: async () => createBundle(), resolvePlazaPostUrl: (postId) => `/plaza/p/${postId}`, sendData(res, req, data, status = 200) { calls.data.push({ res, req, data, status }); return res.status(status).json({ data }); }, sendError(res, req, status, code, message) { calls.errors.push({ res, req, status, code, message }); return res.status(status).json({ error: { code, message } }); }, handlePlazaError(res, req, error) { calls.plazaErrors.push({ res, req, error }); return res.status(409).json({ plaza: error.code }); }, handleMindSpaceError(res, req, error) { calls.errors.push({ res, req, error }); return res.status(418).json({ mapped: error.message }); }, logger: { log() {}, info() {}, error() {}, }, ...overrides, }, }; } test('chat share module preserves route inventory and order', () => { const api = createRouterRecorder(); attachPortalMindSpaceChatShareRoutes(api, createDependencies().dependencies); assert.deepEqual([...api.routes.keys()], [ 'POST /mindspace/v1/pages/quick-share-from-chat', 'POST /mindspace/v1/pages/quick-plaza-from-chat', 'GET /mindspace/v1/pages/quick-plaza-from-public-html/status', 'POST /mindspace/v1/pages/quick-plaza-from-public-html', ]); }); test('quick share preserves localization, path, write, and 201 response', async () => { const calls = []; const api = createRouterRecorder(); const dependencies = createDependencies({ getAuthPool: () => ({ id: 'pool' }), resolveMindSpaceRuntimeConfigFn: () => ({ storageRoot: '/storage', }), async inlinePrivateAssetsInHtmlFn( pool, storageRoot, userId, html, ) { calls.push({ kind: 'inline', pool, storageRoot, userId, html }); return { html: 'Localized' }; }, resolveMindSpaceUserPublishDirFn: () => '/workspace/user', async mkdir(dir, options) { calls.push({ kind: 'mkdir', dir, options }); }, rewriteWorkspacePublicAssetReferencesFn(html, relativePath) { calls.push({ kind: 'rewrite', html, relativePath }); return 'Rewritten'; }, async writeFile(filePath, content, encoding) { calls.push({ kind: 'write', filePath, content, encoding }); }, randomUUID: () => '12345678-aaaa-bbbb-cccc-dddddddddddd', buildMindSpacePublicUrlForUserFn(input) { calls.push({ kind: 'url', input }); return 'https://example.com/MindSpace/user/public/shared/page-12345678.html'; }, }); attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies); const res = createResponseRecorder(); await api.routes.get('POST /mindspace/v1/pages/quick-share-from-chat')( createRequest({ body: { session_id: 'session-1' } }), res, ); assert.equal(res.statusCode, 201); assert.deepEqual(res.body, { data: { publicUrl: 'https://example.com/MindSpace/user/public/shared/page-12345678.html', filename: 'page-12345678.html', }, }); assert.deepEqual(calls.find((call) => call.kind === 'inline'), { kind: 'inline', pool: { id: 'pool' }, storageRoot: '/storage', userId: 'user-1', html: 'Page', }); assert.deepEqual(calls.find((call) => call.kind === 'rewrite'), { kind: 'rewrite', html: 'Localized', relativePath: 'public/shared/page-12345678.html', }); assert.deepEqual(calls.find((call) => call.kind === 'write'), { kind: 'write', filePath: '/workspace/user/public/shared/page-12345678.html', content: 'Rewritten', encoding: 'utf8', }); }); test('quick Plaza from chat preserves dependencies, timing, result, and errors', async () => { const forwarded = []; let clock = 1_000; const api = createRouterRecorder(); const dependencies = createDependencies({ resolveMindSpaceUserPublishDirFn: () => '/workspace/user', now: () => { clock += 25; return clock; }, async quickPlazaFromChatFn(input) { forwarded.push(input); return { pageId: 'page-1', publicationId: 'publication-1', post: { id: 'post-1' }, }; }, }); attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies); const req = createRequest({ body: { title: 'Post title' } }); await api.routes.get('POST /mindspace/v1/pages/quick-plaza-from-chat')( req, createResponseRecorder(), ); assert.equal(dependencies.calls.data[0].status, 201); assert.deepEqual(forwarded[0], { user: req.currentUser, h5Root: '/workspace', bundle: createBundle(), body: { title: 'Post title' }, mindSpacePages: dependencies.services.pages, mindSpacePublications: dependencies.services.publications, plazaPosts: dependencies.services.plazaPosts, publishDir: '/workspace/user', }); const failureApi = createRouterRecorder(); const plazaFailure = Object.assign(new Error('conflict'), { code: 'ALREADY_PUBLISHED', }); const failureDependencies = createDependencies({ quickPlazaFromChatFn: async () => { throw plazaFailure; }, resolveMindSpaceUserPublishDirFn: () => '/workspace/user', mapPlazaError: () => 409, }); attachPortalMindSpaceChatShareRoutes( failureApi, failureDependencies.dependencies, ); await failureApi.routes.get( 'POST /mindspace/v1/pages/quick-plaza-from-chat', )(createRequest(), createResponseRecorder()); assert.equal( failureDependencies.calls.plazaErrors[0].error, plazaFailure, ); }); test('quick Plaza status preserves relative path, viewer result, and URL', async () => { const forwarded = []; const api = createRouterRecorder(); const dependencies = createDependencies({ async getQuickPlazaFromPublicHtmlStatusFn(input) { forwarded.push(input); return { published: true, post: { id: 'post-1' }, }; }, resolvePlazaPostUrl(postId, req) { assert.equal(req.query.relativePath, ' public/page.html '); return `https://plaza.example.com/p/${postId}`; }, }); attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies); const req = createRequest({ query: { relativePath: ' public/page.html ' }, }); await api.routes.get( 'GET /mindspace/v1/pages/quick-plaza-from-public-html/status', )(req, createResponseRecorder()); assert.equal(forwarded[0].relativePath, 'public/page.html'); assert.equal(forwarded[0].mindSpacePages, dependencies.services.pages); assert.deepEqual(dependencies.calls.data[0].data, { published: true, post: { id: 'post-1' }, plaza_url: 'https://plaza.example.com/p/post-1', }); }); test('quick Plaza public HTML preserves publish input, URL, and 201 response', async () => { const forwarded = []; const api = createRouterRecorder(); const dependencies = createDependencies({ resolveMindSpaceUserPublishDirFn: () => '/workspace/user', async quickPlazaFromPublicHtmlFn(input) { forwarded.push(input); return { pageId: 'page-1', publicationId: 'publication-1', post: { id: 'post-1' }, }; }, }); attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies); await api.routes.get( 'POST /mindspace/v1/pages/quick-plaza-from-public-html', )( createRequest({ body: { relative_path: ' public/page.html ' }, }), createResponseRecorder(), ); assert.equal(forwarded[0].relativePath, 'public/page.html'); assert.equal(forwarded[0].publishDir, '/workspace/user'); assert.equal(dependencies.calls.data[0].status, 201); assert.deepEqual(dependencies.calls.data[0].data, { pageId: 'page-1', publicationId: 'publication-1', post: { id: 'post-1' }, plaza_url: '/plaza/p/post-1', }); }); test('quick Plaza public HTML enriches already-published errors with URL', async () => { const failure = Object.assign(new Error('already published'), { code: 'ALREADY_PUBLISHED', details: { post_id: 'post-existing' }, }); const api = createRouterRecorder(); const dependencies = createDependencies({ resolveMindSpaceUserPublishDirFn: () => '/workspace/user', quickPlazaFromPublicHtmlFn: async () => { throw failure; }, mapPlazaError: () => 409, }); attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies); await api.routes.get( 'POST /mindspace/v1/pages/quick-plaza-from-public-html', )(createRequest(), createResponseRecorder()); assert.equal( failure.details.plaza_url, '/plaza/p/post-existing', ); assert.equal(dependencies.calls.plazaErrors[0].error, failure); }); test('chat share routes preserve availability gates and MindSpace fallback errors', async () => { const unavailableApi = createRouterRecorder(); const unavailable = createDependencies({ getMindSpacePages: () => null, getMindSpacePublications: () => null, getPlazaPosts: () => null, }); attachPortalMindSpaceChatShareRoutes( unavailableApi, unavailable.dependencies, ); const shareRes = createResponseRecorder(); await unavailableApi.routes.get( 'POST /mindspace/v1/pages/quick-share-from-chat', )(createRequest(), shareRes); assert.equal(shareRes.statusCode, 503); for (const route of [ 'POST /mindspace/v1/pages/quick-plaza-from-chat', 'GET /mindspace/v1/pages/quick-plaza-from-public-html/status', 'POST /mindspace/v1/pages/quick-plaza-from-public-html', ]) { const res = createResponseRecorder(); await unavailableApi.routes.get(route)(createRequest(), res); assert.equal(res.statusCode, 503, route); assert.equal(res.body.error.code, 'plaza_unavailable'); } const failureApi = createRouterRecorder(); const failure = new Error('share failed'); const dependencies = createDependencies({ resolveChatSaveBundle: async () => { throw failure; }, }); attachPortalMindSpaceChatShareRoutes( failureApi, dependencies.dependencies, ); await failureApi.routes.get( 'POST /mindspace/v1/pages/quick-share-from-chat', )(createRequest(), createResponseRecorder()); assert.equal(dependencies.calls.errors.at(-1).error, failure); });