08feae8bef
落地 H5 Session 架构 Patch 1–5(Broker 收口、Router decision、SSE taxonomy、goosed 边界检查), 并新增可选 MEMIND_RUN_STREAM_REPLAY run 事件回放与 H5 假交付 guard;修复 Finish 先于 agent-run gate 导致 UI 永久 loading 的竞态,接入 verify:h5-session-patches 回归脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
293 lines
8.6 KiB
JavaScript
293 lines
8.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
createDownloadConversationPackageArtifactHandler,
|
|
createDownloadConversationPackageManifestHandler,
|
|
createGetConversationPackageHandler,
|
|
} from './mindspace-conversation-package-routes.mjs';
|
|
|
|
function createResponseRecorder() {
|
|
return {
|
|
statusCode: 200,
|
|
headersSent: false,
|
|
body: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(payload) {
|
|
this.body = payload;
|
|
this.headersSent = true;
|
|
return this;
|
|
},
|
|
headers: {},
|
|
set(key, value) {
|
|
this.headers[key.toLowerCase()] = value;
|
|
return this;
|
|
},
|
|
setHeader(key, value) {
|
|
this.headers[key.toLowerCase()] = value;
|
|
return this;
|
|
},
|
|
send(payload) {
|
|
this.body = payload;
|
|
this.headersSent = true;
|
|
return this;
|
|
},
|
|
};
|
|
}
|
|
|
|
test('GET conversation package returns manifest for owned sessions', async () => {
|
|
const hookCalls = [];
|
|
const handler = createGetConversationPackageHandler({
|
|
registry: {
|
|
async readManifestForSession(input) {
|
|
assert.deepEqual(input, { userId: 'user-1', sessionId: 'session-1' });
|
|
return { packageId: 'cp_session-1', artifacts: [{ artifactId: 'ca_1', kind: 'public_html' }] };
|
|
},
|
|
},
|
|
userAuth: {
|
|
async ownsSession(userId, sessionId) {
|
|
assert.equal(userId, 'user-1');
|
|
assert.equal(sessionId, 'session-1');
|
|
return true;
|
|
},
|
|
},
|
|
beforeReadManifest(input) {
|
|
hookCalls.push({
|
|
userId: input.userId,
|
|
sessionId: input.sessionId,
|
|
req: input.req,
|
|
});
|
|
},
|
|
});
|
|
const req = { currentUser: { id: 'user-1' }, params: { sessionId: 'session-1' } };
|
|
const res = createResponseRecorder();
|
|
|
|
await handler(req, res);
|
|
|
|
assert.equal(res.statusCode, 200);
|
|
assert.deepEqual(res.body, {
|
|
data: { packageId: 'cp_session-1', artifacts: [{ artifactId: 'ca_1', kind: 'public_html' }] },
|
|
});
|
|
assert.deepEqual(hookCalls, [{ userId: 'user-1', sessionId: 'session-1', req }]);
|
|
});
|
|
|
|
test('GET conversation package blocks sessions owned by another user', async () => {
|
|
const handler = createGetConversationPackageHandler({
|
|
registry: {
|
|
async readManifestForSession() {
|
|
assert.fail('registry should not be called for unauthorized sessions');
|
|
},
|
|
},
|
|
userAuth: {
|
|
async ownsSession() {
|
|
return false;
|
|
},
|
|
},
|
|
beforeReadManifest() {
|
|
assert.fail('beforeReadManifest should not run for unauthorized sessions');
|
|
},
|
|
});
|
|
const res = createResponseRecorder();
|
|
|
|
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-2' } }, res);
|
|
|
|
assert.equal(res.statusCode, 403);
|
|
assert.deepEqual(res.body, { message: '无权访问该会话' });
|
|
});
|
|
|
|
test('GET conversation package returns 404 when package has not been recorded', async () => {
|
|
const handler = createGetConversationPackageHandler({
|
|
registry: {
|
|
async readManifestForSession() {
|
|
return null;
|
|
},
|
|
},
|
|
userAuth: {
|
|
async ownsSession() {
|
|
return true;
|
|
},
|
|
},
|
|
});
|
|
const res = createResponseRecorder();
|
|
|
|
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-empty' } }, res);
|
|
|
|
assert.equal(res.statusCode, 404);
|
|
assert.deepEqual(res.body, { message: '对话包不存在' });
|
|
});
|
|
|
|
test('GET conversation package respects MindSpace feature gates', async () => {
|
|
const handler = createGetConversationPackageHandler({
|
|
registry: {},
|
|
userAuth: {
|
|
async ownsSession() {
|
|
assert.fail('user ownership should not be checked when feature gate rejects');
|
|
},
|
|
},
|
|
ensureMindSpaceEnabled(res) {
|
|
res.status(403).json({ message: 'MindSpace 已关闭' });
|
|
return false;
|
|
},
|
|
});
|
|
const res = createResponseRecorder();
|
|
|
|
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-1' } }, res);
|
|
|
|
assert.equal(res.statusCode, 403);
|
|
assert.deepEqual(res.body, { message: 'MindSpace 已关闭' });
|
|
});
|
|
|
|
test('GET conversation package manifest downloads owned package metadata', async () => {
|
|
const handler = createDownloadConversationPackageManifestHandler({
|
|
registry: {
|
|
async readManifestForSession(input) {
|
|
assert.deepEqual(input, { userId: 'user-1', sessionId: 'session-1' });
|
|
return {
|
|
packageId: 'cp_session-1',
|
|
sessionId: 'session-1',
|
|
displayFolderName: '旅行计划',
|
|
artifacts: [],
|
|
};
|
|
},
|
|
},
|
|
userAuth: {
|
|
async ownsSession(userId, sessionId) {
|
|
assert.equal(userId, 'user-1');
|
|
assert.equal(sessionId, 'session-1');
|
|
return true;
|
|
},
|
|
},
|
|
});
|
|
const res = createResponseRecorder();
|
|
|
|
await handler(
|
|
{ currentUser: { id: 'user-1' }, params: { sessionId: 'session-1' }, requestId: 'req-1' },
|
|
res,
|
|
);
|
|
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.headers['content-type'], 'application/json; charset=utf-8');
|
|
assert.match(
|
|
res.headers['content-disposition'],
|
|
/%E6%97%85%E8%A1%8C%E8%AE%A1%E5%88%92-manifest\.json/,
|
|
);
|
|
assert.equal(res.headers['cache-control'], 'private, no-store');
|
|
assert.equal(res.headers['x-request-id'], 'req-1');
|
|
assert.deepEqual(JSON.parse(res.body), {
|
|
packageId: 'cp_session-1',
|
|
sessionId: 'session-1',
|
|
displayFolderName: '旅行计划',
|
|
artifacts: [],
|
|
});
|
|
});
|
|
|
|
test('GET conversation package manifest blocks unauthorized sessions', async () => {
|
|
const handler = createDownloadConversationPackageManifestHandler({
|
|
registry: {
|
|
async readManifestForSession() {
|
|
assert.fail('registry should not be called for unauthorized manifest downloads');
|
|
},
|
|
},
|
|
userAuth: {
|
|
async ownsSession() {
|
|
return false;
|
|
},
|
|
},
|
|
});
|
|
const res = createResponseRecorder();
|
|
|
|
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-2' } }, res);
|
|
|
|
assert.equal(res.statusCode, 403);
|
|
assert.deepEqual(res.body, { message: '无权访问该会话' });
|
|
});
|
|
|
|
test('GET conversation package artifact downloads owned private objects', async () => {
|
|
const handler = createDownloadConversationPackageArtifactHandler({
|
|
registry: {
|
|
async readArtifactObject(input) {
|
|
assert.deepEqual(input, { userId: 'user-1', sessionId: 'session-1', artifactId: 'ca_docx' });
|
|
return {
|
|
artifact: {
|
|
id: 'ca_docx',
|
|
displayName: '研究报告.docx',
|
|
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
},
|
|
body: Buffer.from('docx-body'),
|
|
};
|
|
},
|
|
},
|
|
userAuth: {
|
|
async ownsSession() {
|
|
return true;
|
|
},
|
|
},
|
|
});
|
|
const res = createResponseRecorder();
|
|
|
|
await handler(
|
|
{
|
|
currentUser: { id: 'user-1' },
|
|
params: { sessionId: 'session-1', artifactId: 'ca_docx' },
|
|
requestId: 'req-1',
|
|
},
|
|
res,
|
|
);
|
|
|
|
assert.equal(res.statusCode, 200);
|
|
assert.equal(res.body.toString('utf8'), 'docx-body');
|
|
assert.equal(
|
|
res.headers['content-type'],
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
);
|
|
assert.match(res.headers['content-disposition'], /filename\*=/);
|
|
assert.equal(res.headers['x-request-id'], 'req-1');
|
|
});
|
|
|
|
test('GET conversation package artifact blocks unauthorized sessions before storage access', async () => {
|
|
const handler = createDownloadConversationPackageArtifactHandler({
|
|
registry: {
|
|
async readArtifactObject() {
|
|
assert.fail('registry should not be called for unauthorized artifact downloads');
|
|
},
|
|
},
|
|
userAuth: {
|
|
async ownsSession() {
|
|
return false;
|
|
},
|
|
},
|
|
});
|
|
const res = createResponseRecorder();
|
|
|
|
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-2', artifactId: 'ca_docx' } }, res);
|
|
|
|
assert.equal(res.statusCode, 403);
|
|
assert.deepEqual(res.body, { message: '无权访问该会话' });
|
|
});
|
|
|
|
test('GET conversation package validates ownership through sessionAccess when provided', async () => {
|
|
const validated = [];
|
|
const handler = createGetConversationPackageHandler({
|
|
registry: {
|
|
async readManifestForSession() {
|
|
return { packageId: 'cp_session-broker', sessionId: 'session-broker', artifacts: [] };
|
|
},
|
|
},
|
|
userAuth: {},
|
|
sessionAccess: {
|
|
async validateOwnership(userId, sessionId) {
|
|
validated.push({ userId, sessionId });
|
|
return false;
|
|
},
|
|
},
|
|
});
|
|
const res = createResponseRecorder();
|
|
|
|
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-broker' } }, res);
|
|
|
|
assert.deepEqual(validated, [{ userId: 'user-1', sessionId: 'session-broker' }]);
|
|
assert.equal(res.statusCode, 403);
|
|
});
|