import assert from 'node:assert/strict'; import test from 'node:test'; import { attachPortalMindSpacePageCoreRoutes, createMindSpacePageListSyncScheduler, } from './portal-mindspace-page-core-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); }, put(path, handler) { routes.set(`PUT ${path}`, handler); }, delete(path, handler) { routes.set(`DELETE ${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: {}, params: { pageId: 'page-1' }, currentUser: { id: 'user-1' }, ip: '127.0.0.1', ...overrides, }; } function createDependencies(overrides = {}) { const calls = { data: [], errors: [], routeErrors: [], sync: [], enabled: [], }; return { calls, dependencies: { getMindSpacePages: () => ({}), getMindSpacePublications: () => null, getMindSpace: () => ({ getQuota: async () => ({ used: 1 }) }), getMindSpaceAudit: () => null, getMindSpacePageLiveEdit: () => ({}), getMindSpacePageEditSession: () => ({}), getUserAuth: () => ({ canUseChat: async () => ({ ok: true }) }), async syncUserGeneratedPages(userId) { calls.sync.push(userId); }, ownsAgentSession: async () => true, ensureMindSpaceEnabled(res, req) { calls.enabled.push({ res, req }); return true; }, 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 } }); }, handleMindSpaceError(res, req, error) { calls.routeErrors.push({ res, req, error }); return res.status(418).json({ mapped: error.message }); }, ...overrides, }, }; } test('page core module preserves route inventory and order', () => { const api = createRouterRecorder(); attachPortalMindSpacePageCoreRoutes( api, createDependencies().dependencies, ); assert.deepEqual([...api.routes.keys()], [ 'POST /mindspace/v1/pages', 'GET /mindspace/v1/pages', 'GET /mindspace/v1/pages/:pageId', 'DELETE /mindspace/v1/pages/:pageId', 'GET /mindspace/v1/pages/:pageId/delete-preview', 'PUT /mindspace/v1/pages/:pageId', 'POST /mindspace/v1/pages/:pageId/live-edit/bind', 'POST /mindspace/v1/pages/:pageId/live-edit/fork-session', 'POST /mindspace/v1/pages/:pageId/live-edit/close-session', 'GET /mindspace/v1/pages/:pageId/live-edit/revision', ]); }); test('create and list preserve payload mapping, sync, and pagination envelope', async () => { const received = []; const api = createRouterRecorder(); const setup = createDependencies({ getMindSpacePages: () => ({ async createPage(userId, input) { received.push({ kind: 'create', userId, input }); return { id: 'page-1' }; }, async listPages(userId, input) { received.push({ kind: 'list', userId, input }); return { items: [{ id: 'page-1' }], total: 1, limit: 20, offset: 4, hasMore: false, }; }, }), }); attachPortalMindSpacePageCoreRoutes(api, setup.dependencies); const createRes = createResponseRecorder(); await api.routes.get('POST /mindspace/v1/pages')( createRequest({ body: { title: 'Title', summary: 'Summary', content: '

Page

