Files
memind/mindspace-service/mindspace-rpc-server.test.mjs
john e2ad3bf62b feat(mindspace): 公开页分享组件、workspace 路径与 Plaza 发布增强
- 新增 public share widget 与 workspace relative path 解析
- 增强 publications/chat-plaza 发布链路与缩略图 demo
- 补充 schema、cover 检查脚本与回归测试

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-05 23:14:41 +08:00

250 lines
8.8 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { createMindSpaceRpcRequestHandler } from './mindspace-rpc-server.mjs';
function createStubAdapter() {
const calls = {
writeUploadContent: [],
};
return {
calls,
kind: 'local',
implementationStatus: 'ready',
serviceFacade: {
async prepareConversationPackageRead() {
return [];
},
},
conversationPackageRegistry: {
async ensurePackage() { return { ok: true }; },
async putObjectForSession() { return { ok: true }; },
async recordArtifact() { return { ok: true }; },
async readArtifactObject() { return { ok: true }; },
async readManifestForSession() { return { ok: true }; },
async writeManifestForSession() { return { ok: true }; },
},
assetService: {
async cancelUpload() { return { ok: true }; },
async claimUploadArtifactsForConversation() { return []; },
async completeUpload() { return { ok: true }; },
async createChatAsset() { return { ok: true }; },
async createUpload() { return { ok: true }; },
async deleteAsset() { return { ok: true }; },
async listAssets() { return []; },
async readAsset() { return { ok: true }; },
async readPublicAsset() { return { ok: true }; },
async renderAssetPreview() { return { preview: true }; },
async renderAssetThumbnail() { return { thumbnail: true }; },
async syncWorkspaceAssets() { return { synced: true }; },
async writeUploadContent(...args) {
calls.writeUploadContent.push(args);
return { isBuffer: Buffer.isBuffer(args[2]), size: args[2]?.length ?? 0 };
},
async expireStaleUploads() { return { expired: 0 }; },
},
pageService: {
async createFromChat() { return { ok: true }; },
async createPage() { return { ok: true }; },
async deletePage() { return { ok: true }; },
async findPageByRelativePath() { return null; },
async findPlazaContextByWorkspacePath() { return null; },
async findPageBySourceAsset() { return null; },
async getDeletePreview() { return { ok: true }; },
async getPage() { return { ok: true }; },
async listPages() { return []; },
async listVersions() { return []; },
async localizePrivateResources() { return { ok: true }; },
async redactPage() { return { ok: true }; },
async regenerateThumbnail() { return { ok: true }; },
async renderDraftPreview() { return { html: '' }; },
async renderPreview() { return { html: '' }; },
async renderThumbnail() { return { ok: true }; },
async rewriteHtmlDownloadLinksForPage() { return { ok: true }; },
async updatePage() { return { ok: true }; },
async uploadThumbnail() { return { ok: true }; },
},
pageLiveEditService: {
async applyAgentPatch() { return { ok: true }; },
async bindSession() { return { ok: true }; },
async getRevisionSnapshot() { return { ok: true }; },
},
assetAgentService: {
async applyAgentDelete() { return { ok: true }; },
},
publicationService: {
async check() { return { ok: true }; },
async cleanupExpiredUnconfirmedPublications() { return { cleaned: 0 }; },
async getCurrent() { return null; },
async refreshOnlinePublicationHtml() { return null; },
async getPublicHomepage() { return null; },
async getStats() { return { ok: true }; },
async offline() { return { ok: true }; },
async publish() { return { ok: true }; },
async resolvePrivateLink() { return { ok: true }; },
async resolvePublic() { return { ok: true }; },
async updatePublicationStatus() { return { ok: true }; },
},
cleanupService: {
async listCandidates() { return []; },
async runCleanup() { return { ok: true }; },
},
agentJobService: {
async cancelJob() { return { ok: true }; },
async claimJob() { return { ok: true }; },
async claimNextJob() { return null; },
async completeJob() { return { ok: true }; },
async createJob() { return { ok: true }; },
async getAssetForJob() { return { ok: true }; },
async getJob() { return { ok: true }; },
async heartbeat() { return { ok: true }; },
async listJobs() { return []; },
async reapStaleJobs() { return 0; },
async retryJob() { return { ok: true }; },
},
};
}
async function runRequest(handler, { method = 'GET', path = '/', headers = {}, body = '' } = {}) {
const chunks = [];
const req = {
method,
url: path,
headers,
async *[Symbol.asyncIterator]() {
if (body) yield Buffer.from(body);
},
};
const res = {
statusCode: 0,
headers: {},
writeHead(statusCode, responseHeaders) {
this.statusCode = statusCode;
this.headers = responseHeaders;
},
end(chunk) {
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
},
};
await handler(req, res);
return {
statusCode: res.statusCode,
headers: res.headers,
body: chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : null,
};
}
test('health and contract endpoints are exposed without auth', async () => {
const handler = await createMindSpaceRpcRequestHandler({
adapter: createStubAdapter(),
env: {
MINDSPACE_MEMIND_ROOT: '..',
},
});
const health = await runRequest(handler, { path: '/health' });
const contract = await runRequest(handler, { path: '/mindspace/v1/contract' });
assert.equal(health.statusCode, 200);
assert.equal(health.body.ok, true);
assert.equal(contract.statusCode, 200);
assert.ok(contract.body.bindings.assetService.includes('renderAssetPreview'));
});
test('rpc invocation enforces auth token and dispatches args to adapter method', async () => {
const handler = await createMindSpaceRpcRequestHandler({
adapter: createStubAdapter(),
env: {
MINDSPACE_MEMIND_ROOT: '..',
MINDSPACE_REMOTE_AUTH_TOKEN: 'secret-token',
},
});
const unauthorized = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/adapter/assetService/renderAssetPreview',
body: JSON.stringify({ args: [{ assetId: 'a1' }] }),
});
const authorized = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/adapter/assetService/renderAssetPreview',
headers: {
authorization: 'Bearer secret-token',
},
body: JSON.stringify({ args: [{ assetId: 'a1' }] }),
});
assert.equal(unauthorized.statusCode, 401);
assert.equal(authorized.statusCode, 200);
assert.deepEqual(authorized.body, { preview: true });
});
test('rpc invocation revives JSON-serialized buffers before dispatch', async () => {
const adapter = createStubAdapter();
const handler = await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
},
});
const payload = Buffer.from('asset parity payload');
const response = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/adapter/assetService/writeUploadContent',
body: JSON.stringify({ args: ['user-1', 'upload-1', payload] }),
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response.body, { isBuffer: true, size: payload.length });
assert.equal(Buffer.isBuffer(adapter.calls.writeUploadContent[0][2]), true);
});
test('rpc maps publication_not_found to 404 with error code', async () => {
const adapter = createStubAdapter();
adapter.publicationService.resolvePublic = async () => {
const error = new Error('公开页面不存在');
error.code = 'publication_not_found';
throw error;
};
const handler = await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
},
});
const response = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/adapter/publicationService/resolvePublic',
body: JSON.stringify({ args: ['john', 'missing-page', null, null, {}] }),
});
assert.equal(response.statusCode, 404);
assert.equal(response.body.code, 'publication_not_found');
assert.match(response.body.message, /公开页面不存在/);
});
test('rpc maps publication_login_required to 401 with error code', async () => {
const adapter = createStubAdapter();
adapter.publicationService.resolvePublic = async () => {
const error = new Error('登录后才能访问此页面');
error.code = 'publication_login_required';
throw error;
};
const handler = await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
},
});
const response = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/adapter/publicationService/resolvePublic',
body: JSON.stringify({ args: ['john', 'login-page', null, null, {}] }),
});
assert.equal(response.statusCode, 401);
assert.equal(response.body.code, 'publication_login_required');
});