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'); });