Files
memind/sse-event-taxonomy.test.mjs
john 08feae8bef 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>
2026-07-06 14:19:48 +08:00

112 lines
3.5 KiB
JavaScript

import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import test from 'node:test';
import {
classifyRunStreamTaxonomy,
classifySessionEventTaxonomy,
finalizeSessionStreamEvent,
isSseEventTaxonomyEnabled,
SSE_EVENT_TAXONOMY,
wrapRunStreamPayload,
writeSseErrorAndEnd,
} from './sse-event-taxonomy.mjs';
test('classifySessionEventTaxonomy maps goosed session events', () => {
assert.equal(
classifySessionEventTaxonomy({ type: 'Message', message: { role: 'assistant' } }),
SSE_EVENT_TAXONOMY.DELTA,
);
assert.equal(
classifySessionEventTaxonomy({ type: 'UpdateConversation', conversation: [] }),
SSE_EVENT_TAXONOMY.CONTROL,
);
assert.equal(
classifySessionEventTaxonomy({ type: 'Finish', reason: 'stop' }),
SSE_EVENT_TAXONOMY.TERMINAL,
);
assert.equal(
classifySessionEventTaxonomy({ type: 'Error', error: 'boom' }),
SSE_EVENT_TAXONOMY.TERMINAL,
);
assert.equal(
classifySessionEventTaxonomy({ type: 'Ping' }),
SSE_EVENT_TAXONOMY.CONTROL,
);
});
test('classifyRunStreamTaxonomy separates control and terminal run states', () => {
assert.equal(
classifyRunStreamTaxonomy('run', { run: { status: 'running' } }),
SSE_EVENT_TAXONOMY.CONTROL,
);
assert.equal(
classifyRunStreamTaxonomy('run', { run: { status: 'succeeded' } }),
SSE_EVENT_TAXONOMY.TERMINAL,
);
assert.equal(
classifyRunStreamTaxonomy('error', { message: 'failed' }),
SSE_EVENT_TAXONOMY.TERMINAL,
);
});
test('finalizeSessionStreamEvent attaches taxonomy only when flag enabled', () => {
const event = { type: 'Finish', reason: 'stop' };
const previous = process.env.MEMIND_SSE_EVENT_TAXONOMY;
process.env.MEMIND_SSE_EVENT_TAXONOMY = '0';
try {
assert.deepEqual(finalizeSessionStreamEvent(event), event);
} finally {
if (previous == null) delete process.env.MEMIND_SSE_EVENT_TAXONOMY;
else process.env.MEMIND_SSE_EVENT_TAXONOMY = previous;
}
process.env.MEMIND_SSE_EVENT_TAXONOMY = '1';
try {
assert.equal(isSseEventTaxonomyEnabled(), true);
assert.deepEqual(finalizeSessionStreamEvent(event), {
...event,
taxonomy: SSE_EVENT_TAXONOMY.TERMINAL,
});
} finally {
if (previous == null) delete process.env.MEMIND_SSE_EVENT_TAXONOMY;
else process.env.MEMIND_SSE_EVENT_TAXONOMY = previous;
}
});
test('wrapRunStreamPayload keeps legacy payload when flag disabled', () => {
const payload = { run: { id: 'run-1', status: 'running' } };
const previous = process.env.MEMIND_SSE_EVENT_TAXONOMY;
delete process.env.MEMIND_SSE_EVENT_TAXONOMY;
try {
assert.deepEqual(wrapRunStreamPayload('run', payload), payload);
} finally {
if (previous == null) delete process.env.MEMIND_SSE_EVENT_TAXONOMY;
else process.env.MEMIND_SSE_EVENT_TAXONOMY = previous;
}
});
test('writeSseErrorAndEnd uses SSE error frame after headers sent', () => {
const res = new EventEmitter();
res.headersSent = true;
res.writableEnded = false;
const chunks = [];
res.write = (chunk) => {
chunks.push(String(chunk));
return true;
};
res.end = () => {
res.writableEnded = true;
};
res.status = () => {
throw new Error('status must not be called after SSE started');
};
res.json = () => {
throw new Error('json must not be called after SSE started');
};
writeSseErrorAndEnd(res, 'stream failed');
assert.equal(res.writableEnded, true);
assert.match(chunks.join(''), /event: error/);
assert.match(chunks.join(''), /stream failed/);
});