fix(webui): clear stale stream state after reconnect

This commit is contained in:
Oxygen56
2026-09-03 17:58:45 +08:00
committed by Xubin Ren
parent b909793784
commit 67f0f26b04
3 changed files with 76 additions and 0 deletions
+44
View File
@@ -70,6 +70,7 @@ function normalizeProjection(messages: UIMessage[]): Array<Record<string, unknow
function fakeClient() {
const handlers = new Map<string, Set<(ev: InboundEvent) => void>>();
const statusHandlers = new Set<(status: ConnectionStatus) => void>();
const runStatusHandlers = new Set<(chatId: string, startedAt: number | null) => void>();
const errorHandlers = new Set<(error: StreamError) => void>();
const runStartedAtByChatId = new Map<string, number>();
const unsettledRunByChatId = new Map<string, boolean>();
@@ -111,6 +112,11 @@ function fakeClient() {
handler(status);
return () => statusHandlers.delete(handler);
},
onRunStatus(handler: (chatId: string, startedAt: number | null) => void) {
runStatusHandlers.add(handler);
for (const [chatId, startedAt] of runStartedAtByChatId) handler(chatId, startedAt);
return () => runStatusHandlers.delete(handler);
},
onError(handler: (error: StreamError) => void) {
errorHandlers.add(handler);
return () => errorHandlers.delete(handler);
@@ -154,6 +160,11 @@ function fakeClient() {
status = nextStatus;
statusHandlers.forEach((handler) => handler(status));
},
emitRunStatus(chatId: string, startedAt: number | null) {
if (startedAt === null) runStartedAtByChatId.delete(chatId);
else runStartedAtByChatId.set(chatId, startedAt);
runStatusHandlers.forEach((handler) => handler(chatId, startedAt));
},
emitError(error: StreamError) {
errorHandlers.forEach((handler) => handler(error));
},
@@ -327,6 +338,39 @@ describe("useNanobotStream", () => {
});
});
it("clears stale stream state when the transport resets a run", async () => {
const fake = fakeClient();
const { result } = renderHook(
() => useNanobotStream("chat-reconnect-reset", EMPTY_MESSAGES),
{ wrapper: wrap(fake.client) },
);
act(() => {
fake.emit("chat-reconnect-reset", {
event: "goal_status",
chat_id: "chat-reconnect-reset",
status: "running",
started_at: 1_700,
});
fake.emit("chat-reconnect-reset", {
event: "delta",
chat_id: "chat-reconnect-reset",
text: "partial",
});
});
await flushStreamFrame();
expect(result.current.isStreaming).toBe(true);
act(() => fake.emitRunStatus("chat-reconnect-reset", null));
expect(result.current.runStartedAt).toBeNull();
expect(result.current.isStreaming).toBe(false);
expect(result.current.messages[0]).toMatchObject({
content: "partial",
isStreaming: false,
});
});
it("flushes pending delta text before turn_end finalizes the turn", () => {
const fake = fakeClient();
const { result } = renderHook(() => useNanobotStream("chat-flush", EMPTY_MESSAGES), {