Quellcode durchsuchen

fix tmux status polling

Alvin Unreal vor 2 Monaten
Ursprung
Commit
1ebd162df2
2 geänderte Dateien mit 72 neuen und 19 gelöschten Zeilen
  1. 56 10
      src/multiplexer/session-manager.test.ts
  2. 16 9
      src/multiplexer/session-manager.ts

+ 56 - 10
src/multiplexer/session-manager.test.ts

@@ -1,6 +1,16 @@
-import { beforeEach, describe, expect, mock, test } from 'bun:test';
+import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
 import { MultiplexerSessionManager } from './session-manager';
 
+const originalFetch = globalThis.fetch;
+let mockSessionStatuses: Record<string, { type: string }> = {};
+const mockFetch = mock(
+  async () =>
+    new Response(JSON.stringify(mockSessionStatuses), {
+      headers: { 'content-type': 'application/json' },
+      status: 200,
+    }),
+);
+
 // Define the mock multiplexer
 const mockMultiplexer = {
   type: 'tmux' as const,
@@ -25,6 +35,7 @@ mock.module('../multiplexer', () => ({
 function createMockContext(overrides?: {
   sessionStatusResult?: { data?: Record<string, { type: string }> };
   directory?: string;
+  serverUrl?: string;
 }) {
   const defaultPort = process.env.OPENCODE_PORT ?? '4096';
   return {
@@ -36,10 +47,16 @@ function createMockContext(overrides?: {
       },
     },
     directory: overrides?.directory ?? '/test/directory',
-    serverUrl: new URL(`http://localhost:${defaultPort}`),
+    serverUrl: new URL(
+      overrides?.serverUrl ?? `http://localhost:${defaultPort}`,
+    ),
   } as any;
 }
 
+function setMockSessionStatuses(statuses: Record<string, { type: string }>) {
+  mockSessionStatuses = statuses;
+}
+
 const defaultMultiplexerConfig = {
   type: 'tmux' as const,
   layout: 'main-vertical' as const,
@@ -58,6 +75,9 @@ function createDeferred<T>() {
 
 describe('MultiplexerSessionManager', () => {
   beforeEach(() => {
+    mockSessionStatuses = {};
+    mockFetch.mockClear();
+    globalThis.fetch = mockFetch as typeof fetch;
     mockMultiplexer.spawnPane.mockReset();
     mockMultiplexer.spawnPane.mockResolvedValue({
       success: true,
@@ -69,6 +89,10 @@ describe('MultiplexerSessionManager', () => {
     mockMultiplexer.isInsideSession.mockReturnValue(true);
   });
 
+  afterEach(() => {
+    globalThis.fetch = originalFetch;
+  });
+
   describe('constructor', () => {
     test('initializes with config', () => {
       const ctx = createMockContext();
@@ -227,10 +251,7 @@ describe('MultiplexerSessionManager', () => {
         properties: { info: { id: 'c1', parentID: 'p1' } },
       });
 
-      // Mock status
-      ctx.client.session.status.mockResolvedValue({
-        data: { c1: { type: 'idle' } },
-      });
+      setMockSessionStatuses({ c1: { type: 'idle' } });
 
       await (manager as any).pollSessions();
 
@@ -249,9 +270,36 @@ describe('MultiplexerSessionManager', () => {
         properties: { info: { id: 'c1', parentID: 'p1' } },
       });
 
-      ctx.client.session.status.mockResolvedValue({ data: {} });
+      setMockSessionStatuses({});
+      await (manager as any).pollSessions();
+
+      expect(mockMultiplexer.closePane).not.toHaveBeenCalled();
+    });
+
+    test('polls the actual serverUrl instead of the plugin SDK default URL', async () => {
+      const ctx = createMockContext({
+        serverUrl: 'http://127.0.0.1:63871/',
+        sessionStatusResult: { data: {} },
+      });
+      const manager = new MultiplexerSessionManager(
+        ctx,
+        defaultMultiplexerConfig,
+      );
+
+      await manager.onSessionCreated({
+        type: 'session.created',
+        properties: { info: { id: 'child-live', parentID: 'parent-live' } },
+      });
+
+      setMockSessionStatuses({ 'child-live': { type: 'busy' } });
+
       await (manager as any).pollSessions();
 
+      expect(ctx.client.session.status).not.toHaveBeenCalled();
+      expect(mockFetch).toHaveBeenCalledWith(
+        new URL('http://127.0.0.1:63871/session/status'),
+        expect.any(Object),
+      );
       expect(mockMultiplexer.closePane).not.toHaveBeenCalled();
     });
 
@@ -284,9 +332,7 @@ describe('MultiplexerSessionManager', () => {
         },
       });
 
-      ctx.client.session.status.mockResolvedValue({
-        data: { 'child-789': { type: 'idle' } },
-      });
+      setMockSessionStatuses({ 'child-789': { type: 'idle' } });
       await (manager as any).pollSessions();
 
       await manager.onSessionStatus({

+ 16 - 9
src/multiplexer/session-manager.ts

@@ -8,8 +8,6 @@ import {
 } from '../multiplexer';
 import { log } from '../utils/logger';
 
-type OpencodeClient = PluginInput['client'];
-
 interface TrackedSession {
   sessionId: string;
   paneId: string;
@@ -53,7 +51,6 @@ const SESSION_MISSING_GRACE_MS = POLL_INTERVAL_BACKGROUND_MS * 3;
  * with polling kept as a fallback for reliability.
  */
 export class MultiplexerSessionManager {
-  private client: OpencodeClient;
   private serverUrl: string;
   private directory: string;
   private multiplexer: Multiplexer | null = null;
@@ -65,7 +62,6 @@ export class MultiplexerSessionManager {
   private enabled = false;
 
   constructor(ctx: PluginInput, config: MultiplexerConfig) {
-    this.client = ctx.client;
     this.directory = ctx.directory;
     const defaultPort = process.env.OPENCODE_PORT ?? '4096';
     this.serverUrl =
@@ -246,11 +242,7 @@ export class MultiplexerSessionManager {
     }
 
     try {
-      const statusResult = await this.client.session.status();
-      const allStatuses = (statusResult.data ?? {}) as Record<
-        string,
-        { type: string }
-      >;
+      const allStatuses = await this.fetchSessionStatuses();
 
       const now = Date.now();
       const sessionsToClose: Array<{ sessionId: string; reason: CloseReason }> =
@@ -288,6 +280,21 @@ export class MultiplexerSessionManager {
     }
   }
 
+  private async fetchSessionStatuses(): Promise<
+    Record<string, { type: string }>
+  > {
+    const url = new URL('/session/status', this.serverUrl);
+    const response = await fetch(url, { signal: AbortSignal.timeout(2_000) });
+
+    if (!response.ok) {
+      throw new Error(
+        `session status request failed: ${response.status} ${response.statusText}`,
+      );
+    }
+
+    return (await response.json()) as Record<string, { type: string }>;
+  }
+
   private async closeSession(
     sessionId: string,
     reason: CloseReason,