diff --git a/packages/web/bin/lib/cli-control.js b/packages/web/bin/lib/cli-control.js index 6cbbc864..2a6df6eb 100644 --- a/packages/web/bin/lib/cli-control.js +++ b/packages/web/bin/lib/cli-control.js @@ -10,14 +10,27 @@ const asNonEmptyString = (value) => { const DEFAULT_WAIT_TIMEOUT_SECONDS = 600; const WAIT_HTTP_TIMEOUT_BUFFER_MS = 30_000; +// Provisioning a worktree is not one of the instant control calls the short +// default is sized for: it runs git against the repository and prepares a new +// directory, which on a cold path takes longer than the default allows. The +// server finishes the work regardless of the client giving up, so a client-side +// timeout here reported a failure for a worktree that was in fact created. +const WORKTREE_PROVISION_TIMEOUT_MS = 120_000; + // The control service blocks server-side while wait is set, so the client // HTTP timeout must outlive the requested wait window instead of the short // default used for instant control calls. export const resolveControlTimeoutMs = (input, options) => { if (Number.isFinite(options?.timeoutMs) && options.timeoutMs > 0) return options.timeoutMs; - if (input?.wait !== true) return undefined; + const provisionsWorktree = asNonEmptyString(input?.worktree) !== null; + if (input?.wait !== true) { + return provisionsWorktree ? WORKTREE_PROVISION_TIMEOUT_MS : undefined; + } const waitSeconds = Number(input?.timeout) > 0 ? Number(input.timeout) : DEFAULT_WAIT_TIMEOUT_SECONDS; - return (waitSeconds * 1000) + WAIT_HTTP_TIMEOUT_BUFFER_MS; + const waitTimeoutMs = (waitSeconds * 1000) + WAIT_HTTP_TIMEOUT_BUFFER_MS; + // Waiting for the session and provisioning its worktree are additive, so the + // window must cover whichever is longer rather than only the wait. + return provisionsWorktree ? Math.max(waitTimeoutMs, WORKTREE_PROVISION_TIMEOUT_MS) : waitTimeoutMs; }; export const requestControlAction = async (port, action, input, options = {}) => { diff --git a/packages/web/bin/lib/cli-control.test.js b/packages/web/bin/lib/cli-control.test.js index 0e259e58..1ae93015 100644 --- a/packages/web/bin/lib/cli-control.test.js +++ b/packages/web/bin/lib/cli-control.test.js @@ -19,4 +19,20 @@ describe('resolveControlTimeoutMs', () => { it('never shrinks an explicitly requested HTTP timeout', () => { expect(resolveControlTimeoutMs({ wait: true, timeout: 30 }, { timeoutMs: 5000 })).toBe(5000); }); + + it('allows a worktree to be provisioned without waiting for the session', () => { + expect(resolveControlTimeoutMs({ worktree: 'feature' }, {})).toBe(120_000); + }); + + it('ignores a blank worktree name', () => { + expect(resolveControlTimeoutMs({ worktree: ' ' }, {})).toBeUndefined(); + }); + + it('covers worktree provisioning even when the wait window is shorter', () => { + expect(resolveControlTimeoutMs({ wait: true, timeout: 30, worktree: 'feature' }, {})).toBe(120_000); + }); + + it('keeps a longer wait window when it outlasts worktree provisioning', () => { + expect(resolveControlTimeoutMs({ wait: true, worktree: 'feature' }, {})).toBe(630_000); + }); });