mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 07:07:39 +00:00
fix: isolate TUI /new sessions per client
Landed from contributor PR #39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <widing.marcus@gmail.com>
This commit is contained in:
@@ -7,12 +7,14 @@ type SetActivityStatusMock = ReturnType<typeof vi.fn> & ((text: string) => void)
|
||||
function createHarness(params?: {
|
||||
sendChat?: ReturnType<typeof vi.fn>;
|
||||
resetSession?: ReturnType<typeof vi.fn>;
|
||||
setSession?: ReturnType<typeof vi.fn>;
|
||||
loadHistory?: LoadHistoryMock;
|
||||
setActivityStatus?: SetActivityStatusMock;
|
||||
isConnected?: boolean;
|
||||
}) {
|
||||
const sendChat = params?.sendChat ?? vi.fn().mockResolvedValue({ runId: "r1" });
|
||||
const resetSession = params?.resetSession ?? vi.fn().mockResolvedValue({ ok: true });
|
||||
const setSession = params?.setSession ?? vi.fn().mockResolvedValue(undefined);
|
||||
const addUser = vi.fn();
|
||||
const addSystem = vi.fn();
|
||||
const requestRender = vi.fn();
|
||||
@@ -36,7 +38,7 @@ function createHarness(params?: {
|
||||
closeOverlay: vi.fn(),
|
||||
refreshSessionInfo: vi.fn(),
|
||||
loadHistory,
|
||||
setSession: vi.fn(),
|
||||
setSession,
|
||||
refreshAgents: vi.fn(),
|
||||
abortActive: vi.fn(),
|
||||
setActivityStatus,
|
||||
@@ -51,6 +53,7 @@ function createHarness(params?: {
|
||||
handleCommand,
|
||||
sendChat,
|
||||
resetSession,
|
||||
setSession,
|
||||
addUser,
|
||||
addSystem,
|
||||
requestRender,
|
||||
@@ -104,16 +107,26 @@ describe("tui command handlers", () => {
|
||||
expect(requestRender).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes reset reason when handling /new and /reset", async () => {
|
||||
it("creates unique session for /new and resets shared session for /reset", async () => {
|
||||
const loadHistory = vi.fn().mockResolvedValue(undefined);
|
||||
const { handleCommand, resetSession } = createHarness({ loadHistory });
|
||||
const setSessionMock = vi.fn().mockResolvedValue(undefined);
|
||||
const { handleCommand, resetSession } = createHarness({
|
||||
loadHistory,
|
||||
setSession: setSessionMock,
|
||||
});
|
||||
|
||||
await handleCommand("/new");
|
||||
await handleCommand("/reset");
|
||||
|
||||
expect(resetSession).toHaveBeenNthCalledWith(1, "agent:main:main", "new");
|
||||
expect(resetSession).toHaveBeenNthCalledWith(2, "agent:main:main", "reset");
|
||||
expect(loadHistory).toHaveBeenCalledTimes(2);
|
||||
// /new creates a unique session key (isolates TUI client) (#39217)
|
||||
expect(setSessionMock).toHaveBeenCalledTimes(1);
|
||||
expect(setSessionMock).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^tui-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/),
|
||||
);
|
||||
// /reset still resets the shared session
|
||||
expect(resetSession).toHaveBeenCalledTimes(1);
|
||||
expect(resetSession).toHaveBeenCalledWith("agent:main:main", "reset");
|
||||
expect(loadHistory).toHaveBeenCalledTimes(1); // /reset calls loadHistory directly; /new does so indirectly via setSession
|
||||
});
|
||||
|
||||
it("reports send failures and marks activity status as error", async () => {
|
||||
@@ -129,6 +142,21 @@ describe("tui command handlers", () => {
|
||||
expect(setActivityStatus).toHaveBeenLastCalledWith("error");
|
||||
});
|
||||
|
||||
it("sanitizes control sequences in /new and /reset failures", async () => {
|
||||
const setSession = vi.fn().mockRejectedValue(new Error("\u001b[31mboom\u001b[0m"));
|
||||
const resetSession = vi.fn().mockRejectedValue(new Error("\u001b[31mboom\u001b[0m"));
|
||||
const { handleCommand, addSystem } = createHarness({
|
||||
setSession,
|
||||
resetSession,
|
||||
});
|
||||
|
||||
await handleCommand("/new");
|
||||
await handleCommand("/reset");
|
||||
|
||||
expect(addSystem).toHaveBeenNthCalledWith(1, "new session failed: Error: boom");
|
||||
expect(addSystem).toHaveBeenNthCalledWith(2, "reset failed: Error: boom");
|
||||
});
|
||||
|
||||
it("reports disconnected status and skips gateway send when offline", async () => {
|
||||
const { handleCommand, sendChat, addUser, addSystem, setActivityStatus } = createHarness({
|
||||
isConnected: false,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
createSettingsList,
|
||||
} from "./components/selectors.js";
|
||||
import type { GatewayChatClient } from "./gateway-chat.js";
|
||||
import { sanitizeRenderableText } from "./tui-formatters.js";
|
||||
import { formatStatusSummary } from "./tui-status-summary.js";
|
||||
import type {
|
||||
AgentSummary,
|
||||
@@ -423,6 +424,23 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
}
|
||||
break;
|
||||
case "new":
|
||||
try {
|
||||
// Clear token counts immediately to avoid stale display (#1523)
|
||||
state.sessionInfo.inputTokens = null;
|
||||
state.sessionInfo.outputTokens = null;
|
||||
state.sessionInfo.totalTokens = null;
|
||||
tui.requestRender();
|
||||
|
||||
// Generate unique session key to isolate this TUI client (#39217)
|
||||
// This ensures /new creates a fresh session that doesn't broadcast
|
||||
// to other connected TUI clients sharing the original session key.
|
||||
const uniqueKey = `tui-${randomUUID()}`;
|
||||
await setSession(uniqueKey);
|
||||
chatLog.addSystem(`new session: ${uniqueKey}`);
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`new session failed: ${sanitizeRenderableText(String(err))}`);
|
||||
}
|
||||
break;
|
||||
case "reset":
|
||||
try {
|
||||
// Clear token counts immediately to avoid stale display (#1523)
|
||||
@@ -435,7 +453,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
chatLog.addSystem(`session ${state.currentSessionKey} reset`);
|
||||
await loadHistory();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`reset failed: ${String(err)}`);
|
||||
chatLog.addSystem(`reset failed: ${sanitizeRenderableText(String(err))}`);
|
||||
}
|
||||
break;
|
||||
case "abort":
|
||||
|
||||
Reference in New Issue
Block a user