Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/utils/__tests__/claude-service-status.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EventEmitter } from 'events';
import {
afterEach,
describe,
expect,
it
Expand Down Expand Up @@ -182,6 +183,54 @@ describe('status page response handling', () => {
});
});

describe('proxy agent wiring', () => {
const ORIGINAL_HTTPS_PROXY = process.env.HTTPS_PROXY;

afterEach(() => {
if (ORIGINAL_HTTPS_PROXY === undefined) {
delete process.env.HTTPS_PROXY;
} else {
process.env.HTTPS_PROXY = ORIGINAL_HTTPS_PROXY;
}
});

function captureRequest(captured: { agent: unknown }): StatusPageRequestFn {
return (options, onResponse) => {
captured.agent = (options as { agent?: unknown }).agent;
const response = Object.assign(new EventEmitter(), {
statusCode: 200,
setEncoding: () => undefined
});
const request = Object.assign(new EventEmitter(), {
destroy: () => undefined,
end() {
onResponse(response);
response.emit('data', 'status body');
response.emit('end');
}
});

return request;
};
}

it('attaches a proxy agent when HTTPS_PROXY is set', async () => {
process.env.HTTPS_PROXY = 'http://proxy.example.test:8080';
const captured: { agent: unknown } = { agent: undefined };

await expect(__testing.fetchStatusPagePath('/test', captureRequest(captured))).resolves.toBe('status body');
expect(captured.agent).toBeTruthy();
});

it('sends no proxy agent without HTTPS_PROXY', async () => {
delete process.env.HTTPS_PROXY;
const captured: { agent: unknown } = { agent: undefined };

await expect(__testing.fetchStatusPagePath('/test', captureRequest(captured))).resolves.toBe('status body');
expect(captured.agent).toBeUndefined();
});
});

