Files
memind/server/portal-mindspace-page-core-routes.test.mjs
T
john 264c213fbe fix(mindspace): use lightweight page sync for background list refresh
Background page list sync was calling syncAndDeliver over the remote adapter,
which could exceed the 60s timeout for heavy users and block Portal. Route list
sync through pageSyncService only via listSync while keeping full delivery on
Finish and agent-run paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 08:53:58 +08:00

557 lines
16 KiB
JavaScript

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/achievements',
'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: '<h1>Page</h1>',
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, options) {
calls.push({ userId, options });
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, [{ userId: 'user-1', options: { listSync: true } }]);
resolvers.shift()?.({ created: 0, updated: 0, skipped: 0 });
await first;
currentTime += 1_000;
assert.equal(scheduler('user-1'), null);
assert.deepEqual(calls, [{ userId: 'user-1', options: { listSync: true } }]);
currentTime += 30_000;
const third = scheduler('user-1');
await Promise.resolve();
assert.deepEqual(calls, [
{ userId: 'user-1', options: { listSync: true } },
{ userId: 'user-1', options: { listSync: true } },
]);
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');
});
test('achievements route uses listPages and enriches stats before page detail route', async () => {
const api = createRouterRecorder();
const setup = createDependencies({
getMindSpacePages: () => ({
async listPages(userId, input) {
return {
items: [{ id: 'page-1', title: 'Demo', viewCount: 3, clickCount: null, statsSource: 'publication' }],
total: 1,
limit: input.limit ?? 20,
offset: input.offset ?? 0,
hasMore: false,
};
},
}),
getAuthPool: () => null,
});
attachPortalMindSpacePageCoreRoutes(api, setup.dependencies);
const res = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/pages/achievements')(
createRequest({ query: { limit: '20', offset: '0' } }),
res,
);
assert.equal(res.statusCode, 200);
assert.equal(res.body.data[0].viewCount, 3);
assert.equal(res.body.page.total, 1);
});