Files
memind/server/portal-mindspace-asset-delivery-routes.test.mjs
T

381 lines
11 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalMindSpaceAssetDeliveryRoutes } from './portal-mindspace-asset-delivery-routes.mjs';
function createRouterRecorder() {
const routes = new Map();
const register = (method) => (path, ...handlers) => {
routes.set(`${method} ${path}`, handlers);
};
return {
routes,
get: register('GET'),
post: register('POST'),
put: register('PUT'),
delete: register('DELETE'),
};
}
function createResponseRecorder() {
return {
statusCode: 200,
body: undefined,
headers: {},
headersSent: false,
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
this.headersSent = true;
return this;
},
set(name, value) {
this.headers[name] = value;
return this;
},
setHeader(name, value) {
this.headers[name] = value;
},
send(body) {
this.body = body;
this.headersSent = true;
return this;
},
};
}
function createRequest(overrides = {}) {
return {
body: {},
query: {},
params: {},
currentUser: { id: 'user-1' },
requestId: 'request-1',
...overrides,
};
}
function createDependencies(overrides = {}) {
const calls = {
data: [],
errors: [],
};
return {
calls,
dependencies: {
ensureMindSpaceEnabled: () => true,
sendData(res, req, data, status = 200) {
calls.data.push({ res, req, data, status });
return res.status(status).json({ data });
},
handleMindSpaceError(res, req, error) {
calls.errors.push({ res, req, error });
return res.status(418).json({ mapped: error.message });
},
...overrides,
},
};
}
function routeHandler(api, key, index = 0) {
return api.routes.get(key)[index];
}
test('asset delivery module preserves route inventory and upload middleware order', () => {
const api = createRouterRecorder();
const rawUploadBody = () => {};
attachPortalMindSpaceAssetDeliveryRoutes(api, { rawUploadBody });
assert.deepEqual([...api.routes.keys()], [
'GET /mindspace/v1/conversation-packages/:sessionId',
'GET /mindspace/v1/conversation-packages/:sessionId/manifest.json',
'GET /mindspace/v1/conversation-packages/:sessionId/artifacts/:artifactId/download',
'POST /mindspace/v1/uploads',
'PUT /mindspace/v1/uploads/:uploadId/content',
'POST /mindspace/v1/uploads/:uploadId/complete',
'POST /mindspace/v1/conversation-packages/:sessionId/claim-uploads',
'DELETE /mindspace/v1/uploads/:uploadId',
'GET /mindspace/v1/assets',
]);
const contentHandlers = api.routes.get(
'PUT /mindspace/v1/uploads/:uploadId/content',
);
assert.equal(contentHandlers.length, 2);
assert.equal(contentHandlers[0], rawUploadBody);
});
test('conversation package routes preserve ownership, prepare, manifest, and artifact downloads', async () => {
const prepareCalls = [];
const manifest = {
title: 'Conversation One',
displayFolderName: 'Conversation One',
};
const artifactBody = Buffer.from('artifact-data');
const registry = {
async readManifestForSession(input) {
assert.deepEqual(input, { userId: 'user-1', sessionId: 'session-1' });
return manifest;
},
async readArtifactObject(input) {
assert.deepEqual(input, {
userId: 'user-1',
sessionId: 'session-1',
artifactId: 'artifact-1',
});
return {
artifact: {
id: 'artifact-1',
displayName: 'report.txt',
mimeType: 'text/plain',
},
body: artifactBody,
};
},
};
const api = createRouterRecorder();
const deps = createDependencies({
getConversationPackageRegistry: () => registry,
getSessionAccess: () => ({
validateOwnership: async (userId, sessionId) =>
userId === 'user-1' && sessionId === 'session-1',
}),
beforeReadManifest: async (input) => {
prepareCalls.push(input);
},
});
attachPortalMindSpaceAssetDeliveryRoutes(api, deps.dependencies);
const packageReq = createRequest({ params: { sessionId: 'session-1' } });
const packageRes = createResponseRecorder();
await routeHandler(
api,
'GET /mindspace/v1/conversation-packages/:sessionId',
)(packageReq, packageRes);
assert.equal(deps.calls.data[0].data, manifest);
const manifestReq = createRequest({ params: { sessionId: 'session-1' } });
const manifestRes = createResponseRecorder();
await routeHandler(
api,
'GET /mindspace/v1/conversation-packages/:sessionId/manifest.json',
)(manifestReq, manifestRes);
assert.equal(manifestRes.headers['Content-Type'], 'application/json; charset=utf-8');
assert.match(manifestRes.headers['Content-Disposition'], /Conversation%20One-manifest\.json/);
assert.match(manifestRes.body, /"title": "Conversation One"/);
const artifactReq = createRequest({
params: { sessionId: 'session-1', artifactId: 'artifact-1' },
});
const artifactRes = createResponseRecorder();
await routeHandler(
api,
'GET /mindspace/v1/conversation-packages/:sessionId/artifacts/:artifactId/download',
)(artifactReq, artifactRes);
assert.equal(artifactRes.headers['Content-Type'], 'text/plain');
assert.equal(artifactRes.body, artifactBody);
assert.equal(prepareCalls.length, 2);
});
test('create upload preserves feature gate, payload mapping, 201, and error routing', async () => {
let received = null;
const api = createRouterRecorder();
const deps = createDependencies({
getMindSpaceAssets: () => ({
async createUpload(userId, input) {
received = { userId, input };
return { id: 'upload-1' };
},
}),
});
attachPortalMindSpaceAssetDeliveryRoutes(api, deps.dependencies);
const req = createRequest({
body: {
category_id: 'category-1',
filename: 'report.pdf',
size_bytes: 42,
declared_mime_type: 'application/pdf',
session_id: 'session-1',
message_id: 'message-1',
},
});
const res = createResponseRecorder();
await routeHandler(api, 'POST /mindspace/v1/uploads')(req, res);
assert.deepEqual(received, {
userId: 'user-1',
input: {
categoryId: 'category-1',
filename: 'report.pdf',
sizeBytes: 42,
declaredMimeType: 'application/pdf',
sourceSessionId: 'session-1',
sourceMessageId: 'message-1',
},
});
assert.equal(deps.calls.data[0].status, 201);
const failureApi = createRouterRecorder();
const error = new Error('upload rejected');
const failure = createDependencies({
getMindSpaceAssets: () => ({
async createUpload() {
throw error;
},
}),
});
attachPortalMindSpaceAssetDeliveryRoutes(failureApi, failure.dependencies);
const failureReq = createRequest();
const failureRes = createResponseRecorder();
await routeHandler(failureApi, 'POST /mindspace/v1/uploads')(
failureReq,
failureRes,
);
assert.deepEqual(failure.calls.errors[0], {
res: failureRes,
req: failureReq,
error,
});
});
test('upload content preserves raw body forwarding and unavailable response', async () => {
const rawBody = Buffer.from('raw-upload');
let received = null;
const api = createRouterRecorder();
const deps = createDependencies({
rawUploadBody: () => {},
getMindSpaceAssets: () => ({
async writeUploadContent(...args) {
received = args;
return { storedBytes: rawBody.length };
},
}),
});
attachPortalMindSpaceAssetDeliveryRoutes(api, deps.dependencies);
const res = createResponseRecorder();
await routeHandler(
api,
'PUT /mindspace/v1/uploads/:uploadId/content',
1,
)(
createRequest({ params: { uploadId: 'upload-1' }, body: rawBody }),
res,
);
assert.deepEqual(received, ['user-1', 'upload-1', rawBody]);
assert.deepEqual(res.body, { data: { storedBytes: rawBody.length } });
const unavailableApi = createRouterRecorder();
attachPortalMindSpaceAssetDeliveryRoutes(unavailableApi);
const unavailableRes = createResponseRecorder();
await routeHandler(
unavailableApi,
'PUT /mindspace/v1/uploads/:uploadId/content',
1,
)(createRequest(), unavailableRes);
assert.equal(unavailableRes.statusCode, 503);
assert.deepEqual(unavailableRes.body, { message: 'MindSpace 未启用' });
});
test('complete and cancel upload preserve service forwarding and response status', async () => {
const calls = [];
const api = createRouterRecorder();
const deps = createDependencies({
getMindSpaceAssets: () => ({
async completeUpload(...args) {
calls.push({ kind: 'complete', args });
return { id: 'asset-1' };
},
async cancelUpload(...args) {
calls.push({ kind: 'cancel', args });
return { cancelled: true };
},
}),
});
attachPortalMindSpaceAssetDeliveryRoutes(api, deps.dependencies);
const completeRes = createResponseRecorder();
await routeHandler(api, 'POST /mindspace/v1/uploads/:uploadId/complete')(
createRequest({ params: { uploadId: 'upload-1' } }),
completeRes,
);
assert.equal(completeRes.statusCode, 201);
assert.deepEqual(completeRes.body, { data: { id: 'asset-1' } });
const cancelRes = createResponseRecorder();
await routeHandler(api, 'DELETE /mindspace/v1/uploads/:uploadId')(
createRequest({ params: { uploadId: 'upload-2' } }),
cancelRes,
);
assert.deepEqual(cancelRes.body, { data: { cancelled: true } });
assert.deepEqual(calls, [
{ kind: 'complete', args: ['user-1', 'upload-1'] },
{ kind: 'cancel', args: ['user-1', 'upload-2'] },
]);
});
test('claim uploads preserves session/message mapping and feature gate options', async () => {
let received = null;
let enabledOptions = null;
const api = createRouterRecorder();
const deps = createDependencies({
ensureMindSpaceEnabled: (_res, _req, options) => {
enabledOptions = options;
return true;
},
getMindSpaceAssets: () => ({
async claimUploadArtifactsForConversation(userId, input) {
received = { userId, input };
return { claimed: 2 };
},
}),
});
attachPortalMindSpaceAssetDeliveryRoutes(api, deps.dependencies);
const res = createResponseRecorder();
await routeHandler(
api,
'POST /mindspace/v1/conversation-packages/:sessionId/claim-uploads',
)(
createRequest({
params: { sessionId: 'session-1' },
body: { message_id: 'message-1' },
}),
res,
);
assert.deepEqual(enabledOptions, { upload: true });
assert.deepEqual(received, {
userId: 'user-1',
input: { sessionId: 'session-1', messageId: 'message-1' },
});
assert.equal(deps.calls.data[0].data.claimed, 2);
});
test('asset list preserves string query filtering and stable page envelope', async () => {
let received = null;
const assets = [{ id: 'asset-1' }];
const api = createRouterRecorder();
const deps = createDependencies({
getMindSpaceAssets: () => ({
async listAssets(userId, query) {
received = { userId, query };
return assets;
},
}),
});
attachPortalMindSpaceAssetDeliveryRoutes(api, deps.dependencies);
const res = createResponseRecorder();
await routeHandler(api, 'GET /mindspace/v1/assets')(
createRequest({
query: { category_id: 'category-1', category_code: ['ignored'] },
}),
res,
);
assert.deepEqual(received, {
userId: 'user-1',
query: { categoryId: 'category-1', categoryCode: undefined },
});
assert.deepEqual(res.body, {
data: assets,
page: { next_cursor: null, has_more: false },
});
});