diff --git a/.github/workflows/smoke-copilot-byok.lock.yml b/.github/workflows/smoke-copilot-byok.lock.yml index 5bd706ec3..1202e84ce 100644 --- a/.github/workflows/smoke-copilot-byok.lock.yml +++ b/.github/workflows/smoke-copilot-byok.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"930022900b482b823f5dcd272263f643434b779a55a6411bb191aa8a618793f3","compiler_version":"v0.71.1","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f8f1a5a5de9f24cfafa723f59e0a78293b95ef935c2bde7d368d2d5c9b8028ee","compiler_version":"v0.71.1","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"239aec45b78c8799417efdd5bc6d8cc036629ec1","version":"v0.71.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.29"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.29"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.29"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.0","digest":"sha256:9c2228324fb1f26f39dc9471612e530ae3efc3156dac05efb2e8d212878d454d","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.0@sha256:9c2228324fb1f26f39dc9471612e530ae3efc3156dac05efb2e8d212878d454d"},{"image":"ghcr.io/github/github-mcp-server:v1.0.2","digest":"sha256:26db03408086a99cf1916348dcc4f9614206658f9082a8060dc7c81ad787f4ba","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.2@sha256:26db03408086a99cf1916348dcc4f9614206658f9082a8060dc7c81ad787f4ba"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -398,7 +398,7 @@ jobs: GH_TOKEN: ${{ github.token }} id: smoke-data name: Pre-compute BYOK smoke test data - run: "echo \"::group::Verify BYOK configuration\"\necho \"COPILOT_API_KEY=${COPILOT_API_KEY:+set (${#COPILOT_API_KEY} chars)}\"\necho \"COPILOT_API_TARGET=${COPILOT_API_TARGET:-api.githubcopilot.com (default)}\"\necho \"::endgroup::\"\n\necho \"::group::Fetching last 2 merged PRs\"\nPR_DATA=$(gh pr list --repo \"$GITHUB_REPOSITORY\" --state merged --limit 2 \\\n --json number,title,author,mergedAt \\\n --jq '.[] | \"PR #\\(.number): \\(.title) (by @\\(.author.login), merged \\(.mergedAt))\"')\necho \"$PR_DATA\"\necho \"::endgroup::\"\n\necho \"::group::GitHub.com connectivity check\"\nHTTP_CODE=$(curl -s -o /dev/null -w \"%{http_code}\" --max-time 10 https://github.com)\necho \"github.com returned HTTP $HTTP_CODE\"\necho \"::endgroup::\"\n\necho \"::group::File write/read test\"\nTEST_DIR=\"/tmp/gh-aw/agent\"\nTEST_FILE=\"$TEST_DIR/smoke-test-copilot-byok-${GITHUB_RUN_ID}.txt\"\nmkdir -p \"$TEST_DIR\"\necho \"BYOK smoke test passed at $(date)\" > \"$TEST_FILE\"\nFILE_CONTENT=$(cat \"$TEST_FILE\")\necho \"Wrote and read back: $FILE_CONTENT\"\necho \"::endgroup::\"\n\n{\n echo \"SMOKE_PR_DATA<> \"$GITHUB_OUTPUT\"\n" + run: "echo \"::group::Verify BYOK configuration\"\necho \"COPILOT_API_KEY=${COPILOT_API_KEY:+set (${#COPILOT_API_KEY} chars)}\"\necho \"COPILOT_API_TARGET=${COPILOT_API_TARGET:-api.githubcopilot.com (default)}\"\necho \"::endgroup::\"\n\necho \"::group::Fetching last 2 merged PRs\"\nPR_DATA=$(gh pr list --repo \"$GITHUB_REPOSITORY\" --state merged --limit 2 \\\n --json number,title,author,mergedAt \\\n --jq '.[] | \"PR #\\(.number): \\(.title) (by @\\(.author.login), merged \\(.mergedAt))\"' \\\n || echo \"(PR fetch failed)\")\necho \"$PR_DATA\"\necho \"::endgroup::\"\n\necho \"::group::GitHub.com connectivity check\"\nHTTP_CODE=$(curl -s -o /dev/null -w \"%{http_code}\" --max-time 10 https://github.com || echo \"000\")\necho \"github.com returned HTTP $HTTP_CODE\"\necho \"::endgroup::\"\n\necho \"::group::File write/read test\"\nTEST_DIR=\"/tmp/gh-aw/agent\"\nTEST_FILE=\"$TEST_DIR/smoke-test-copilot-byok-${GITHUB_RUN_ID}.txt\"\nmkdir -p \"$TEST_DIR\"\necho \"BYOK smoke test passed at $(date)\" > \"$TEST_FILE\"\nFILE_CONTENT=$(cat \"$TEST_FILE\")\necho \"Wrote and read back: $FILE_CONTENT\"\necho \"::endgroup::\"\n\n{\n echo \"SMOKE_PR_DATA<> \"$GITHUB_OUTPUT\"\n" - name: Configure Git credentials env: diff --git a/.github/workflows/smoke-copilot-byok.md b/.github/workflows/smoke-copilot-byok.md index e18398154..9937139a4 100644 --- a/.github/workflows/smoke-copilot-byok.md +++ b/.github/workflows/smoke-copilot-byok.md @@ -55,12 +55,13 @@ steps: echo "::group::Fetching last 2 merged PRs" PR_DATA=$(gh pr list --repo "$GITHUB_REPOSITORY" --state merged --limit 2 \ --json number,title,author,mergedAt \ - --jq '.[] | "PR #\(.number): \(.title) (by @\(.author.login), merged \(.mergedAt))"') + --jq '.[] | "PR #\(.number): \(.title) (by @\(.author.login), merged \(.mergedAt))"' \ + || echo "(PR fetch failed)") echo "$PR_DATA" echo "::endgroup::" echo "::group::GitHub.com connectivity check" - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://github.com) + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://github.com || echo "000") echo "github.com returned HTTP $HTTP_CODE" echo "::endgroup::" diff --git a/containers/api-proxy/providers/anthropic.js b/containers/api-proxy/providers/anthropic.js index 1c482eab8..900666a98 100644 --- a/containers/api-proxy/providers/anthropic.js +++ b/containers/api-proxy/providers/anthropic.js @@ -73,7 +73,17 @@ function createAnthropicAdapter(env, deps = {}) { name: 'anthropic', port: 10001, isManagementPort: false, - alwaysBind: false, + + /** + * Port 10001 always starts so agents get a clear 503 "not configured" + * error rather than a silent connection-refused. + */ + alwaysBind: true, + + /** + * The stub server does NOT count toward the startup validation latch — + * only the fully-configured server (when ANTHROPIC_API_KEY is set) does. + */ get participatesInValidation() { return this.isEnabled(); }, isEnabled() { return !!apiKey; }, @@ -159,6 +169,29 @@ function createAnthropicAdapter(env, deps = {}) { }; }, + /** Response returned for all requests when no ANTHROPIC_API_KEY is configured. */ + getUnconfiguredResponse() { + return { + statusCode: 503, + body: { + error: { + message: 'Credentials for Anthropic (port 10001) are not configured. Set ANTHROPIC_API_KEY to enable this provider.', + type: 'provider_not_configured', + provider: 'anthropic', + port: 10001, + }, + }, + }; + }, + + /** /health response when not configured. */ + getUnconfiguredHealthResponse() { + return { + statusCode: 503, + body: { status: 'not_configured', service: 'awf-api-proxy-anthropic', error: 'ANTHROPIC_API_KEY not configured in api-proxy sidecar' }, + }; + }, + // Exposed for introspection (logging, tests) _autoCache: autoCache, _cacheTailTtl: cacheTailTtl, diff --git a/containers/api-proxy/providers/copilot.js b/containers/api-proxy/providers/copilot.js index a25ed1adf..789349b6e 100644 --- a/containers/api-proxy/providers/copilot.js +++ b/containers/api-proxy/providers/copilot.js @@ -157,7 +157,17 @@ function createCopilotAdapter(env, deps = {}) { name: 'copilot', port: 10002, isManagementPort: false, - alwaysBind: false, + + /** + * Port 10002 always starts so agents get a clear 503 "not configured" + * error rather than a silent connection-refused. + */ + alwaysBind: true, + + /** + * The stub server does NOT count toward the startup validation latch — + * only the fully-configured server (when credentials are present) does. + */ get participatesInValidation() { return this.isEnabled(); }, isEnabled() { return !!authToken; }, @@ -252,6 +262,29 @@ function createCopilotAdapter(env, deps = {}) { }; }, + /** Response returned for all requests when no Copilot credentials are configured. */ + getUnconfiguredResponse() { + return { + statusCode: 503, + body: { + error: { + message: 'Credentials for GitHub Copilot (port 10002) are not configured. Set COPILOT_GITHUB_TOKEN or COPILOT_API_KEY to enable this provider.', + type: 'provider_not_configured', + provider: 'copilot', + port: 10002, + }, + }, + }; + }, + + /** /health response when not configured. */ + getUnconfiguredHealthResponse() { + return { + statusCode: 503, + body: { status: 'not_configured', service: 'awf-api-proxy-copilot', error: 'COPILOT_GITHUB_TOKEN or COPILOT_API_KEY not configured in api-proxy sidecar' }, + }; + }, + // Exposed for introspection / testing _githubToken: githubToken, _apiKey: apiKey, diff --git a/containers/api-proxy/providers/opencode.js b/containers/api-proxy/providers/opencode.js index f4cd21bdb..8252562fc 100644 --- a/containers/api-proxy/providers/opencode.js +++ b/containers/api-proxy/providers/opencode.js @@ -101,7 +101,17 @@ function createOpenCodeAdapter(env, { candidateAdapters = [] } = {}) { name: 'opencode', port: 10004, isManagementPort: false, - alwaysBind: false, + + /** + * Port 10004 always starts so agents get a clear 503 "not configured" + * error rather than a silent connection-refused. + */ + alwaysBind: true, + + /** + * The stub server does NOT count toward the startup validation latch — + * only the fully-configured server (when enabled and a candidate is active) does. + */ get participatesInValidation() { return this.isEnabled(); }, isEnabled() { return enabled && !!resolveActiveAdapter(); }, @@ -168,6 +178,45 @@ function createOpenCodeAdapter(env, { candidateAdapters = [] } = {}) { }; }, + /** Response returned for all requests when OpenCode is not configured. */ + getUnconfiguredResponse() { + if (!enabled) { + return { + statusCode: 503, + body: { + error: { + message: 'OpenCode proxy (port 10004) is not enabled. Set AWF_ENABLE_OPENCODE=true and configure at least one of OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_GITHUB_TOKEN, or COPILOT_API_KEY.', + type: 'provider_not_configured', + provider: 'opencode', + port: 10004, + }, + }, + }; + } + return { + statusCode: 503, + body: { + error: { + message: 'Credentials for OpenCode (port 10004) are not configured. Set at least one of OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_GITHUB_TOKEN, or COPILOT_API_KEY.', + type: 'provider_not_configured', + provider: 'opencode', + port: 10004, + }, + }, + }; + }, + + /** /health response when not configured. */ + getUnconfiguredHealthResponse() { + const reason = !enabled + ? 'AWF_ENABLE_OPENCODE not set to true' + : 'no candidate provider credentials configured'; + return { + statusCode: 503, + body: { status: 'not_configured', service: 'awf-api-proxy-opencode', error: `OpenCode proxy not configured: ${reason}` }, + }; + }, + // Exposed for introspection / testing _startupActiveAdapterName: startupActiveAdapter?.name || null, _candidateAdapters: candidateAdapters, diff --git a/containers/api-proxy/server.js b/containers/api-proxy/server.js index d7a3e1698..358a7eb59 100644 --- a/containers/api-proxy/server.js +++ b/containers/api-proxy/server.js @@ -1129,6 +1129,13 @@ function createProviderServer(adapter) { return; } + // ── Provider-local reflect endpoint ────────────────────────────────────── + if (req.url === '/reflect' && req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(reflectEndpoints())); + return; + } + // ── Disabled adapter: return provider-specific error ───────────────────── if (!adapter.isEnabled()) { const response = adapter.getUnconfiguredResponse diff --git a/containers/api-proxy/server.test.js b/containers/api-proxy/server.test.js index 69eb1f5e3..15112357f 100644 --- a/containers/api-proxy/server.test.js +++ b/containers/api-proxy/server.test.js @@ -2401,6 +2401,77 @@ describe('createProviderServer', () => { expect(body.error).toMatch(/test-no-stub.*not configured/); }); + // ── /reflect endpoint — non-management port ────────────────────────────── + + it('returns 200 /reflect on a non-management port (enabled adapter)', async () => { + const adapter = { + name: 'test-reflect-enabled', port: 0, isManagementPort: false, alwaysBind: true, + participatesInValidation: false, + isEnabled: () => true, + getTargetHost: () => 'api.example.com', + getBasePath: () => '', + getAuthHeaders: () => ({}), + getBodyTransform: () => null, + getReflectionInfo: () => ({ + provider: 'test-reflect-enabled', port: 0, base_url: 'http://api-proxy:0', + configured: true, models_cache_key: null, models_url: null, + }), + }; + const port = await startAdapter(adapter); + const { status, body } = await fetch(port, '/reflect'); + expect(status).toBe(200); + expect(body).toHaveProperty('endpoints'); + expect(body).toHaveProperty('models_fetch_complete'); + }); + + it('returns 200 /reflect on a non-management port (disabled/unconfigured adapter)', async () => { + const adapter = { + name: 'test-reflect-disabled', port: 0, isManagementPort: false, alwaysBind: true, + participatesInValidation: false, + isEnabled: () => false, + getTargetHost: () => '', + getBasePath: () => '', + getAuthHeaders: () => ({}), + getBodyTransform: () => null, + getUnconfiguredResponse: () => ({ statusCode: 503, body: { error: 'not configured' } }), + getReflectionInfo: () => ({ + provider: 'test-reflect-disabled', port: 0, base_url: 'http://api-proxy:0', + configured: false, models_cache_key: null, models_url: null, + }), + }; + const port = await startAdapter(adapter); + const { status, body } = await fetch(port, '/reflect'); + // /reflect should return 200 even for unconfigured adapters + expect(status).toBe(200); + expect(body).toHaveProperty('endpoints'); + }); + + // ── /reflect not intercepted before unconfigured-stub check ──────────────── + + it('does not intercept /reflect as a proxy request on disabled adapters', async () => { + const adapter = { + name: 'test-no-intercept', port: 0, isManagementPort: false, alwaysBind: true, + participatesInValidation: false, + isEnabled: () => false, + getTargetHost: () => '', + getBasePath: () => '', + getAuthHeaders: () => ({}), + getBodyTransform: () => null, + getUnconfiguredResponse: () => ({ statusCode: 503, body: { error: 'not configured' } }), + getReflectionInfo: () => ({ + provider: 'test-no-intercept', port: 0, base_url: 'http://api-proxy:0', + configured: false, models_cache_key: null, models_url: null, + }), + }; + const port = await startAdapter(adapter); + // /reflect should return 200, not the unconfigured 503 + const { status } = await fetch(port, '/reflect'); + expect(status).toBe(200); + // Other paths should still return the unconfigured response + const { status: proxyStatus } = await fetch(port, '/v1/chat/completions', { method: 'POST', body: '{}' }); + expect(proxyStatus).toBe(503); + }); + // ── URL transform ───────────────────────────────────────────────────────── it('applies transformRequestUrl before proxying', async () => { @@ -2597,3 +2668,108 @@ describe('createProviderServer', () => { expect(authErrLog).toBeUndefined(); }); }); + +// ── Provider adapter alwaysBind tests ───────────────────────────────────────── +// +// These tests verify that anthropic, copilot, and opencode always bind and +// return clear errors when credentials are absent. +// + +const { createAnthropicAdapter } = require('./providers/anthropic'); +const { createOpenCodeAdapter } = require('./providers/opencode'); + +describe('provider adapter alwaysBind', () => { + it('anthropic alwaysBind is true', () => { + const adapter = createAnthropicAdapter({}); + expect(adapter.alwaysBind).toBe(true); + }); + + it('copilot alwaysBind is true', () => { + const adapter = createCopilotAdapter({}); + expect(adapter.alwaysBind).toBe(true); + }); + + it('opencode alwaysBind is true', () => { + const adapter = createOpenCodeAdapter({}); + expect(adapter.alwaysBind).toBe(true); + }); + + it('anthropic getUnconfiguredResponse returns 503 with structured error', () => { + const adapter = createAnthropicAdapter({}); + const { statusCode, body } = adapter.getUnconfiguredResponse(); + expect(statusCode).toBe(503); + expect(body.error.type).toBe('provider_not_configured'); + expect(body.error.provider).toBe('anthropic'); + expect(body.error.port).toBe(10001); + expect(body.error.message).toMatch(/ANTHROPIC_API_KEY/); + }); + + it('anthropic getUnconfiguredHealthResponse returns 503 with not_configured status', () => { + const adapter = createAnthropicAdapter({}); + const { statusCode, body } = adapter.getUnconfiguredHealthResponse(); + expect(statusCode).toBe(503); + expect(body.status).toBe('not_configured'); + expect(body.service).toBe('awf-api-proxy-anthropic'); + expect(body.error).toMatch(/ANTHROPIC_API_KEY/); + }); + + it('copilot getUnconfiguredResponse returns 503 with structured error', () => { + const adapter = createCopilotAdapter({}); + const { statusCode, body } = adapter.getUnconfiguredResponse(); + expect(statusCode).toBe(503); + expect(body.error.type).toBe('provider_not_configured'); + expect(body.error.provider).toBe('copilot'); + expect(body.error.port).toBe(10002); + expect(body.error.message).toMatch(/COPILOT_GITHUB_TOKEN/); + }); + + it('copilot getUnconfiguredHealthResponse returns 503 with not_configured status', () => { + const adapter = createCopilotAdapter({}); + const { statusCode, body } = adapter.getUnconfiguredHealthResponse(); + expect(statusCode).toBe(503); + expect(body.status).toBe('not_configured'); + expect(body.service).toBe('awf-api-proxy-copilot'); + expect(body.error).toMatch(/COPILOT_GITHUB_TOKEN/); + }); + + it('opencode getUnconfiguredResponse returns 503 mentioning AWF_ENABLE_OPENCODE when not enabled', () => { + const adapter = createOpenCodeAdapter({}); + const { statusCode, body } = adapter.getUnconfiguredResponse(); + expect(statusCode).toBe(503); + expect(body.error.type).toBe('provider_not_configured'); + expect(body.error.provider).toBe('opencode'); + expect(body.error.port).toBe(10004); + expect(body.error.message).toMatch(/AWF_ENABLE_OPENCODE/); + }); + + it('opencode getUnconfiguredResponse returns 503 mentioning credentials when enabled but no candidates', () => { + const adapter = createOpenCodeAdapter({ AWF_ENABLE_OPENCODE: 'true' }); + const { statusCode, body } = adapter.getUnconfiguredResponse(); + expect(statusCode).toBe(503); + expect(body.error.type).toBe('provider_not_configured'); + expect(body.error.message).toMatch(/OPENAI_API_KEY/); + expect(body.error.message).toMatch(/COPILOT_API_KEY/); + }); + + it('opencode getUnconfiguredResponse mentions COPILOT_API_KEY when not enabled', () => { + const adapter = createOpenCodeAdapter({}); + const { body } = adapter.getUnconfiguredResponse(); + expect(body.error.message).toMatch(/COPILOT_API_KEY/); + }); + + it('opencode getUnconfiguredHealthResponse returns 503 with not_configured status (disabled)', () => { + const adapter = createOpenCodeAdapter({}); + const { statusCode, body } = adapter.getUnconfiguredHealthResponse(); + expect(statusCode).toBe(503); + expect(body.status).toBe('not_configured'); + expect(body.service).toBe('awf-api-proxy-opencode'); + expect(body.error).toMatch(/AWF_ENABLE_OPENCODE/); + }); + + it('opencode getUnconfiguredHealthResponse mentions credentials when enabled but no candidates', () => { + const adapter = createOpenCodeAdapter({ AWF_ENABLE_OPENCODE: 'true' }); + const { statusCode, body } = adapter.getUnconfiguredHealthResponse(); + expect(statusCode).toBe(503); + expect(body.error).toMatch(/no candidate provider credentials/); + }); +}); diff --git a/src/docker-manager-compose.test.ts b/src/docker-manager-compose.test.ts index f6d3da5ec..60bb1d49c 100644 --- a/src/docker-manager-compose.test.ts +++ b/src/docker-manager-compose.test.ts @@ -3160,7 +3160,7 @@ describe('docker-manager generateDockerCompose', () => { const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); const proxy = result.services['cli-proxy']; const env = proxy.environment as Record; - expect(env.AWF_DIFC_PROXY_HOST).toBe('[::1]'); + expect(env.AWF_DIFC_PROXY_HOST).toBe('::1'); expect(env.AWF_DIFC_PROXY_PORT).toBe('18443'); }); diff --git a/src/host-env.ts b/src/host-env.ts index 099a04342..0c903ff0f 100644 --- a/src/host-env.ts +++ b/src/host-env.ts @@ -484,16 +484,20 @@ export function parseDifcProxyHost(value: string): { host: string; port: string if (!trimmed) { return { host: 'host.docker.internal', port: '18443' }; } - // Use URL to parse host:port correctly (handles IPv6 brackets) - const hasScheme = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmed); - const candidate = hasScheme ? trimmed : `tcp://${trimmed}`; + // Use URL to parse host:port correctly (handles IPv6 brackets). + // Always normalise to tcp:// so the WHATWG URL parser never drops a port + // that happens to be the default for a scheme (e.g. 443 for https://). + const hostPart = trimmed.replace(/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//, ''); + const candidate = `tcp://${hostPart}`; let parsed: URL; try { parsed = new URL(candidate); } catch { throw new Error(`Invalid --difc-proxy-host value: "${value}". Expected host:port format.`); } - const host = parsed.hostname || 'host.docker.internal'; + // URL.hostname returns IPv6 addresses with surrounding brackets; strip them. + const rawHost = parsed.hostname || 'host.docker.internal'; + const host = rawHost.replace(/^\[(.+)\]$/, '$1'); const port = parsed.port || '18443'; if (!/^\d+$/.test(port)) { throw new Error(`Invalid --difc-proxy-host port: "${port}". Must be a number.`); diff --git a/tests/integration/protocol-support.test.ts b/tests/integration/protocol-support.test.ts index e4f5cff3d..b46db9bd9 100644 --- a/tests/integration/protocol-support.test.ts +++ b/tests/integration/protocol-support.test.ts @@ -117,7 +117,7 @@ describe('Protocol Support', () => { describe('Connection Headers', () => { test('should pass custom headers', async () => { const result = await runner.runWithSudo( - 'curl -fsS -H "Accept: application/json" https://api.github.com/zen', + 'curl -fsS -H "Accept: application/vnd.github+json" https://api.github.com/zen', { allowDomains: ['github.com'], logLevel: 'debug',