|
1 | 1 | import { act, renderHook } from "@testing-library/react"; |
2 | 2 | import { createStore, Provider } from "jotai"; |
3 | 3 | import type { ReactNode } from "react"; |
4 | | -import { describe, expect, it, vi } from "vitest"; |
| 4 | +import { afterEach, describe, expect, it, vi } from "vitest"; |
5 | 5 | import { wsClientAtom } from "../../../atoms/connection"; |
6 | 6 | import { sessionsAtom } from "../../../atoms/sessions"; |
7 | 7 | import { useSessionActions } from "./use-session-actions"; |
8 | 8 |
|
9 | 9 | describe("useSessionActions", () => { |
| 10 | + afterEach(() => { |
| 11 | + vi.useRealTimers(); |
| 12 | + }); |
| 13 | + |
10 | 14 | it("removes ended sessions directly without issuing session.stop", async () => { |
11 | 15 | const store = createStore(); |
12 | 16 | const sendCommand = vi.fn(async (op: string) => { |
@@ -47,4 +51,96 @@ describe("useSessionActions", () => { |
47 | 51 | expect(sendCommand).toHaveBeenCalledTimes(1); |
48 | 52 | expect(sendCommand).toHaveBeenCalledWith("session.remove", { sessionId: "sess-1" }, undefined); |
49 | 53 | }); |
| 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 | + }); |
50 | 146 | }); |
0 commit comments