From b437e185e6331e5bdec55d0b3b3a6f2ed57801a4 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Mon, 7 Sep 2026 12:32:40 -0700 Subject: [PATCH] test(terminal): pin the spawn watchdog against main's real daemon timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The budget test imported the startup gate but hand-copied the daemon client's connect and request timeouts, because those were module-private. Raising REQUEST_TIMEOUT_MS in main would then leave the watchdog silently short and the test still green — the drift it exists to catch. Move the three to an import-free module both sides can read, and note in each pinned file that it is typechecked under the web config so it stays that way. --- config/tsconfig.tc.web.json | 1 + src/main/daemon/client.ts | 4 +--- src/main/daemon/rpc-timeouts.ts | 6 ++++++ src/main/startup/first-window-startup-services.ts | 3 +++ .../spawn-settlement-watchdog-budget.test.ts | 12 ++++++------ 5 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 src/main/daemon/rpc-timeouts.ts diff --git a/config/tsconfig.tc.web.json b/config/tsconfig.tc.web.json index dfda2ddb13b..f4ea0bfebf5 100644 --- a/config/tsconfig.tc.web.json +++ b/config/tsconfig.tc.web.json @@ -38,6 +38,7 @@ "../src/main/persistence/applying-settings/ui-selection-normalization.ts", "../src/main/persistence/applying-settings/ui-interaction-merge.ts", "../src/main/protected-secret-persistence.ts", + "../src/main/daemon/rpc-timeouts.ts", "../src/main/startup/first-window-startup-services.ts", "../src/main/startup/serve-desktop-activation.ts", "../src/main/startup/serve-mode-argv.ts", diff --git a/src/main/daemon/client.ts b/src/main/daemon/client.ts index 37d628a1464..9854adbffd8 100644 --- a/src/main/daemon/client.ts +++ b/src/main/daemon/client.ts @@ -2,6 +2,7 @@ import type { Socket } from 'node:net' import { readFileSync } from 'node:fs' import { randomUUID } from 'node:crypto' import { encodeNdjson } from './ndjson' +import { CONNECTION_ATTEMPT_WAIT_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS } from './rpc-timeouts' import { PROTOCOL_VERSION, NOTIFY_PREFIX, @@ -25,9 +26,6 @@ import { import { writeNotifyWithSettlement } from './daemon-client-notify-settlement' import { requestDaemonRpc } from './daemon-client-rpc-request' -const CONNECT_TIMEOUT_MS = 5000 -const CONNECTION_ATTEMPT_WAIT_MS = CONNECT_TIMEOUT_MS * 4 -const REQUEST_TIMEOUT_MS = 30000 const NOTIFY_SETTLEMENT_TIMEOUT_MS = 5000 export type DaemonClientOptions = { diff --git a/src/main/daemon/rpc-timeouts.ts b/src/main/daemon/rpc-timeouts.ts new file mode 100644 index 00000000000..2fd306f387b --- /dev/null +++ b/src/main/daemon/rpc-timeouts.ts @@ -0,0 +1,6 @@ +// Why these live apart from the client: the renderer's spawn-settlement watchdog must +// outlast them, and its budget test imports them from here to prove it still does. That +// test typechecks under the web config, so this file must stay import-free. +export const CONNECT_TIMEOUT_MS = 5000 +export const CONNECTION_ATTEMPT_WAIT_MS = CONNECT_TIMEOUT_MS * 4 +export const REQUEST_TIMEOUT_MS = 30000 diff --git a/src/main/startup/first-window-startup-services.ts b/src/main/startup/first-window-startup-services.ts index 407ba23f555..35cba4f4ec7 100644 --- a/src/main/startup/first-window-startup-services.ts +++ b/src/main/startup/first-window-startup-services.ts @@ -22,6 +22,9 @@ export const FIRST_WINDOW_STARTUP_SERVICE_TIMEOUT_MS = 12_000 // them lose their daemon sessions permanently (#5232). The PTY gate therefore // waits for the daemon attempt itself and only fail-opens at a hard cap that // exists solely as a deadlock backstop. +// The renderer's spawn-settlement watchdog must outlast this, and its budget test +// imports it from here — so this file is typechecked under the web config too and +// must stay import-free. export const LOCAL_PTY_STARTUP_FAIL_OPEN_TIMEOUT_MS = 60_000 function startService( diff --git a/src/renderer/src/components/terminal-pane/pty-connection/spawn-settlement-watchdog-budget.test.ts b/src/renderer/src/components/terminal-pane/pty-connection/spawn-settlement-watchdog-budget.test.ts index 5af0c719cd5..194abc70c14 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/spawn-settlement-watchdog-budget.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/spawn-settlement-watchdog-budget.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' import { LOCAL_PTY_STARTUP_FAIL_OPEN_TIMEOUT_MS } from '../../../../../main/startup/first-window-startup-services' +import { + CONNECTION_ATTEMPT_WAIT_MS, + REQUEST_TIMEOUT_MS +} from '../../../../../main/daemon/rpc-timeouts' import { REMOTE_RUNTIME_SPAWN_SETTLEMENT_WATCHDOG_MS, SPAWN_SETTLEMENT_WATCHDOG_MS @@ -13,12 +17,8 @@ import { REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS } from '../remote-runtime-pty-r describe('spawn settlement watchdog budget', () => { // Sequential, not overlapping: pty:spawn awaits the startup gate, and only then // does the daemon client spend its connection-attempt wait plus one request timeout. - const DAEMON_CONNECTION_ATTEMPT_WAIT_MS = 5_000 * 4 - const DAEMON_REQUEST_TIMEOUT_MS = 30_000 const worstLegitimateLocalSettleMs = - LOCAL_PTY_STARTUP_FAIL_OPEN_TIMEOUT_MS + - DAEMON_CONNECTION_ATTEMPT_WAIT_MS + - DAEMON_REQUEST_TIMEOUT_MS + LOCAL_PTY_STARTUP_FAIL_OPEN_TIMEOUT_MS + CONNECTION_ATTEMPT_WAIT_MS + REQUEST_TIMEOUT_MS it('outlasts the slowest settle a local cold start can legitimately take', () => { expect(SPAWN_SETTLEMENT_WATCHDOG_MS).toBeGreaterThan(worstLegitimateLocalSettleMs) @@ -26,7 +26,7 @@ describe('spawn settlement watchdog budget', () => { it('keeps real headroom over that worst case rather than racing it', () => { expect(SPAWN_SETTLEMENT_WATCHDOG_MS - worstLegitimateLocalSettleMs).toBeGreaterThanOrEqual( - DAEMON_REQUEST_TIMEOUT_MS + REQUEST_TIMEOUT_MS ) })