Files
gstack/browse/test/cli-loopback-no-proxy.test.ts
T
garrytan 616773158b G4: keep CLI-to-daemon loopback calls off HTTP_PROXY (#494)
The CLI appends 127.0.0.1, localhost and ::1 to the user's NO_PROXY (and
no_proxy), keeping existing entries, before its first fetch. Verified on
Bun 1.4.0: fetch honors NO_PROXY when it is set before the first request
and ignores later changes, so it is set at the top of main().
2026-10-03 20:53:42 +00:00

78 lines
3.6 KiB
TypeScript

/**
* G4 (#494): with HTTP_PROXY set, the CLI's loopback calls to its own daemon
* went through the proxy, so the daemon never looked healthy. The CLI appends
* loopback to NO_PROXY (keeping the user's entries) before its first fetch.
*/
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { withLoopbackNoProxy } from '../src/cli';
const CLI = path.resolve(import.meta.dir, '../src/cli.ts');
describe('withLoopbackNoProxy', () => {
test('appends loopback names to an existing list without replacing it', () => {
expect(withLoopbackNoProxy({ NO_PROXY: 'corp.internal, .example.com' })).toBe('corp.internal,.example.com,127.0.0.1,localhost,::1');
expect(withLoopbackNoProxy({ no_proxy: 'corp.internal' })).toBe('corp.internal,127.0.0.1,localhost,::1');
expect(withLoopbackNoProxy({})).toBe('127.0.0.1,localhost,::1');
expect(withLoopbackNoProxy({ NO_PROXY: 'localhost,127.0.0.1,::1' })).toBe('localhost,127.0.0.1,::1');
});
});
describe('CLI → daemon behind a proxy that fails every request', () => {
let scratch: string;
let proxy: ReturnType<typeof Bun.serve>;
let proxied = 0;
beforeEach(() => {
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-no-proxy-'));
proxied = 0;
proxy = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => { proxied++; return new Response('proxy refuses', { status: 502 }); } });
});
afterEach(() => {
try { proxy.stop(true); } catch {}
const stateFile = path.join(scratch, 'state', 'browse.json');
if (fs.existsSync(stateFile)) {
try { process.kill(JSON.parse(fs.readFileSync(stateFile, 'utf-8')).pid, 'SIGKILL'); } catch {}
}
fs.rmSync(scratch, { recursive: true, force: true });
});
test.skipIf(process.platform === 'win32')('status reaches the daemon directly and the daemon keeps the user NO_PROXY entries', async () => {
const stateFile = path.join(scratch, 'state', 'browse.json');
const stub = path.join(scratch, 'stub-server.ts');
fs.writeFileSync(stub, `
import * as fs from 'node:fs';
const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => Response.json({ status: 'healthy' }) });
fs.writeFileSync(process.env.BROWSE_STATE_FILE!, JSON.stringify({
pid: process.pid, port: server.port, token: 'no-proxy-test', mode: 'launched', noProxy: process.env.NO_PROXY,
}));
setTimeout(() => process.exit(0), 5000);
`);
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined && !/^(BROWSE_|GSTACK_|NO_PROXY$|no_proxy$|HTTPS?_PROXY$|https?_proxy$|ALL_PROXY$|all_proxy$)/.test(key)) env[key] = value;
}
Object.assign(env, {
HOME: scratch,
GSTACK_HOME: path.join(scratch, '.gstack'),
BROWSE_STATE_FILE: stateFile,
BROWSE_SERVER_SCRIPT: stub,
BROWSE_PARENT_PID: '0',
BROWSE_START_TIMEOUT: '8000',
HTTP_PROXY: `http://127.0.0.1:${proxy.port}`,
http_proxy: `http://127.0.0.1:${proxy.port}`,
NO_PROXY: 'corp.internal',
});
const cli = Bun.spawn([process.execPath, CLI, 'status'], { cwd: scratch, env, stdout: 'pipe', stderr: 'pipe' });
const timer = setTimeout(() => cli.kill('SIGKILL'), 30_000);
const [code, stdout, stderr] = await Promise.all([cli.exited, new Response(cli.stdout).text(), new Response(cli.stderr).text()]);
clearTimeout(timer);
expect(code, stdout + stderr).toBe(0);
expect(proxied).toBe(0);
expect(JSON.parse(fs.readFileSync(stateFile, 'utf-8')).noProxy).toBe('corp.internal,127.0.0.1,localhost,::1');
}, 45_000);
});