diff --git a/src/features/tmux-subagent/polling-manager.test.ts b/src/features/tmux-subagent/polling-manager.test.ts index 060ee23f5..38b32797f 100644 --- a/src/features/tmux-subagent/polling-manager.test.ts +++ b/src/features/tmux-subagent/polling-manager.test.ts @@ -68,6 +68,8 @@ describe("TmuxPollingManager overlap", () => { closePending: false, closeRetryCount: 0, activityVersion: 0, + stableIdlePolls: 2, + observedIdleActivityVersion: 0, }) let messagesCallCount = 0 @@ -100,10 +102,138 @@ describe("TmuxPollingManager overlap", () => { await pollSessions.call(manager) await pollSessions.call(manager) await pollSessions.call(manager) - await pollSessions.call(manager) //#then expect(messagesCallCount).toBe(0) expect(closedSessionIds).toEqual(["ses-1"]) }) + + test("does not close sessions missing from one poll until the longer grace window elapses", async () => { + // given + const now = Date.now() + const sessions = new Map() + sessions.set("ses-1", { + sessionId: "ses-1", + paneId: "%1", + description: "test", + createdAt: new Date(now - 1_000), + lastSeenAt: new Date(now - 7_000), + closePending: false, + closeRetryCount: 0, + activityVersion: 0, + }) + + const closedSessionIds: string[] = [] + const client = { + session: { + status: async () => ({ data: {} }), + messages: async () => ({ data: [] }), + }, + } + + const manager = new TmuxPollingManager( + client as unknown as import("../../tools/delegate-task/types").OpencodeClient, + sessions, + async (sessionId) => { + closedSessionIds.push(sessionId) + }, + ) + + // when + const pollSessions = (manager as unknown as { pollSessions: () => Promise }).pollSessions + await pollSessions.call(manager) + + // then + expect(closedSessionIds).toEqual([]) + }) + + test("does not time out active sessions after only eleven minutes", async () => { + // given + const now = Date.now() + const sessions = new Map() + sessions.set("ses-1", { + sessionId: "ses-1", + paneId: "%1", + description: "test", + createdAt: new Date(now - 11 * 60 * 1000), + lastSeenAt: new Date(now), + closePending: false, + closeRetryCount: 0, + activityVersion: 0, + }) + + const closedSessionIds: string[] = [] + const client = { + session: { + status: async () => ({ data: { "ses-1": { type: "running" } } }), + messages: async () => ({ data: [] }), + }, + } + + const manager = new TmuxPollingManager( + client as unknown as import("../../tools/delegate-task/types").OpencodeClient, + sessions, + async (sessionId) => { + closedSessionIds.push(sessionId) + }, + ) + + // when + const pollSessions = (manager as unknown as { pollSessions: () => Promise }).pollSessions + await pollSessions.call(manager) + + // then + expect(closedSessionIds).toEqual([]) + }) + + test("does not close when activityVersion changes before the idle recheck resolves", async () => { + // given + const sessions = new Map() + sessions.set("ses-1", { + sessionId: "ses-1", + paneId: "%1", + description: "test", + createdAt: new Date(Date.now() - 15_000), + lastSeenAt: new Date(), + closePending: false, + closeRetryCount: 0, + activityVersion: 0, + }) + + const closedSessionIds: string[] = [] + let statusCallCount = 0 + let manager: TmuxPollingManager + + const client = { + session: { + status: async () => { + statusCallCount += 1 + if (statusCallCount === 2) { + manager.handleEvent({ + type: "message.part.delta", + properties: { sessionID: "ses-1", field: "text", delta: "new activity" }, + }) + } + + return { data: { "ses-1": { type: "idle" } } } + }, + messages: async () => ({ data: [] }), + }, + } + + manager = new TmuxPollingManager( + client as unknown as import("../../tools/delegate-task/types").OpencodeClient, + sessions, + async (sessionId) => { + closedSessionIds.push(sessionId) + }, + ) + const pollSessions = (manager as unknown as { pollSessions: () => Promise }).pollSessions + + // when + await pollSessions.call(manager) + + // then + expect(closedSessionIds).toEqual([]) + }) })