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' }, chatSave: { async createSharedHtml() { return { publicUrl: 'https://example.com/MindSpace/user-1/public/shared/page-default.html', filename: 'page-default.html', relativePath: 'public/shared/page-default.html', }; }, async readWorkspaceHtml(input) { return { html: 'Page', relativePath: input.relativePath, }; }, }, publications: { id: 'publications' }, plazaPosts: { id: 'plaza-posts' }, }; const calls = { data: [], errors: [], plazaErrors: [], }; return { calls, services, dependencies: { h5Root: '/workspace', getMindSpacePages: () => services.pages, getMindSpaceChatSave: () => services.chatSave, 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 delegates shared HTML write and URL authority to MindSpace', async () => { const calls = []; const api = createRouterRecorder(); const dependencies = createDependencies({ getMindSpaceChatSave: () => ({ async createSharedHtml(input) { calls.push(input); return { publicUrl: 'https://example.com/MindSpace/user/public/shared/page-12345678.html', filename: 'page-12345678.html', relativePath: '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[0], { userId: 'user-1', html: 'Page', sourceFilename: 'page.html', }); }); test('quick Plaza from chat preserves dependencies, timing, result, and errors', async () => { const forwarded = []; let clock = 1_000; const api = createRouterRecorder(); const dependencies = createDependencies({ 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, bundle: createBundle(), body: { title: 'Post title' }, mindSpacePages: dependencies.services.pages, mindSpacePublications: dependencies.services.publications, plazaPosts: dependencies.services.plazaPosts, }); const failureApi = createRouterRecorder(); const plazaFailure = Object.assign(new Error('conflict'), { code: 'ALREADY_PUBLISHED', }); const failureDependencies = createDependencies({ quickPlazaFromChatFn: async () => { throw plazaFailure; }, 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({ 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( typeof forwarded[0].readWorkspaceHtml, 'function', ); assert.deepEqual( await forwarded[0].readWorkspaceHtml({ userId: 'user-1', relativePath: 'public/page.html', }), { html: 'Page', relativePath: 'public/page.html', }, ); 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({ 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, getMindSpaceChatSave: () => 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); });