381 lines
11 KiB
JavaScript
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 },
|
|
});
|
|
});
|