describe('claude-status prefetch predicates', () => {
it('detects claude-status widgets in configured lines', () => {
expect(hasClaudeStatusWidgets([[{ id: '1', type: 'model' }]])).toBe(false);
Expand Down
116 changes: 49 additions & 67 deletions src/utils/__tests__/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,8 @@ describe('terminal utils', () => {
it('returns width from the immediate parent tty when available', () => {
pinPosixPlatform();
mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return 'ttys001\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ttys001\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys001 size') {
Expand All @@ -83,29 +79,24 @@ describe('terminal utils', () => {

expect(getTerminalWidth()).toBe(120);
expect(mockExecFileSync.mock.calls.map(([file, args]) => `${file as string} ${(args as string[]).join(' ')}`)).toEqual([
`ps -o ppid= -p ${process.pid}`,
'ps -o tty= -p 1234',
`ps -o ppid= -o tty= -p ${process.ppid}`,
'stty -F /dev/ttys001 size'
]);
});

it('walks ancestor processes until it finds a valid tty', () => {
pinPosixPlatform();
mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return '??\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ?\n';
}

if (file === 'ps' && args.join(' ') === '-o ppid= -p 1234') {
return '5678\n';
if (file === 'ps' && args.join(' ') === '-o ppid= -o tty= -p 1234') {
return '5678 ??\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 5678') {
return ' ttys009 \n';
if (file === 'ps' && args.join(' ') === '-o ppid= -o tty= -p 5678') {
return '1 ttys009\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys009 size') {
Expand All @@ -118,16 +109,31 @@ describe('terminal utils', () => {
expect(getTerminalWidth()).toBe(104);
});

it('falls back through stty variants when the first form returns no value', () => {
it('reads the current generation TTY even when its PPID ends the walk', () => {
pinPosixPlatform();
// Simulates BSD/macOS, where `stty -F` exits with an error; `stty -f` succeeds.
// A PPID of 0 terminates the walk after this row, not before it:
// the parent's own width must still win over the tput fallback.
mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '0 ttys001\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys001 size') {
return '24 120\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return 'ttys003\n';
throw new Error(`Unexpected command: ${file} ${args.join(' ')}`);
});

expect(getTerminalWidth()).toBe(120);
});

it('falls back through stty variants when the first form returns no value', () => {
pinPosixPlatform();
// Simulates BSD/macOS, where `stty -F` exits with an error; `stty -f` succeeds.
mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ttys003\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys003 size') {
Expand All @@ -154,25 +160,21 @@ describe('terminal utils', () => {
expect(mockExecFileSync.mock.calls[1]?.[1]).toEqual(['cols']);
});

it('returns null when ancestor and fallback probes fail', () => {
it('returns null when ancestor and fallback probes all fail', () => {
pinPosixPlatform();
mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ttys001\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return 'ttys001\n';
if (file === 'ps' && args.join(' ') === '-o ppid= -o tty= -p 1234') {
return '0 ttys001\n';
}

if (file === 'stty') {
return 'not-a-number not-a-number\n';
}

if (file === 'ps' && args.join(' ') === '-o ppid= -p 1234') {
return '0\n';
}

if (file === 'tput') {
throw new Error('tput unavailable');
}
Expand All @@ -186,20 +188,16 @@ describe('terminal utils', () => {
it('detects availability when an ancestor tty probe succeeds', () => {
pinPosixPlatform();
mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return '??\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ?\n';
}

if (file === 'ps' && args.join(' ') === '-o ppid= -p 1234') {
return '5678\n';
if (file === 'ps' && args.join(' ') === '-o ppid= -o tty= -p 1234') {
return '5678 ??\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 5678') {
return 'ttys010\n';
if (file === 'ps' && args.join(' ') === '-o ppid= -o tty= -p 5678') {
return '1 ttys010\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys010 size') {
Expand Down Expand Up @@ -232,12 +230,8 @@ describe('terminal utils', () => {
process.env.CCSTATUSLINE_WIDTH = '0';

mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return 'ttys001\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ttys001\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys001 size') {
Expand All @@ -255,12 +249,8 @@ describe('terminal utils', () => {
process.env.CCSTATUSLINE_WIDTH = 'wide';

mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return 'ttys001\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ttys001\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys001 size') {
Expand Down Expand Up @@ -293,12 +283,8 @@ describe('terminal utils', () => {
it('probes only once across repeated calls when a width is found', () => {
pinPosixPlatform();
mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return 'ttys001\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ttys001\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys001 size') {
Expand Down Expand Up @@ -434,12 +420,8 @@ describe('terminal utils', () => {
it('never persists a discovered numeric width to the L2 cache', () => {
pinPosixPlatform();
mockExecFileSync.mockImplementation((file: string, args: string[]) => {
if (file === 'ps' && args.join(' ') === `-o ppid= -p ${process.pid}`) {
return '1234\n';
}

if (file === 'ps' && args.join(' ') === '-o tty= -p 1234') {
return 'ttys001\n';
if (file === 'ps' && args.join(' ') === `-o ppid= -o tty= -p ${process.ppid}`) {
return '1234 ttys001\n';
}

if (file === 'stty' && args.join(' ') === '-F /dev/ttys001 size') {
Expand Down
98 changes: 59 additions & 39 deletions src/utils/claude-service-status.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as fs from 'fs';
import * as https from 'https';
import { HttpsProxyAgent } from 'https-proxy-agent';
import * as os from 'os';
import * as path from 'path';
import { z } from 'zod';
Expand Down Expand Up @@ -282,55 +281,76 @@ type StatusPageRequestFn = (

const requestStatusPage: StatusPageRequestFn = (options, onResponse) => https.request(options, onResponse);

async function getStatusPageRequestOptions(): Promise<https.RequestOptions | null> {
const proxyUrl = getStatusPageProxyUrl();

try {
let agent: https.RequestOptions['agent'] | undefined;
if (proxyUrl) {
// Loaded on demand: the agent (and its transitive module graph) is
// only needed when a proxy is actually configured, and the
// statusline entry point re-runs on every repaint (#397).
const { HttpsProxyAgent: ProxyAgent } = await import('https-proxy-agent');
Comment thread
axisrow marked this conversation as resolved.
agent = new ProxyAgent(proxyUrl);
}

return {
hostname: STATUS_HOST,
path: '',
method: 'GET',
timeout: STATUS_TIMEOUT_MS,
...(agent ? { agent } : {})
};
} catch {
return null;
}
}

function fetchStatusPagePath(
pathName: string,
requestFn: StatusPageRequestFn = requestStatusPage
): Promise<string | null> {
return new Promise((resolve) => {
let settled = false;
return getStatusPageRequestOptions().then((baseOptions) => {
if (!baseOptions) {
return null;
}

const finish = (value: string | null) => {
if (settled) {
return;
}
settled = true;
resolve(value);
};
return new Promise<string | null>((resolve) => {
let settled = false;

let requestOptions: https.RequestOptions;
try {
const proxyUrl = getStatusPageProxyUrl();
requestOptions = {
hostname: STATUS_HOST,
path: pathName,
method: 'GET',
timeout: STATUS_TIMEOUT_MS,
...(proxyUrl ? { agent: new HttpsProxyAgent(proxyUrl) } : {})
const finish = (value: string | null) => {
if (settled) {
return;
}
settled = true;
resolve(value);
};
} catch {
finish(null);
return;
}

const request = requestFn(requestOptions, (response) => {
let data = '';
response.setEncoding('utf8');
response.on('data', (chunk: string) => {
data += chunk;
});
response.on('end', () => {
finish(response.statusCode === 200 && data ? data : null);
const requestOptions: https.RequestOptions = {
...baseOptions,
path: pathName
};

const request = requestFn(requestOptions, (response) => {
let data = '';
response.setEncoding('utf8');
response.on('data', (chunk: string) => {
data += chunk;
});
response.on('end', () => {
finish(response.statusCode === 200 && data ? data : null);
});
response.on('aborted', () => { finish(null); });
response.on('error', () => { finish(null); });
});
response.on('aborted', () => { finish(null); });
response.on('error', () => { finish(null); });
});

request.on('error', () => { finish(null); });
request.on('timeout', () => {
request.destroy();
finish(null);
request.on('error', () => { finish(null); });
request.on('timeout', () => {
request.destroy();
finish(null);
});
request.end();
});
request.end();
});
}

Expand Down
Loading
Loading