e2ad3bf62b
- 新增 public share widget 与 workspace relative path 解析 - 增强 publications/chat-plaza 发布链路与缩略图 demo - 补充 schema、cover 检查脚本与回归测试 Co-authored-by: Cursor <cursoragent@cursor.com>
250 lines
8.8 KiB
JavaScript
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');
|
|
});
|