mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
- Updated the TerminalService to utilize getShellPaths() for better shell detection across platforms. - Improved logic for detecting user-configured shells in WSL and added fallbacks for various platforms. - Enhanced unit tests to mock shell paths for comprehensive cross-platform testing, ensuring accurate shell detection behavior. These changes aim to streamline shell detection and improve the user experience across different operating systems.
643 lines
22 KiB
TypeScript
643 lines
22 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { TerminalService, getTerminalService } from '@/services/terminal-service.js';
|
|
import * as pty from 'node-pty';
|
|
import * as os from 'os';
|
|
import * as platform from '@automaker/platform';
|
|
import * as secureFs from '@/lib/secure-fs.js';
|
|
|
|
vi.mock('node-pty');
|
|
vi.mock('os');
|
|
vi.mock('@automaker/platform', async () => {
|
|
const actual = await vi.importActual('@automaker/platform');
|
|
return {
|
|
...actual,
|
|
systemPathExists: vi.fn(),
|
|
systemPathReadFileSync: vi.fn(),
|
|
getWslVersionPath: vi.fn(),
|
|
getShellPaths: vi.fn(), // Mock shell paths for cross-platform testing
|
|
isAllowedSystemPath: vi.fn(() => true), // Allow all paths in tests
|
|
};
|
|
});
|
|
vi.mock('@/lib/secure-fs.js');
|
|
|
|
describe('terminal-service.ts', () => {
|
|
let service: TerminalService;
|
|
let mockPtyProcess: any;
|
|
|
|
// Shell paths for each platform (matching system-paths.ts)
|
|
const linuxShellPaths = [
|
|
'/bin/zsh',
|
|
'/bin/bash',
|
|
'/bin/sh',
|
|
'/usr/bin/zsh',
|
|
'/usr/bin/bash',
|
|
'/usr/bin/sh',
|
|
'/usr/local/bin/zsh',
|
|
'/usr/local/bin/bash',
|
|
'/opt/homebrew/bin/zsh',
|
|
'/opt/homebrew/bin/bash',
|
|
'zsh',
|
|
'bash',
|
|
'sh',
|
|
];
|
|
|
|
const windowsShellPaths = [
|
|
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
'C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe',
|
|
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
|
'C:\\Windows\\System32\\cmd.exe',
|
|
'pwsh.exe',
|
|
'pwsh',
|
|
'powershell.exe',
|
|
'powershell',
|
|
'cmd.exe',
|
|
'cmd',
|
|
];
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
service = new TerminalService();
|
|
|
|
// Mock PTY process
|
|
mockPtyProcess = {
|
|
onData: vi.fn(),
|
|
onExit: vi.fn(),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
};
|
|
|
|
vi.mocked(pty.spawn).mockReturnValue(mockPtyProcess);
|
|
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
vi.mocked(os.arch).mockReturnValue('x64');
|
|
|
|
// Default mocks for system paths and secureFs
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(platform.systemPathReadFileSync).mockReturnValue('');
|
|
vi.mocked(platform.getWslVersionPath).mockReturnValue('/proc/version');
|
|
vi.mocked(platform.getShellPaths).mockReturnValue(linuxShellPaths); // Default to Linux paths
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
});
|
|
|
|
afterEach(() => {
|
|
service.cleanup();
|
|
});
|
|
|
|
describe('detectShell', () => {
|
|
it('should detect PowerShell Core on Windows when available', () => {
|
|
vi.mocked(os.platform).mockReturnValue('win32');
|
|
vi.mocked(platform.getShellPaths).mockReturnValue(windowsShellPaths);
|
|
vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
|
|
return path === 'C:\\Program Files\\PowerShell\\7\\pwsh.exe';
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe');
|
|
expect(result.args).toEqual([]);
|
|
});
|
|
|
|
it('should fall back to PowerShell on Windows if Core not available', () => {
|
|
vi.mocked(os.platform).mockReturnValue('win32');
|
|
vi.mocked(platform.getShellPaths).mockReturnValue(windowsShellPaths);
|
|
vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
|
|
return path === 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe');
|
|
expect(result.args).toEqual([]);
|
|
});
|
|
|
|
it('should fall back to cmd.exe on Windows if no PowerShell', () => {
|
|
vi.mocked(os.platform).mockReturnValue('win32');
|
|
vi.mocked(platform.getShellPaths).mockReturnValue(windowsShellPaths);
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(false);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('cmd.exe');
|
|
expect(result.args).toEqual([]);
|
|
});
|
|
|
|
it('should detect user shell on macOS', () => {
|
|
vi.mocked(os.platform).mockReturnValue('darwin');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/zsh' });
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('/bin/zsh');
|
|
expect(result.args).toEqual(['--login']);
|
|
});
|
|
|
|
it('should fall back to zsh on macOS if user shell not available', () => {
|
|
vi.mocked(os.platform).mockReturnValue('darwin');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({});
|
|
vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
|
|
return path === '/bin/zsh';
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('/bin/zsh');
|
|
expect(result.args).toEqual(['--login']);
|
|
});
|
|
|
|
it('should fall back to bash on macOS if zsh not available', () => {
|
|
vi.mocked(os.platform).mockReturnValue('darwin');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({});
|
|
// zsh not available, but bash is
|
|
vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
|
|
return path === '/bin/bash';
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('/bin/bash');
|
|
expect(result.args).toEqual(['--login']);
|
|
});
|
|
|
|
it('should detect user shell on Linux', () => {
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('/bin/bash');
|
|
expect(result.args).toEqual(['--login']);
|
|
});
|
|
|
|
it('should fall back to bash on Linux if user shell not available', () => {
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({});
|
|
vi.mocked(platform.systemPathExists).mockImplementation((path: string) => {
|
|
return path === '/bin/bash';
|
|
});
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('/bin/bash');
|
|
expect(result.args).toEqual(['--login']);
|
|
});
|
|
|
|
it('should fall back to sh on Linux if bash not available', () => {
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({});
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(false);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('/bin/sh');
|
|
expect(result.args).toEqual([]);
|
|
});
|
|
|
|
it('should detect WSL and use appropriate shell', () => {
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(platform.systemPathReadFileSync).mockReturnValue(
|
|
'Linux version 5.10.0-microsoft-standard-WSL2'
|
|
);
|
|
|
|
const result = service.detectShell();
|
|
|
|
expect(result.shell).toBe('/bin/bash');
|
|
expect(result.args).toEqual(['--login']);
|
|
});
|
|
});
|
|
|
|
describe('isWSL', () => {
|
|
it('should return true if /proc/version contains microsoft', () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(platform.systemPathReadFileSync).mockReturnValue(
|
|
'Linux version 5.10.0-microsoft-standard-WSL2'
|
|
);
|
|
|
|
expect(service.isWSL()).toBe(true);
|
|
});
|
|
|
|
it('should return true if /proc/version contains wsl', () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(platform.systemPathReadFileSync).mockReturnValue('Linux version 5.10.0-wsl2');
|
|
|
|
expect(service.isWSL()).toBe(true);
|
|
});
|
|
|
|
it('should return true if WSL_DISTRO_NAME is set', () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(false);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ WSL_DISTRO_NAME: 'Ubuntu' });
|
|
|
|
expect(service.isWSL()).toBe(true);
|
|
});
|
|
|
|
it('should return true if WSLENV is set', () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(false);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ WSLENV: 'PATH/l' });
|
|
|
|
expect(service.isWSL()).toBe(true);
|
|
});
|
|
|
|
it('should return false if not in WSL', () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(false);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({});
|
|
|
|
expect(service.isWSL()).toBe(false);
|
|
});
|
|
|
|
it('should return false if error reading /proc/version', () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(platform.systemPathReadFileSync).mockImplementation(() => {
|
|
throw new Error('Permission denied');
|
|
});
|
|
|
|
expect(service.isWSL()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getPlatformInfo', () => {
|
|
it('should return platform information', () => {
|
|
vi.mocked(os.platform).mockReturnValue('linux');
|
|
vi.mocked(os.arch).mockReturnValue('x64');
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const info = service.getPlatformInfo();
|
|
|
|
expect(info.platform).toBe('linux');
|
|
expect(info.arch).toBe('x64');
|
|
expect(info.defaultShell).toBe('/bin/bash');
|
|
expect(typeof info.isWSL).toBe('boolean');
|
|
});
|
|
});
|
|
|
|
describe('createSession', () => {
|
|
it('should create a new terminal session', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession({
|
|
cwd: '/test/dir',
|
|
cols: 100,
|
|
rows: 30,
|
|
});
|
|
|
|
expect(session).not.toBeNull();
|
|
expect(session!.id).toMatch(/^term-/);
|
|
expect(session!.cwd).toBe('/test/dir');
|
|
expect(session!.shell).toBe('/bin/bash');
|
|
expect(pty.spawn).toHaveBeenCalledWith(
|
|
'/bin/bash',
|
|
['--login'],
|
|
expect.objectContaining({
|
|
cwd: '/test/dir',
|
|
cols: 100,
|
|
rows: 30,
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should use default cols and rows if not provided', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
await service.createSession();
|
|
|
|
expect(pty.spawn).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.any(Array),
|
|
expect.objectContaining({
|
|
cols: 80,
|
|
rows: 24,
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should fall back to home directory if cwd does not exist', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockRejectedValue(new Error('ENOENT'));
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession({
|
|
cwd: '/nonexistent',
|
|
});
|
|
|
|
expect(session).not.toBeNull();
|
|
expect(session!.cwd).toBe('/home/user');
|
|
});
|
|
|
|
it('should fall back to home directory if cwd is not a directory', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => false } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession({
|
|
cwd: '/file.txt',
|
|
});
|
|
|
|
expect(session).not.toBeNull();
|
|
expect(session!.cwd).toBe('/home/user');
|
|
});
|
|
|
|
it('should fix double slashes in path', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession({
|
|
cwd: '//test/dir',
|
|
});
|
|
|
|
expect(session).not.toBeNull();
|
|
expect(session!.cwd).toBe('/test/dir');
|
|
});
|
|
|
|
it('should preserve WSL UNC paths', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession({
|
|
cwd: '//wsl$/Ubuntu/home',
|
|
});
|
|
|
|
expect(session).not.toBeNull();
|
|
expect(session!.cwd).toBe('//wsl$/Ubuntu/home');
|
|
});
|
|
|
|
it('should handle data events from PTY', async () => {
|
|
vi.useFakeTimers();
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const dataCallback = vi.fn();
|
|
service.onData(dataCallback);
|
|
|
|
await service.createSession();
|
|
|
|
// Simulate data event
|
|
const onDataHandler = mockPtyProcess.onData.mock.calls[0][0];
|
|
onDataHandler('test data');
|
|
|
|
// Wait for throttled output
|
|
vi.advanceTimersByTime(20);
|
|
|
|
expect(dataCallback).toHaveBeenCalled();
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should handle exit events from PTY', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const exitCallback = vi.fn();
|
|
service.onExit(exitCallback);
|
|
|
|
const session = await service.createSession();
|
|
|
|
// Simulate exit event
|
|
const onExitHandler = mockPtyProcess.onExit.mock.calls[0][0];
|
|
onExitHandler({ exitCode: 0 });
|
|
|
|
expect(session).not.toBeNull();
|
|
expect(exitCallback).toHaveBeenCalledWith(session!.id, 0);
|
|
expect(service.getSession(session!.id)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('write', () => {
|
|
it('should write data to existing session', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession();
|
|
const result = service.write(session!.id, 'ls\n');
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockPtyProcess.write).toHaveBeenCalledWith('ls\n');
|
|
});
|
|
|
|
it('should return false for non-existent session', () => {
|
|
const result = service.write('nonexistent', 'data');
|
|
|
|
expect(result).toBe(false);
|
|
expect(mockPtyProcess.write).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('resize', () => {
|
|
it('should resize existing session', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession();
|
|
const result = service.resize(session!.id, 120, 40);
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockPtyProcess.resize).toHaveBeenCalledWith(120, 40);
|
|
});
|
|
|
|
it('should return false for non-existent session', () => {
|
|
const result = service.resize('nonexistent', 120, 40);
|
|
|
|
expect(result).toBe(false);
|
|
expect(mockPtyProcess.resize).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle resize errors', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
mockPtyProcess.resize.mockImplementation(() => {
|
|
throw new Error('Resize failed');
|
|
});
|
|
|
|
const session = await service.createSession();
|
|
const result = service.resize(session!.id, 120, 40);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('killSession', () => {
|
|
it('should kill existing session', async () => {
|
|
vi.useFakeTimers();
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession();
|
|
const result = service.killSession(session!.id);
|
|
|
|
expect(result).toBe(true);
|
|
expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGTERM');
|
|
|
|
// Session is removed after SIGKILL timeout (1 second)
|
|
vi.advanceTimersByTime(1000);
|
|
|
|
expect(mockPtyProcess.kill).toHaveBeenCalledWith('SIGKILL');
|
|
expect(service.getSession(session!.id)).toBeUndefined();
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should return false for non-existent session', () => {
|
|
const result = service.killSession('nonexistent');
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should handle kill errors', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
mockPtyProcess.kill.mockImplementation(() => {
|
|
throw new Error('Kill failed');
|
|
});
|
|
|
|
const session = await service.createSession();
|
|
const result = service.killSession(session!.id);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getSession', () => {
|
|
it('should return existing session', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession();
|
|
const retrieved = service.getSession(session!.id);
|
|
|
|
expect(retrieved).toBe(session);
|
|
});
|
|
|
|
it('should return undefined for non-existent session', () => {
|
|
const retrieved = service.getSession('nonexistent');
|
|
|
|
expect(retrieved).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('getScrollback', () => {
|
|
it('should return scrollback buffer for existing session', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session = await service.createSession();
|
|
session!.scrollbackBuffer = 'test scrollback';
|
|
|
|
const scrollback = service.getScrollback(session!.id);
|
|
|
|
expect(scrollback).toBe('test scrollback');
|
|
});
|
|
|
|
it('should return null for non-existent session', () => {
|
|
const scrollback = service.getScrollback('nonexistent');
|
|
|
|
expect(scrollback).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getAllSessions', () => {
|
|
it('should return all active sessions', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session1 = await service.createSession({ cwd: '/dir1' });
|
|
const session2 = await service.createSession({ cwd: '/dir2' });
|
|
|
|
const sessions = service.getAllSessions();
|
|
|
|
expect(sessions).toHaveLength(2);
|
|
expect(session1).not.toBeNull();
|
|
expect(session2).not.toBeNull();
|
|
expect(sessions[0].id).toBe(session1!.id);
|
|
expect(sessions[1].id).toBe(session2!.id);
|
|
expect(sessions[0].cwd).toBe('/dir1');
|
|
expect(sessions[1].cwd).toBe('/dir2');
|
|
});
|
|
|
|
it('should return empty array if no sessions', () => {
|
|
const sessions = service.getAllSessions();
|
|
|
|
expect(sessions).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('onData and onExit', () => {
|
|
it('should allow subscribing and unsubscribing from data events', () => {
|
|
const callback = vi.fn();
|
|
const unsubscribe = service.onData(callback);
|
|
|
|
expect(typeof unsubscribe).toBe('function');
|
|
|
|
unsubscribe();
|
|
});
|
|
|
|
it('should allow subscribing and unsubscribing from exit events', () => {
|
|
const callback = vi.fn();
|
|
const unsubscribe = service.onExit(callback);
|
|
|
|
expect(typeof unsubscribe).toBe('function');
|
|
|
|
unsubscribe();
|
|
});
|
|
});
|
|
|
|
describe('cleanup', () => {
|
|
it('should clean up all sessions', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
|
|
const session1 = await service.createSession();
|
|
const session2 = await service.createSession();
|
|
|
|
service.cleanup();
|
|
|
|
expect(session1).not.toBeNull();
|
|
expect(session2).not.toBeNull();
|
|
expect(service.getSession(session1!.id)).toBeUndefined();
|
|
expect(service.getSession(session2!.id)).toBeUndefined();
|
|
expect(service.getAllSessions()).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle cleanup errors gracefully', async () => {
|
|
vi.mocked(platform.systemPathExists).mockReturnValue(true);
|
|
vi.mocked(secureFs.stat).mockResolvedValue({ isDirectory: () => true } as any);
|
|
vi.spyOn(process, 'env', 'get').mockReturnValue({ SHELL: '/bin/bash' });
|
|
mockPtyProcess.kill.mockImplementation(() => {
|
|
throw new Error('Kill failed');
|
|
});
|
|
|
|
await service.createSession();
|
|
|
|
expect(() => service.cleanup()).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('getTerminalService', () => {
|
|
it('should return singleton instance', () => {
|
|
const instance1 = getTerminalService();
|
|
const instance2 = getTerminalService();
|
|
|
|
expect(instance1).toBe(instance2);
|
|
});
|
|
});
|
|
});
|