', content_format: 'html', template_id: 'template-1', page_type: 'html', category_code: 'draft', }, }), createRes, ); assert.equal(createRes.statusCode, 201); assert.equal(received[0].input.contentFormat, 'html'); const listRes = createResponseRecorder(); await api.routes.get('GET /mindspace/v1/pages')( createRequest({ query: { status: 'active', category_code: 'draft', limit: '20', offset: '4', }, }), listRes, ); assert.deepEqual(setup.calls.sync, ['user-1']); assert.deepEqual(received[1].input, { status: 'active', categoryCode: 'draft', limit: 20, offset: 4, }); assert.deepEqual(listRes.body.page, { total: 1, limit: 20, offset: 4, has_more: false, next_cursor: null, }); }); test('list pages responds without waiting for background workspace sync', async () => { let syncStarted = 0; let resolveSync; const received = []; const api = createRouterRecorder(); const setup = createDependencies({ getMindSpacePages: () => ({ async listPages(userId, input) { received.push({ userId, input }); return { items: [{ id: 'page-1' }], total: 1, limit: 6, offset: 0, hasMore: false, }; }, }), syncUserGeneratedPages() { syncStarted += 1; return new Promise((resolve) => { resolveSync = resolve; }); }, }); attachPortalMindSpacePageCoreRoutes(api, setup.dependencies); const listRes = createResponseRecorder(); await api.routes.get('GET /mindspace/v1/pages')( createRequest({ query: { limit: '6', offset: '0' } }), listRes, ); assert.equal(syncStarted, 1); assert.equal(received.length, 1); assert.deepEqual(listRes.body.data, [{ id: 'page-1' }]); resolveSync?.({ created: 0, updated: 0, skipped: 0 }); }); test('page list background sync coalesces in-flight and throttled requests', async () => { let currentTime = 1_000; const calls = []; const resolvers = []; const scheduler = createMindSpacePageListSyncScheduler({ now: () => currentTime, minIntervalMs: 30_000, syncUserGeneratedPages(userId) { calls.push(userId); return new Promise((resolve) => { resolvers.push(resolve); }); }, }); const first = scheduler('user-1'); const second = scheduler('user-1'); assert.equal(first, second); await Promise.resolve(); assert.deepEqual(calls, ['user-1']); resolvers.shift()?.({ created: 0, updated: 0, skipped: 0 }); await first; currentTime += 1_000; assert.equal(scheduler('user-1'), null); assert.deepEqual(calls, ['user-1']); currentTime += 30_000; const third = scheduler('user-1'); await Promise.resolve(); assert.deepEqual(calls, ['user-1', 'user-1']); resolvers.shift()?.({ created: 0, updated: 0, skipped: 0 }); await third; }); test('get and update preserve parallel lookup and version mapping', async () => { const api = createRouterRecorder(); const setup = createDependencies({ getMindSpacePages: () => ({ getPage: async () => ({ id: 'page-1', title: 'Page' }), listVersions: async () => [{ id: 'version-1' }], async updatePage(userId, pageId, input) { assert.equal(userId, 'user-1'); assert.equal(pageId, 'page-1'); assert.deepEqual(input, { expectedVersion: 2, title: 'Updated', summary: undefined, content: 'Body', templateId: undefined, pageType: undefined, changeNote: 'edit', }); return { id: pageId, versionNo: 3 }; }, }), getMindSpacePublications: () => ({ getCurrent: async () => ({ id: 'publication-1' }), }), }); attachPortalMindSpacePageCoreRoutes(api, setup.dependencies); const getRes = createResponseRecorder(); await api.routes.get('GET /mindspace/v1/pages/:pageId')( createRequest(), getRes, ); assert.equal(getRes.body.data.publication.id, 'publication-1'); assert.equal(getRes.body.data.versions[0].id, 'version-1'); const updateRes = createResponseRecorder(); await api.routes.get('PUT /mindspace/v1/pages/:pageId')( createRequest({ body: { expected_version: 2, title: 'Updated', content: 'Body', change_note: 'edit', }, }), updateRes, ); assert.equal(updateRes.body.data.versionNo, 3); }); test('delete and delete-preview preserve cleanup, quota, audit, and feature gate', async () => { const audit = []; const api = createRouterRecorder(); const setup = createDependencies({ getMindSpacePages: () => ({ async deletePage(userId, pageId, options) { assert.equal(userId, 'user-1'); assert.equal(pageId, 'page-1'); assert.deepEqual(options, { removeFromPlaza: true }); return { offlinedPublicationCount: 1, hiddenPlazaPostCount: 2, deletedAssetCount: 3, freedBytes: 4, }; }, async getDeletePreview() { return { pageId: 'page-1', assetCount: 3 }; }, }), getMindSpaceAudit: () => ({ async write(entry) { audit.push(entry); }, }), }); attachPortalMindSpacePageCoreRoutes(api, setup.dependencies); const deleteRes = createResponseRecorder(); await api.routes.get('DELETE /mindspace/v1/pages/:pageId')( createRequest({ query: { remove_from_plaza: '1' } }), deleteRes, ); assert.equal(deleteRes.body.data.quota.used, 1); assert.equal(audit[0].action, 'page.delete'); assert.deepEqual(audit[0].detail, { offlinedPublicationCount: 1, hiddenPlazaPostCount: 2, deletedAssetCount: 3, freedBytes: 4, }); const previewRes = createResponseRecorder(); await api.routes.get( 'GET /mindspace/v1/pages/:pageId/delete-preview', )(createRequest(), previewRes); assert.equal(previewRes.body.data.assetCount, 3); assert.equal(setup.calls.enabled.length, 2); }); test('live edit bind preserves validation, ownership, page check, and binding', async () => { const calls = []; const api = createRouterRecorder(); const setup = createDependencies({ getMindSpacePages: () => ({ async getPage(userId, pageId) { calls.push({ kind: 'page', userId, pageId }); }, }), ownsAgentSession: async (userId, sessionId) => { calls.push({ kind: 'owns', userId, sessionId }); return true; }, getMindSpacePageLiveEdit: () => ({ bindSession(input) { calls.push({ kind: 'bind', input }); return { revision: 1 }; }, }), }); attachPortalMindSpacePageCoreRoutes(api, setup.dependencies); const res = createResponseRecorder(); await api.routes.get( 'POST /mindspace/v1/pages/:pageId/live-edit/bind', )( createRequest({ body: { session_id: 'session-1', parent_session_id: 'parent-1', }, }), res, ); assert.deepEqual(calls.map((call) => call.kind), [ 'owns', 'page', 'bind', ]); assert.deepEqual(calls[2].input, { userId: 'user-1', sessionId: 'session-1', pageId: 'page-1', parentSessionId: 'parent-1', }); const deniedApi = createRouterRecorder(); const denied = createDependencies({ getMindSpacePageLiveEdit: () => ({}), ownsAgentSession: async () => false, }); attachPortalMindSpacePageCoreRoutes( deniedApi, denied.dependencies, ); const deniedRes = createResponseRecorder(); await deniedApi.routes.get( 'POST /mindspace/v1/pages/:pageId/live-edit/bind', )( createRequest({ body: { session_id: 'session-2' } }), deniedRes, ); assert.equal(deniedRes.statusCode, 403); assert.equal(denied.calls.errors[0].code, 'forbidden'); }); test('live edit fork, close, and revision preserve service contracts', async () => { const calls = []; const api = createRouterRecorder(); const setup = createDependencies({ getUserAuth: () => ({ async canUseChat(userId) { calls.push({ kind: 'gate', userId }); return { ok: true }; }, }), getMindSpacePageEditSession: () => ({ async forkSession(input) { calls.push({ kind: 'fork', input }); return { sessionId: 'fork-1' }; }, async closeSession(input) { calls.push({ kind: 'close', input }); return { closed: true }; }, }), getMindSpacePageLiveEdit: () => ({ async getRevisionSnapshot(userId, pageId) { calls.push({ kind: 'revision', userId, pageId }); return { revision: 7 }; }, }), }); attachPortalMindSpacePageCoreRoutes(api, setup.dependencies); await api.routes.get( 'POST /mindspace/v1/pages/:pageId/live-edit/fork-session', )( createRequest({ body: { parent_session_id: 'parent-1', h5_api_base: ' https://api.example ', }, }), createResponseRecorder(), ); assert.equal(calls[1].input.h5ApiBase, 'https://api.example'); await api.routes.get( 'POST /mindspace/v1/pages/:pageId/live-edit/close-session', )( createRequest({ body: { session_id: 'session-1', parent_session_id: 'parent-1', summary: 'done', }, }), createResponseRecorder(), ); assert.equal(calls[2].input.summary, 'done'); const revisionRes = createResponseRecorder(); await api.routes.get( 'GET /mindspace/v1/pages/:pageId/live-edit/revision', )(createRequest(), revisionRes); assert.equal(revisionRes.body.data.revision, 7); }); test('page core routes preserve unavailable and mapped error responses', async () => { const unavailableApi = createRouterRecorder(); const unavailable = createDependencies({ getMindSpacePages: () => null, }); attachPortalMindSpacePageCoreRoutes( unavailableApi, unavailable.dependencies, ); const unavailableRes = createResponseRecorder(); await unavailableApi.routes.get('GET /mindspace/v1/pages')( createRequest(), unavailableRes, ); assert.equal(unavailableRes.statusCode, 503); const failureApi = createRouterRecorder(); const failure = createDependencies({ getMindSpacePages: () => ({ async listPages() { throw new Error('list failed'); }, }), }); attachPortalMindSpacePageCoreRoutes( failureApi, failure.dependencies, ); const failureRes = createResponseRecorder(); await failureApi.routes.get('GET /mindspace/v1/pages')( createRequest(), failureRes, ); assert.equal(failureRes.statusCode, 418); assert.equal(failure.calls.routeErrors[0].error.message, 'list failed'); });