Jelajahi Sumber

Merge pull request #602 from alvinunreal/task/600-resource-optimization-followup

fix: clear interview fallback timer
Alvin 1 bulan lalu
induk
melakukan
438da8df2b
2 mengubah file dengan 76 tambahan dan 0 penghapusan
  1. 68 0
      src/interview/manager.test.ts
  2. 8 0
      src/interview/manager.ts

+ 68 - 0
src/interview/manager.test.ts

@@ -444,6 +444,74 @@ describe('interview manager - session registration', () => {
       await fs.rm(tempDir, { recursive: true, force: true });
     }
   });
+
+  test('clears fallback timer when last registered session is deleted', async () => {
+    const dashboardDir = await fs.mkdtemp('/tmp/manager-test-');
+    const clientDir = await fs.mkdtemp('/tmp/manager-test-');
+    const dashboardCtx = createMockContext({ directory: dashboardDir });
+    const clientCtx = createMockContext({ directory: clientDir });
+
+    const freePort = await findFreePort();
+    const config = createTestConfig({
+      port: freePort,
+      dashboard: true,
+    });
+
+    const originalSetInterval = globalThis.setInterval;
+    const originalClearInterval = globalThis.clearInterval;
+    const intervalHandles: Array<{ unref: ReturnType<typeof mock> }> = [];
+    const setIntervalSpy = mock(() => {
+      const handle = { unref: mock(() => {}) };
+      intervalHandles.push(handle);
+      return handle;
+    });
+    const clearIntervalSpy = mock(() => {});
+
+    try {
+      (globalThis as any).setInterval = setIntervalSpy;
+      (globalThis as any).clearInterval = clearIntervalSpy;
+
+      createInterviewManager(dashboardCtx, config);
+
+      // Wait for dashboard init
+      await new Promise((r) => setTimeout(r, 100));
+
+      const clientManager = createInterviewManager(clientCtx, config);
+
+      // Wait for client init to connect to the dashboard
+      await new Promise((r) => setTimeout(r, 100));
+
+      const output = { parts: [] as Array<{ type: string; text?: string }> };
+      await clientManager.handleCommandExecuteBefore(
+        {
+          command: 'interview',
+          sessionID: 'session-fallback-cleanup',
+          arguments: 'Fallback Cleanup Test',
+        },
+        output,
+      );
+
+      expect(intervalHandles.length).toBeGreaterThan(0);
+      const fallbackTimerHandle = intervalHandles.at(-1);
+      expect(fallbackTimerHandle).toBeDefined();
+
+      await clientManager.handleEvent({
+        event: {
+          type: 'session.deleted',
+          properties: { sessionID: 'session-fallback-cleanup' },
+        },
+      });
+
+      expect(clearIntervalSpy).toHaveBeenCalledTimes(1);
+      expect(clearIntervalSpy).toHaveBeenCalledWith(fallbackTimerHandle);
+      expect(fallbackTimerHandle?.unref).toHaveBeenCalledTimes(1);
+    } finally {
+      (globalThis as any).setInterval = originalSetInterval;
+      (globalThis as any).clearInterval = originalClearInterval;
+      await fs.rm(dashboardDir, { recursive: true, force: true });
+      await fs.rm(clientDir, { recursive: true, force: true });
+    }
+  });
 });
 
 describe('interview manager - edge cases', () => {

+ 8 - 0
src/interview/manager.ts

@@ -97,6 +97,11 @@ export function createInterviewManager(
   // we're in session mode. References poll functions defined below.
   const FALLBACK_POLL_INTERVAL = 10_000;
   let fallbackTimer: ReturnType<typeof setInterval> | null = null;
+  const stopFallbackTimer = () => {
+    if (!fallbackTimer) return;
+    clearInterval(fallbackTimer);
+    fallbackTimer = null;
+  };
   const startFallbackTimer = () => {
     if (fallbackTimer) return;
     fallbackTimer = setInterval(() => {
@@ -478,6 +483,9 @@ export function createInterviewManager(
       // Clean up when a session is deleted
       if (event.type === 'session.deleted' && sessionID) {
         registeredSessions.delete(sessionID);
+        if (registeredSessions.size === 0) {
+          stopFallbackTimer();
+        }
         dashboard?.removeSession(sessionID);
       }
     },