mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
fix(ui): ignore detached btw terminal teardown
This commit is contained in:
@@ -98,7 +98,14 @@ vi.mock("./controllers/chat.ts", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
function createHost() {
|
||||
type TestGatewayHost = Parameters<typeof connectGateway>[0] & {
|
||||
chatSideResult: unknown;
|
||||
chatSideResultTerminalRuns: Set<string>;
|
||||
chatStream: string | null;
|
||||
toolStreamOrder: string[];
|
||||
};
|
||||
|
||||
function createHost(): TestGatewayHost {
|
||||
return {
|
||||
settings: {
|
||||
gatewayUrl: "ws://127.0.0.1:18789",
|
||||
@@ -155,10 +162,7 @@ function createHost() {
|
||||
execApprovalQueue: [],
|
||||
execApprovalError: null,
|
||||
updateAvailable: null,
|
||||
} as unknown as Parameters<typeof connectGateway>[0] & {
|
||||
chatSideResult: unknown;
|
||||
chatSideResultTerminalRuns: Set<string>;
|
||||
};
|
||||
} as unknown as TestGatewayHost;
|
||||
}
|
||||
|
||||
function connectHostGateway() {
|
||||
@@ -594,9 +598,12 @@ describe("connectGateway", () => {
|
||||
expect(host.chatSideResultTerminalRuns.has("btw-run-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not reload chat history for BTW terminal finals, even after tool events", () => {
|
||||
it("ignores tracked BTW terminal finals without tearing down the active run", () => {
|
||||
const { host, client } = connectHostGateway();
|
||||
host.chatRunId = "main-run-1";
|
||||
emitToolResultEvent(client);
|
||||
host.chatStream = "still streaming";
|
||||
expect(host.toolStreamOrder).toHaveLength(1);
|
||||
|
||||
client.emitEvent({
|
||||
event: "chat.side_result",
|
||||
@@ -619,9 +626,78 @@ describe("connectGateway", () => {
|
||||
});
|
||||
|
||||
expect(loadChatHistoryMock).not.toHaveBeenCalled();
|
||||
expect(host.chatRunId).toBe("main-run-1");
|
||||
expect(host.chatStream).toBe("still streaming");
|
||||
expect(host.toolStreamOrder).toHaveLength(1);
|
||||
expect(host.chatSideResultTerminalRuns.has("btw-run-2")).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["aborted", "error"] as const)(
|
||||
"cleans up tracked BTW %s events without touching the active run",
|
||||
(terminalState) => {
|
||||
const { host, client } = connectHostGateway();
|
||||
host.chatRunId = "main-run-2";
|
||||
emitToolResultEvent(client);
|
||||
host.chatStream = "stream in progress";
|
||||
|
||||
client.emitEvent({
|
||||
event: "chat.side_result",
|
||||
payload: {
|
||||
kind: "btw",
|
||||
runId: `btw-run-${terminalState}`,
|
||||
sessionKey: "main",
|
||||
question: "what changed?",
|
||||
text: "Detached BTW response",
|
||||
ts: 789,
|
||||
},
|
||||
});
|
||||
client.emitEvent({
|
||||
event: "chat",
|
||||
payload: {
|
||||
runId: `btw-run-${terminalState}`,
|
||||
sessionKey: "main",
|
||||
state: terminalState,
|
||||
errorMessage: terminalState === "error" ? "btw failed" : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(host.chatSideResultTerminalRuns.has(`btw-run-${terminalState}`)).toBe(false);
|
||||
expect(host.chatRunId).toBe("main-run-2");
|
||||
expect(host.chatStream).toBe("stream in progress");
|
||||
expect(host.toolStreamOrder).toHaveLength(1);
|
||||
expect(host.lastError).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it("clears tracked BTW terminal runs after reconnect hello", () => {
|
||||
const host = createHost();
|
||||
|
||||
connectGateway(host);
|
||||
const firstClient = gatewayClientInstances[0];
|
||||
expect(firstClient).toBeDefined();
|
||||
|
||||
firstClient.emitEvent({
|
||||
event: "chat.side_result",
|
||||
payload: {
|
||||
kind: "btw",
|
||||
runId: "btw-run-reconnect",
|
||||
sessionKey: "main",
|
||||
question: "what changed?",
|
||||
text: "Temporary BTW state",
|
||||
ts: 987,
|
||||
},
|
||||
});
|
||||
expect(host.chatSideResultTerminalRuns.has("btw-run-reconnect")).toBe(true);
|
||||
|
||||
connectGateway(host);
|
||||
const reconnectClient = gatewayClientInstances[1];
|
||||
expect(reconnectClient).toBeDefined();
|
||||
|
||||
reconnectClient.emitHello();
|
||||
|
||||
expect(host.chatSideResultTerminalRuns.size).toBe(0);
|
||||
});
|
||||
|
||||
it("ignores BTW side results for other sessions", () => {
|
||||
const { host, client } = connectHostGateway();
|
||||
|
||||
|
||||
@@ -115,6 +115,12 @@ type GatewayHostWithSideResults = GatewayHost & {
|
||||
chatSideResultTerminalRuns?: Set<string>;
|
||||
};
|
||||
|
||||
function isTerminalChatState(
|
||||
state: ChatEventPayload["state"] | ReturnType<typeof handleChatEvent> | null | undefined,
|
||||
): state is "final" | "aborted" | "error" {
|
||||
return state === "final" || state === "aborted" || state === "error";
|
||||
}
|
||||
|
||||
type ConnectGatewayOptions = {
|
||||
reason?: "initial" | "seq-gap";
|
||||
};
|
||||
@@ -251,6 +257,7 @@ export function connectGateway(host: GatewayHost, options?: ConnectGatewayOption
|
||||
host.chatRunId = null;
|
||||
(host as unknown as { chatStream: string | null }).chatStream = null;
|
||||
(host as unknown as { chatStreamStartedAt: number | null }).chatStreamStartedAt = null;
|
||||
(host as GatewayHostWithSideResults).chatSideResultTerminalRuns?.clear();
|
||||
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
||||
if (shutdownHost.resumeChatQueueAfterReconnect) {
|
||||
// The interrupted run will never emit its terminal event now that the
|
||||
@@ -328,7 +335,6 @@ function handleTerminalChatEvent(
|
||||
host: GatewayHost,
|
||||
payload: ChatEventPayload | undefined,
|
||||
state: ReturnType<typeof handleChatEvent>,
|
||||
opts?: { skipHistoryReload?: boolean },
|
||||
): boolean {
|
||||
if (state !== "final" && state !== "error" && state !== "aborted") {
|
||||
return false;
|
||||
@@ -353,7 +359,7 @@ function handleTerminalChatEvent(
|
||||
}
|
||||
// Reload history when tools were used so the persisted tool results
|
||||
// replace the now-cleared streaming state.
|
||||
if (hadToolEvents && state === "final" && !opts?.skipHistoryReload) {
|
||||
if (hadToolEvents && state === "final") {
|
||||
void loadChatHistory(host as unknown as ChatState);
|
||||
return true;
|
||||
}
|
||||
@@ -367,24 +373,18 @@ function handleChatGatewayEvent(host: GatewayHost, payload: ChatEventPayload | u
|
||||
payload.sessionKey,
|
||||
);
|
||||
}
|
||||
const state = handleChatEvent(host as unknown as ChatState, payload);
|
||||
const sideResultHost = host as GatewayHostWithSideResults;
|
||||
const skipHistoryReloadForSideResult =
|
||||
state === "final" &&
|
||||
const isTrackedSideResultTerminalEvent =
|
||||
isTerminalChatState(payload?.state) &&
|
||||
typeof payload?.runId === "string" &&
|
||||
sideResultHost.chatSideResultTerminalRuns?.has(payload.runId) === true;
|
||||
if (skipHistoryReloadForSideResult && payload?.runId) {
|
||||
if (isTrackedSideResultTerminalEvent && payload?.runId) {
|
||||
sideResultHost.chatSideResultTerminalRuns?.delete(payload.runId);
|
||||
return;
|
||||
}
|
||||
const historyReloaded = handleTerminalChatEvent(host, payload, state, {
|
||||
skipHistoryReload: skipHistoryReloadForSideResult,
|
||||
});
|
||||
if (
|
||||
state === "final" &&
|
||||
!skipHistoryReloadForSideResult &&
|
||||
!historyReloaded &&
|
||||
shouldReloadHistoryForFinalEvent(payload)
|
||||
) {
|
||||
const state = handleChatEvent(host as unknown as ChatState, payload);
|
||||
const historyReloaded = handleTerminalChatEvent(host, payload, state);
|
||||
if (state === "final" && !historyReloaded && shouldReloadHistoryForFinalEvent(payload)) {
|
||||
void loadChatHistory(host as unknown as ChatState);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user