Add MindSpace service runtime release chain
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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 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 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);
|
||||
});
|
||||
Reference in New Issue
Block a user