Skip to content

Commit 7d2c036

Browse files
committed
fix(web): avoid window timer dependency in session close polling
1 parent c50f624 commit 7d2c036

2 files changed

Lines changed: 104 additions & 5 deletions

File tree

packages/web/src/features/agent-panes/actions/use-session-actions.test.tsx

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { act, renderHook } from "@testing-library/react";
22
import { createStore, Provider } from "jotai";
33
import type { ReactNode } from "react";
4-
import { describe, expect, it, vi } from "vitest";
4+
import { afterEach, describe, expect, it, vi } from "vitest";
55
import { wsClientAtom } from "../../../atoms/connection";
66
import { sessionsAtom } from "../../../atoms/sessions";
77
import { useSessionActions } from "./use-session-actions";
88

99
describe("useSessionActions", () => {
10+
afterEach(() => {
11+
vi.useRealTimers();
12+
});
13+
1014
it("removes ended sessions directly without issuing session.stop", async () => {
1115
const store = createStore();
1216
const sendCommand = vi.fn(async (op: string) => {
@@ -47,4 +51,96 @@ describe("useSessionActions", () => {
4751
expect(sendCommand).toHaveBeenCalledTimes(1);
4852
expect(sendCommand).toHaveBeenCalledWith("session.remove", { sessionId: "sess-1" }, undefined);
4953
});
54+
55+
it("closes running sessions even when window timers are unavailable", async () => {
56+
vi.useFakeTimers();
57+
58+
const store = createStore();
59+
let resolveStop: (() => void) | undefined;
60+
const sendCommand = vi.fn((op: string) => {
61+
if (op === "session.stop") {
62+
return new Promise<void>((resolve) => {
63+
resolveStop = resolve;
64+
});
65+
}
66+
if (op === "session.remove") {
67+
return Promise.resolve(undefined);
68+
}
69+
throw new Error(`Unexpected op: ${op}`);
70+
});
71+
72+
store.set(wsClientAtom, {
73+
sendCommand,
74+
subscribe: vi.fn(() => () => {}),
75+
} as never);
76+
store.set(sessionsAtom, {
77+
"sess-1": {
78+
id: "sess-1",
79+
workspaceId: "ws-1",
80+
terminalId: "term-1",
81+
providerId: "codex",
82+
state: "running",
83+
capability: "full",
84+
startedAt: 1,
85+
lastActiveAt: 1,
86+
},
87+
});
88+
89+
const wrapper = ({ children }: { children: ReactNode }) => (
90+
<Provider store={store}>{children}</Provider>
91+
);
92+
93+
const { result } = renderHook(() => useSessionActions(), { wrapper });
94+
const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
95+
96+
Object.defineProperty(globalThis, "window", {
97+
configurable: true,
98+
value: {},
99+
});
100+
101+
try {
102+
const closePromise = result.current.closeSession("sess-1");
103+
await Promise.resolve();
104+
resolveStop?.();
105+
await Promise.resolve();
106+
107+
queueMicrotask(() => {
108+
store.set(sessionsAtom, {
109+
"sess-1": {
110+
id: "sess-1",
111+
workspaceId: "ws-1",
112+
terminalId: "term-1",
113+
providerId: "codex",
114+
state: "ended",
115+
capability: "full",
116+
startedAt: 1,
117+
lastActiveAt: 1,
118+
endedAt: 2,
119+
},
120+
});
121+
});
122+
123+
await vi.advanceTimersByTimeAsync(100);
124+
await expect(closePromise).resolves.toBeUndefined();
125+
} finally {
126+
if (windowDescriptor) {
127+
Object.defineProperty(globalThis, "window", windowDescriptor);
128+
} else {
129+
Reflect.deleteProperty(globalThis, "window");
130+
}
131+
}
132+
133+
expect(sendCommand).toHaveBeenNthCalledWith(
134+
1,
135+
"session.stop",
136+
{ sessionId: "sess-1" },
137+
undefined
138+
);
139+
expect(sendCommand).toHaveBeenNthCalledWith(
140+
2,
141+
"session.remove",
142+
{ sessionId: "sess-1" },
143+
undefined
144+
);
145+
});
50146
});

packages/web/src/features/agent-panes/actions/use-session-actions.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ const terminalInputEncoder = new TextEncoder();
77
const SESSION_REMOVAL_POLL_INTERVAL_MS = 100;
88
const SESSION_REMOVAL_TIMEOUT_MS = 5_000;
99

10+
function delay(ms: number) {
11+
return new Promise<void>((resolve) => {
12+
globalThis.setTimeout(resolve, ms);
13+
});
14+
}
15+
1016
export function useSessionActions() {
1117
const dispatch = useAtomValue(dispatchCommandAtom);
1218
const wsClient = useAtomValue(wsClientAtom);
@@ -56,10 +62,7 @@ export function useSessionActions() {
5662
}
5763
return;
5864
}
59-
60-
await new Promise((resolve) => {
61-
window.setTimeout(resolve, SESSION_REMOVAL_POLL_INTERVAL_MS);
62-
});
65+
await delay(SESSION_REMOVAL_POLL_INTERVAL_MS);
6366
}
6467

6568
console.error("Timed out waiting for session to end before removal:", sessionId);

0 commit comments

Comments
 (0)