diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0e5efef..2c03ecb36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to Agent Relay will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unreleased - Patch] + +### Fixed + +- Increased the Relayfile integration control-plane request budget so subscription provisioning does not fail at the previous 10-second boundary when Cloud provider status is slow. ## [11.5.2] - 2026-08-11 diff --git a/packages/cli/src/cli/commands/integration-relayfile-contract.test.ts b/packages/cli/src/cli/commands/integration-relayfile-contract.test.ts index 686ea9ac3..67887ed74 100644 --- a/packages/cli/src/cli/commands/integration-relayfile-contract.test.ts +++ b/packages/cli/src/cli/commands/integration-relayfile-contract.test.ts @@ -6,7 +6,13 @@ import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { MIN_RELAYFILE_VERSION, assertRelayfileVersion, defaultRelayfileBridge } from './integration.js'; +import { + MIN_RELAYFILE_VERSION, + assertRelayfileVersion, + defaultRelayfileBridge, + relayfileIntegrationClientOptions, + resetSharedRelayfileControlPlaneClientForTests, +} from './integration.js'; import { RelayfileControlPlaneClient } from '@relayfile/client'; let socketSequence = 0; @@ -221,6 +227,45 @@ describe('assertRelayfileVersion', () => { }); }); +describe('default Relayfile integration bridge', () => { + it('constructs Relayfile clients with the exact 30-second request budget', () => { + expect(relayfileIntegrationClientOptions()).toEqual({ requestTimeoutMs: 30_000 }); + expect(relayfileIntegrationClientOptions({ socketPath: '/tmp/relayfile-test.sock' })).toEqual({ + requestTimeoutMs: 30_000, + socketPath: '/tmp/relayfile-test.sock', + }); + }); + + it('allows a real provider-status request to clear the former 10-second boundary', async () => { + resetSharedRelayfileControlPlaneClientForTests(); + await withControlPlaneServer( + (request, response) => { + if (request.method === 'GET' && request.url === '/v1/hello') { + writeJson(response, 200, { + daemonVersion: '0.10.27', + apiVersion: 3, + supportedApiVersions: [1, 2, 3], + }); + return; + } + if (request.method === 'GET' && request.url?.startsWith('/v1/integrations/provider-status?')) { + setTimeout(() => writeJson(response, 200, { provider: 'github', status: 'ready' }), 10_100); + return; + } + writeJson(response, 404, { error: { code: 'NOT_FOUND', message: 'not found' } }); + }, + async (socketPath) => { + try { + const bridge = defaultRelayfileBridge({ socketPath, autoStart: false }); + await expect(bridge.isConnected('github')).resolves.toBe(true); + } finally { + resetSharedRelayfileControlPlaneClientForTests(); + } + } + ); + }, 15_000); +}); + describe('relayfile control-plane hello negotiation', () => { it('discovers supported APIs without a version header, then uses v3 for normal requests', async () => { const requests: Array<{ method?: string; path?: string; version?: string }> = []; diff --git a/packages/cli/src/cli/commands/integration.ts b/packages/cli/src/cli/commands/integration.ts index 75789226c..592fff332 100644 --- a/packages/cli/src/cli/commands/integration.ts +++ b/packages/cli/src/cli/commands/integration.ts @@ -152,9 +152,29 @@ export type IntegrationCommandDependencies = SdkCommandDeps & { */ let sharedClient: RelayfileControlPlaneClient | undefined; const RELAYFILE_WEBHOOK_BINDINGS_API_VERSION = 3; +const RELAYFILE_INTEGRATION_REQUEST_TIMEOUT_MS = 30_000; + +export function relayfileIntegrationClientOptions( + options: RelayfileClientOptions = {} +): RelayfileClientOptions { + return { + requestTimeoutMs: RELAYFILE_INTEGRATION_REQUEST_TIMEOUT_MS, + ...options, + }; +} + +export function resetSharedRelayfileControlPlaneClientForTests(): void { + sharedClient = undefined; +} + function controlPlaneClient(options?: RelayfileClientOptions): RelayfileControlPlaneClient { - if (options) return new RelayfileControlPlaneClient(options); - if (!sharedClient) sharedClient = new RelayfileControlPlaneClient(); + if (options) return new RelayfileControlPlaneClient(relayfileIntegrationClientOptions(options)); + if (!sharedClient) { + // Provider status can include a Cloud request plus optional runtime + // enrichment. Keep subscription provisioning tolerant of a slow upstream + // while the daemon independently bounds that optional enrichment. + sharedClient = new RelayfileControlPlaneClient(relayfileIntegrationClientOptions()); + } return sharedClient; }