mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 14:22:02 +00:00
- Modified command arguments in tests to include '--add-dir' for better context. - Updated error messages for authentication and timeout scenarios to provide clearer guidance. - Adjusted timer values in tests to align with implementation delays, ensuring accurate simulation of usage data retrieval.
716 lines
22 KiB
TypeScript
716 lines
22 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { ClaudeUsageService } from '@/services/claude-usage-service.js';
|
|
import { spawn } from 'child_process';
|
|
import * as pty from 'node-pty';
|
|
import * as os from 'os';
|
|
|
|
vi.mock('child_process');
|
|
vi.mock('node-pty');
|
|
vi.mock('os');
|
|
|
|
describe('claude-usage-service.ts', () => {
|
|
let service: ClaudeUsageService;
|
|
let mockSpawnProcess: any;
|
|
let mockPtyProcess: any;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
service = new ClaudeUsageService();
|
|
|
|
// Mock spawn process for isAvailable and Mac commands
|
|
mockSpawnProcess = {
|
|
on: vi.fn(),
|
|
kill: vi.fn(),
|
|
stdout: {
|
|
on: vi.fn(),
|
|
},
|
|
stderr: {
|
|
on: vi.fn(),
|
|
},
|
|
};
|
|
|
|
// Mock PTY process for Windows
|
|
mockPtyProcess = {
|
|
onData: vi.fn(),
|
|
onExit: vi.fn(),
|
|
write: vi.fn(),
|
|
kill: vi.fn(),
|
|
};
|
|
|
|
vi.mocked(spawn).mockReturnValue(mockSpawnProcess as any);
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPtyProcess);
|
|
});
|
|
|
|
describe('isAvailable', () => {
|
|
it('should return true when Claude CLI is available', async () => {
|
|
vi.mocked(os.platform).mockReturnValue('darwin');
|
|
|
|
// Simulate successful which/where command
|
|
mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
|
|
if (event === 'close') {
|
|
callback(0); // Exit code 0 = found
|
|
}
|
|
return mockSpawnProcess;
|
|
});
|
|
|
|
const result = await service.isAvailable();
|
|
|
|
expect(result).toBe(true);
|
|
expect(spawn).toHaveBeenCalledWith('which', ['claude']);
|
|
});
|
|
|
|
it('should return false when Claude CLI is not available', async () => {
|
|
vi.mocked(os.platform).mockReturnValue('darwin');
|
|
|
|
mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
|
|
if (event === 'close') {
|
|
callback(1); // Exit code 1 = not found
|
|
}
|
|
return mockSpawnProcess;
|
|
});
|
|
|
|
const result = await service.isAvailable();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should return false on error', async () => {
|
|
vi.mocked(os.platform).mockReturnValue('darwin');
|
|
|
|
mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
|
|
if (event === 'error') {
|
|
callback(new Error('Command failed'));
|
|
}
|
|
return mockSpawnProcess;
|
|
});
|
|
|
|
const result = await service.isAvailable();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("should use 'where' command on Windows", async () => {
|
|
vi.mocked(os.platform).mockReturnValue('win32');
|
|
const windowsService = new ClaudeUsageService(); // Create new service after platform mock
|
|
|
|
mockSpawnProcess.on.mockImplementation((event: string, callback: Function) => {
|
|
if (event === 'close') {
|
|
callback(0);
|
|
}
|
|
return mockSpawnProcess;
|
|
});
|
|
|
|
await windowsService.isAvailable();
|
|
|
|
expect(spawn).toHaveBeenCalledWith('where', ['claude']);
|
|
});
|
|
});
|
|
|
|
describe('stripAnsiCodes', () => {
|
|
it('should strip ANSI color codes from text', () => {
|
|
const service = new ClaudeUsageService();
|
|
const input = '\x1B[31mRed text\x1B[0m Normal text';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.stripAnsiCodes(input);
|
|
|
|
expect(result).toBe('Red text Normal text');
|
|
});
|
|
|
|
it('should handle text without ANSI codes', () => {
|
|
const service = new ClaudeUsageService();
|
|
const input = 'Plain text';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.stripAnsiCodes(input);
|
|
|
|
expect(result).toBe('Plain text');
|
|
});
|
|
});
|
|
|
|
describe('parseResetTime', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2025-01-15T10:00:00Z'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should parse duration format with hours and minutes', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Resets in 2h 15m';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'session');
|
|
|
|
const expected = new Date('2025-01-15T12:15:00Z');
|
|
expect(new Date(result)).toEqual(expected);
|
|
});
|
|
|
|
it('should parse duration format with only minutes', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Resets in 30m';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'session');
|
|
|
|
const expected = new Date('2025-01-15T10:30:00Z');
|
|
expect(new Date(result)).toEqual(expected);
|
|
});
|
|
|
|
it('should parse simple time format (AM)', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Resets 11am';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'session');
|
|
|
|
// Should be today at 11am, or tomorrow if already passed
|
|
const resultDate = new Date(result);
|
|
expect(resultDate.getHours()).toBe(11);
|
|
expect(resultDate.getMinutes()).toBe(0);
|
|
});
|
|
|
|
it('should parse simple time format (PM)', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Resets 3pm';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'session');
|
|
|
|
const resultDate = new Date(result);
|
|
expect(resultDate.getHours()).toBe(15);
|
|
expect(resultDate.getMinutes()).toBe(0);
|
|
});
|
|
|
|
it('should parse date format with month, day, and time', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Resets Dec 22 at 8pm';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'weekly');
|
|
|
|
const resultDate = new Date(result);
|
|
expect(resultDate.getMonth()).toBe(11); // December = 11
|
|
expect(resultDate.getDate()).toBe(22);
|
|
expect(resultDate.getHours()).toBe(20);
|
|
});
|
|
|
|
it('should parse date format with comma separator', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Resets Jan 15, 3:30pm';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'weekly');
|
|
|
|
const resultDate = new Date(result);
|
|
expect(resultDate.getMonth()).toBe(0); // January = 0
|
|
expect(resultDate.getDate()).toBe(15);
|
|
expect(resultDate.getHours()).toBe(15);
|
|
expect(resultDate.getMinutes()).toBe(30);
|
|
});
|
|
|
|
it('should handle 12am correctly', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Resets 12am';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'session');
|
|
|
|
const resultDate = new Date(result);
|
|
expect(resultDate.getHours()).toBe(0);
|
|
});
|
|
|
|
it('should handle 12pm correctly', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Resets 12pm';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'session');
|
|
|
|
const resultDate = new Date(result);
|
|
expect(resultDate.getHours()).toBe(12);
|
|
});
|
|
|
|
it('should return default reset time for unparseable text', () => {
|
|
const service = new ClaudeUsageService();
|
|
const text = 'Invalid reset text';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseResetTime(text, 'session');
|
|
// @ts-expect-error - accessing private method for testing
|
|
const defaultResult = service.getDefaultResetTime('session');
|
|
|
|
expect(result).toBe(defaultResult);
|
|
});
|
|
});
|
|
|
|
describe('getDefaultResetTime', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2025-01-15T10:00:00Z')); // Wednesday
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should return session default (5 hours from now)', () => {
|
|
const service = new ClaudeUsageService();
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.getDefaultResetTime('session');
|
|
|
|
const expected = new Date('2025-01-15T15:00:00Z');
|
|
expect(new Date(result)).toEqual(expected);
|
|
});
|
|
|
|
it('should return weekly default (next Monday at noon)', () => {
|
|
const service = new ClaudeUsageService();
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.getDefaultResetTime('weekly');
|
|
|
|
const resultDate = new Date(result);
|
|
// Next Monday from Wednesday should be 5 days away
|
|
expect(resultDate.getDay()).toBe(1); // Monday
|
|
expect(resultDate.getHours()).toBe(12);
|
|
expect(resultDate.getMinutes()).toBe(59);
|
|
});
|
|
});
|
|
|
|
describe('parseSection', () => {
|
|
it('should parse section with percentage left', () => {
|
|
const service = new ClaudeUsageService();
|
|
const lines = ['Current session', '████████████████░░░░ 65% left', 'Resets in 2h 15m'];
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseSection(lines, 'Current session', 'session');
|
|
|
|
expect(result.percentage).toBe(35); // 100 - 65 = 35% used
|
|
expect(result.resetText).toBe('Resets in 2h 15m');
|
|
});
|
|
|
|
it('should parse section with percentage used', () => {
|
|
const service = new ClaudeUsageService();
|
|
const lines = [
|
|
'Current week (all models)',
|
|
'██████████░░░░░░░░░░ 40% used',
|
|
'Resets Jan 15, 3:30pm',
|
|
];
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseSection(lines, 'Current week (all models)', 'weekly');
|
|
|
|
expect(result.percentage).toBe(40); // Already in % used
|
|
});
|
|
|
|
it('should return zero percentage when section not found', () => {
|
|
const service = new ClaudeUsageService();
|
|
const lines = ['Some other text', 'No matching section'];
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseSection(lines, 'Current session', 'session');
|
|
|
|
expect(result.percentage).toBe(0);
|
|
});
|
|
|
|
it('should strip timezone from reset text', () => {
|
|
const service = new ClaudeUsageService();
|
|
const lines = ['Current session', '65% left', 'Resets 3pm (America/Los_Angeles)'];
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseSection(lines, 'Current session', 'session');
|
|
|
|
expect(result.resetText).toBe('Resets 3pm');
|
|
expect(result.resetText).not.toContain('America/Los_Angeles');
|
|
});
|
|
|
|
it('should handle case-insensitive section matching', () => {
|
|
const service = new ClaudeUsageService();
|
|
const lines = ['CURRENT SESSION', '65% left', 'Resets in 2h'];
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseSection(lines, 'current session', 'session');
|
|
|
|
expect(result.percentage).toBe(35);
|
|
});
|
|
});
|
|
|
|
describe('parseUsageOutput', () => {
|
|
it('should parse complete usage output', () => {
|
|
const service = new ClaudeUsageService();
|
|
const output = `
|
|
Claude Code v1.0.27
|
|
|
|
Current session
|
|
████████████████░░░░ 65% left
|
|
Resets in 2h 15m
|
|
|
|
Current week (all models)
|
|
██████████░░░░░░░░░░ 35% left
|
|
Resets Jan 15, 3:30pm (America/Los_Angeles)
|
|
|
|
Current week (Sonnet only)
|
|
████████████████████ 80% left
|
|
Resets Jan 15, 3:30pm (America/Los_Angeles)
|
|
`;
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseUsageOutput(output);
|
|
|
|
expect(result.sessionPercentage).toBe(35); // 100 - 65
|
|
expect(result.weeklyPercentage).toBe(65); // 100 - 35
|
|
expect(result.sonnetWeeklyPercentage).toBe(20); // 100 - 80
|
|
expect(result.sessionResetText).toContain('Resets in 2h 15m');
|
|
expect(result.weeklyResetText).toContain('Resets Jan 15, 3:30pm');
|
|
expect(result.userTimezone).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
});
|
|
|
|
it('should handle output with ANSI codes', () => {
|
|
const service = new ClaudeUsageService();
|
|
const output = `
|
|
\x1B[1mClaude Code v1.0.27\x1B[0m
|
|
|
|
\x1B[1mCurrent session\x1B[0m
|
|
\x1B[32m████████████████░░░░\x1B[0m 65% left
|
|
Resets in 2h 15m
|
|
`;
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseUsageOutput(output);
|
|
|
|
expect(result.sessionPercentage).toBe(35);
|
|
});
|
|
|
|
it('should handle Opus section name', () => {
|
|
const service = new ClaudeUsageService();
|
|
const output = `
|
|
Current session
|
|
65% left
|
|
Resets in 2h
|
|
|
|
Current week (all models)
|
|
35% left
|
|
Resets Jan 15, 3pm
|
|
|
|
Current week (Opus)
|
|
90% left
|
|
Resets Jan 15, 3pm
|
|
`;
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseUsageOutput(output);
|
|
|
|
expect(result.sonnetWeeklyPercentage).toBe(10); // 100 - 90
|
|
});
|
|
|
|
it('should set default values for missing sections', () => {
|
|
const service = new ClaudeUsageService();
|
|
const output = 'Claude Code v1.0.27';
|
|
// @ts-expect-error - accessing private method for testing
|
|
const result = service.parseUsageOutput(output);
|
|
|
|
expect(result.sessionPercentage).toBe(0);
|
|
expect(result.weeklyPercentage).toBe(0);
|
|
expect(result.sonnetWeeklyPercentage).toBe(0);
|
|
expect(result.sessionTokensUsed).toBe(0);
|
|
expect(result.sessionLimit).toBe(0);
|
|
expect(result.costUsed).toBeNull();
|
|
expect(result.costLimit).toBeNull();
|
|
expect(result.costCurrency).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('executeClaudeUsageCommandMac', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(os.platform).mockReturnValue('darwin');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ HOME: '/Users/testuser' });
|
|
});
|
|
|
|
it('should execute expect script and return output', async () => {
|
|
const mockOutput = `
|
|
Current session
|
|
65% left
|
|
Resets in 2h
|
|
`;
|
|
|
|
let stdoutCallback: Function;
|
|
let closeCallback: Function;
|
|
|
|
mockSpawnProcess.stdout = {
|
|
on: vi.fn((event: string, callback: Function) => {
|
|
if (event === 'data') {
|
|
stdoutCallback = callback;
|
|
}
|
|
}),
|
|
};
|
|
mockSpawnProcess.stderr = {
|
|
on: vi.fn(),
|
|
};
|
|
mockSpawnProcess.on = vi.fn((event: string, callback: Function) => {
|
|
if (event === 'close') {
|
|
closeCallback = callback;
|
|
}
|
|
return mockSpawnProcess;
|
|
});
|
|
|
|
const promise = service.fetchUsageData();
|
|
|
|
// Simulate stdout data
|
|
stdoutCallback!(Buffer.from(mockOutput));
|
|
|
|
// Simulate successful close
|
|
closeCallback!(0);
|
|
|
|
const result = await promise;
|
|
|
|
expect(result.sessionPercentage).toBe(35); // 100 - 65
|
|
expect(spawn).toHaveBeenCalledWith(
|
|
'expect',
|
|
expect.arrayContaining(['-c']),
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
it('should handle authentication errors', async () => {
|
|
const mockOutput = 'token_expired';
|
|
|
|
let stdoutCallback: Function;
|
|
let closeCallback: Function;
|
|
|
|
mockSpawnProcess.stdout = {
|
|
on: vi.fn((event: string, callback: Function) => {
|
|
if (event === 'data') {
|
|
stdoutCallback = callback;
|
|
}
|
|
}),
|
|
};
|
|
mockSpawnProcess.stderr = {
|
|
on: vi.fn(),
|
|
};
|
|
mockSpawnProcess.on = vi.fn((event: string, callback: Function) => {
|
|
if (event === 'close') {
|
|
closeCallback = callback;
|
|
}
|
|
return mockSpawnProcess;
|
|
});
|
|
|
|
const promise = service.fetchUsageData();
|
|
|
|
stdoutCallback!(Buffer.from(mockOutput));
|
|
closeCallback!(1);
|
|
|
|
await expect(promise).rejects.toThrow('Authentication required');
|
|
});
|
|
|
|
it('should handle timeout with no data', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
mockSpawnProcess.stdout = {
|
|
on: vi.fn(),
|
|
};
|
|
mockSpawnProcess.stderr = {
|
|
on: vi.fn(),
|
|
};
|
|
mockSpawnProcess.on = vi.fn(() => mockSpawnProcess);
|
|
mockSpawnProcess.kill = vi.fn();
|
|
|
|
const promise = service.fetchUsageData();
|
|
|
|
// Advance time past timeout (30 seconds)
|
|
vi.advanceTimersByTime(31000);
|
|
|
|
await expect(promise).rejects.toThrow('Command timed out');
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
|
|
describe('executeClaudeUsageCommandWindows', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(os.platform).mockReturnValue('win32');
|
|
vi.mocked(os.homedir).mockReturnValue('C:\\Users\\testuser');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ USERPROFILE: 'C:\\Users\\testuser' });
|
|
});
|
|
|
|
it('should use node-pty on Windows and return output', async () => {
|
|
const windowsService = new ClaudeUsageService(); // Create new service for Windows platform
|
|
const mockOutput = `
|
|
Current session
|
|
65% left
|
|
Resets in 2h
|
|
`;
|
|
|
|
let dataCallback: Function | undefined;
|
|
let exitCallback: Function | undefined;
|
|
|
|
const mockPty = {
|
|
onData: vi.fn((callback: Function) => {
|
|
dataCallback = callback;
|
|
}),
|
|
onExit: vi.fn((callback: Function) => {
|
|
exitCallback = callback;
|
|
}),
|
|
write: vi.fn(),
|
|
kill: vi.fn(),
|
|
};
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
|
|
|
|
const promise = windowsService.fetchUsageData();
|
|
|
|
// Simulate data
|
|
dataCallback!(mockOutput);
|
|
|
|
// Simulate successful exit
|
|
exitCallback!({ exitCode: 0 });
|
|
|
|
const result = await promise;
|
|
|
|
expect(result.sessionPercentage).toBe(35);
|
|
expect(pty.spawn).toHaveBeenCalledWith(
|
|
'cmd.exe',
|
|
['/c', 'claude', '--add-dir', 'C:\\Users\\testuser'],
|
|
expect.any(Object)
|
|
);
|
|
});
|
|
|
|
it('should send escape key after seeing usage data', async () => {
|
|
vi.useFakeTimers();
|
|
const windowsService = new ClaudeUsageService();
|
|
|
|
const mockOutput = 'Current session\n65% left';
|
|
|
|
let dataCallback: Function | undefined;
|
|
let exitCallback: Function | undefined;
|
|
|
|
const mockPty = {
|
|
onData: vi.fn((callback: Function) => {
|
|
dataCallback = callback;
|
|
}),
|
|
onExit: vi.fn((callback: Function) => {
|
|
exitCallback = callback;
|
|
}),
|
|
write: vi.fn(),
|
|
kill: vi.fn(),
|
|
};
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
|
|
|
|
const promise = windowsService.fetchUsageData();
|
|
|
|
// Simulate seeing usage data
|
|
dataCallback!(mockOutput);
|
|
|
|
// Advance time to trigger escape key sending (impl uses 3000ms delay)
|
|
vi.advanceTimersByTime(3100);
|
|
|
|
expect(mockPty.write).toHaveBeenCalledWith('\x1b');
|
|
|
|
// Complete the promise to avoid unhandled rejection
|
|
exitCallback!({ exitCode: 0 });
|
|
await promise;
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should handle authentication errors on Windows', async () => {
|
|
const windowsService = new ClaudeUsageService();
|
|
let dataCallback: Function | undefined;
|
|
let exitCallback: Function | undefined;
|
|
|
|
const mockPty = {
|
|
onData: vi.fn((callback: Function) => {
|
|
dataCallback = callback;
|
|
}),
|
|
onExit: vi.fn((callback: Function) => {
|
|
exitCallback = callback;
|
|
}),
|
|
write: vi.fn(),
|
|
kill: vi.fn(),
|
|
};
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
|
|
|
|
const promise = windowsService.fetchUsageData();
|
|
|
|
dataCallback!('authentication_error');
|
|
|
|
await expect(promise).rejects.toThrow(
|
|
"Claude CLI authentication issue. Please run 'claude logout' and then 'claude login' in your terminal to refresh permissions."
|
|
);
|
|
});
|
|
|
|
it('should handle timeout with no data on Windows', async () => {
|
|
vi.useFakeTimers();
|
|
const windowsService = new ClaudeUsageService();
|
|
|
|
const mockPty = {
|
|
onData: vi.fn(),
|
|
onExit: vi.fn(),
|
|
write: vi.fn(),
|
|
kill: vi.fn(),
|
|
killed: false,
|
|
};
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
|
|
|
|
const promise = windowsService.fetchUsageData();
|
|
|
|
// Advance time past timeout (45 seconds)
|
|
vi.advanceTimersByTime(46000);
|
|
|
|
await expect(promise).rejects.toThrow(
|
|
'The Claude CLI took too long to respond. This can happen if the CLI is waiting for a trust prompt or is otherwise busy.'
|
|
);
|
|
expect(mockPty.kill).toHaveBeenCalled();
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should return data on timeout if data was captured', async () => {
|
|
vi.useFakeTimers();
|
|
const windowsService = new ClaudeUsageService();
|
|
|
|
let dataCallback: Function | undefined;
|
|
|
|
const mockPty = {
|
|
onData: vi.fn((callback: Function) => {
|
|
dataCallback = callback;
|
|
}),
|
|
onExit: vi.fn(),
|
|
write: vi.fn(),
|
|
kill: vi.fn(),
|
|
killed: false,
|
|
};
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
|
|
|
|
const promise = windowsService.fetchUsageData();
|
|
|
|
// Simulate receiving usage data
|
|
dataCallback!('Current session\n65% left\nResets in 2h');
|
|
|
|
// Advance time past timeout (45 seconds)
|
|
vi.advanceTimersByTime(46000);
|
|
|
|
// Should resolve with data instead of rejecting
|
|
const result = await promise;
|
|
expect(result.sessionPercentage).toBe(35); // 100 - 65
|
|
expect(mockPty.kill).toHaveBeenCalled();
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should send SIGTERM after ESC if process does not exit', async () => {
|
|
vi.useFakeTimers();
|
|
const windowsService = new ClaudeUsageService();
|
|
|
|
let dataCallback: Function | undefined;
|
|
|
|
const mockPty = {
|
|
onData: vi.fn((callback: Function) => {
|
|
dataCallback = callback;
|
|
}),
|
|
onExit: vi.fn(),
|
|
write: vi.fn(),
|
|
kill: vi.fn(),
|
|
killed: false,
|
|
};
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPty as any);
|
|
|
|
windowsService.fetchUsageData();
|
|
|
|
// Simulate seeing usage data
|
|
dataCallback!('Current session\n65% left');
|
|
|
|
// Advance 3s to trigger ESC (impl uses 3000ms delay)
|
|
vi.advanceTimersByTime(3100);
|
|
expect(mockPty.write).toHaveBeenCalledWith('\x1b');
|
|
|
|
// Advance another 2s to trigger SIGTERM fallback
|
|
vi.advanceTimersByTime(2100);
|
|
expect(mockPty.kill).toHaveBeenCalledWith('SIGTERM');
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
});
|