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