feat(h5-session): Session Broker、run SSE replay 与 Finish 竞态修复
落地 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>
This commit is contained in:
@@ -386,6 +386,70 @@ test('proxySessionEvents does not send JSON after SSE headers were sent', async
|
||||
}
|
||||
});
|
||||
|
||||
test('proxySessionEvents attaches session taxonomy when flag enabled', async () => {
|
||||
const previous = process.env.MEMIND_SSE_EVENT_TAXONOMY;
|
||||
process.env.MEMIND_SSE_EVENT_TAXONOMY = '1';
|
||||
let upstream;
|
||||
|
||||
try {
|
||||
upstream = createServer((req, res) => {
|
||||
if (req.method === 'GET' && req.url === '/sessions/session-1/events') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
||||
res.write('data: {"type":"Finish","reason":"stop"}\n\n');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ message: `unexpected ${req.method} ${req.url}` }));
|
||||
});
|
||||
const upstreamPort = await listen(upstream);
|
||||
const proxy = createTkmindProxy({
|
||||
apiTarget: `http://127.0.0.1:${upstreamPort}`,
|
||||
apiSecret: 'test-secret',
|
||||
userAuth: createMemoryTestUserAuth(process.cwd()),
|
||||
});
|
||||
|
||||
const req = new EventEmitter();
|
||||
req.currentUser = { id: 'user-1', username: 'john' };
|
||||
req.get = () => '';
|
||||
req.once = req.once.bind(req);
|
||||
req.off = req.off.bind(req);
|
||||
|
||||
const chunks = [];
|
||||
const res = new EventEmitter();
|
||||
res.headersSent = false;
|
||||
res.writableEnded = false;
|
||||
res.statusCode = 200;
|
||||
res.status = (code) => {
|
||||
res.statusCode = code;
|
||||
return res;
|
||||
};
|
||||
res.setHeader = () => {};
|
||||
res.flushHeaders = () => {
|
||||
res.headersSent = true;
|
||||
};
|
||||
res.write = (chunk) => {
|
||||
res.headersSent = true;
|
||||
chunks.push(String(chunk));
|
||||
return true;
|
||||
};
|
||||
res.end = () => {
|
||||
res.writableEnded = true;
|
||||
};
|
||||
res.json = () => {
|
||||
throw new Error('json must not be called after SSE started');
|
||||
};
|
||||
|
||||
await proxy.proxySessionEvents(req, res, 'session-1');
|
||||
|
||||
assert.match(chunks.join(''), /"taxonomy":"terminal"/);
|
||||
} finally {
|
||||
if (previous == null) delete process.env.MEMIND_SSE_EVENT_TAXONOMY;
|
||||
else process.env.MEMIND_SSE_EVENT_TAXONOMY = previous;
|
||||
await closeServer(upstream);
|
||||
}
|
||||
});
|
||||
|
||||
test('startSessionForUser resolves memories through Memory V2 facade', async () => {
|
||||
let resolveInput = null;
|
||||
await withFakeGoosedSession(async ({ apiTarget, workingDir, harnessEntries }) => {
|
||||
|
||||
Reference in New Issue
Block a user