import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartshell from '../ts/index.js'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; tap.test('execSpawn should execute commands with args array (shell:false)', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); // Test basic command with args const result = await testSmartshell.execSpawn('echo', ['Hello', 'World']); expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('Hello World'); }); tap.test('execSpawn should run in the configured cwd', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'smartshell-cwd-')); try { const result = await testSmartshell.execSpawn('node', ['-e', 'console.log(process.cwd())'], { cwd: tmpDir, silent: true, }); expect(result.exitCode).toEqual(0); expect(result.stdout.trim()).toEqual(tmpDir); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } }); tap.test('execSilent should run shell commands in the configured cwd', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'smartshell-shell-cwd-')); try { const result = await testSmartshell.execSilent('pwd', { cwd: tmpDir }); expect(result.exitCode).toEqual(0); expect(result.stdout.trim()).toEqual(tmpDir); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } }); tap.test('execSpawn should handle command not found errors', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); let errorThrown = false; try { await testSmartshell.execSpawn('nonexistentcommand123', ['arg1']); } catch (error) { errorThrown = true; expect((error as NodeJS.ErrnoException).code).toEqual('ENOENT'); } expect(errorThrown).toBeTrue(); }); tap.test('execSpawn should properly escape arguments', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); // Test that shell metacharacters are treated as literals const result = await testSmartshell.execSpawn('echo', ['$HOME', '&&', 'ls']); expect(result.exitCode).toEqual(0); // Should output literal strings, not expanded/executed expect(result.stdout).toContain('$HOME && ls'); }); tap.test('execSpawn streaming should work', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const streaming = await testSmartshell.execSpawnStreaming('echo', ['Streaming test']); const result = await streaming.finalPromise; expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('Streaming test'); }); tap.test('execSpawn interactive control should work', async (tools) => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const interactive = await testSmartshell.execSpawnInteractiveControl('cat', []); await interactive.sendLine('Input line'); interactive.endInput(); const result = await interactive.finalPromise; expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('Input line'); }); tap.test('execSpawn should capture stderr', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); // ls on non-existent directory should produce stderr const result = await testSmartshell.execSpawn('ls', ['/nonexistent/directory/path']); expect(result.exitCode).not.toEqual(0); expect(result.stderr).toBeTruthy(); expect(result.stderr).toContain('No such file or directory'); }); tap.test('execSpawn with timeout should terminate process', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const start = Date.now(); const result = await testSmartshell.execSpawn('sleep', ['10'], { timeout: 100 }); const duration = Date.now() - start; // Process should be terminated by timeout expect(duration).toBeLessThan(500); expect(result.exitCode).not.toEqual(0); expect(result.signal).toBeTruthy(); // Should have been killed by signal }); tap.test('execSpawn timeout should terminate the spawned process tree', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const markerPath = path.join(os.tmpdir(), `smartshell-spawn-timeout-${process.pid}-${Date.now()}`); try { const result = await testSmartshell.execSpawn( 'bash', ['-c', `(sleep 0.6; touch "${markerPath}") & wait`], { timeout: 100, silent: true }, ); await new Promise((resolve) => setTimeout(resolve, 800)); expect(result.exitCode).not.toEqual(0); expect(fs.existsSync(markerPath)).toBeFalse(); } finally { fs.rmSync(markerPath, { force: true }); } }); tap.test('exec timeout should terminate the shell process tree', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const markerPath = path.join(os.tmpdir(), `smartshell-shell-timeout-${process.pid}-${Date.now()}`); try { const result = await testSmartshell.execSilent(`(sleep 0.6; touch "${markerPath}") & wait`, { timeout: 100, }); await new Promise((resolve) => setTimeout(resolve, 800)); expect(result.exitCode).not.toEqual(0); expect(fs.existsSync(markerPath)).toBeFalse(); } finally { fs.rmSync(markerPath, { force: true }); } }); tap.test('execSpawn with maxBuffer should truncate output', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); // Generate large output const result = await testSmartshell.execSpawn('bash', ['-c', 'for i in {1..1000}; do echo "Line $i with some padding text to make it longer"; done'], { maxBuffer: 1024, // Very small buffer }); expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('[Output truncated - exceeded maxBuffer]'); }); tap.test('execSpawn with onData callback should stream data', async (tools) => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); let dataReceived = ''; const result = await testSmartshell.execSpawn('echo', ['Test data'], { onData: (chunk) => { dataReceived += chunk.toString(); } }); expect(result.exitCode).toEqual(0); expect(dataReceived).toContain('Test data'); }); tap.test('execSpawn with signal should report signal in result', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const streaming = await testSmartshell.execSpawnStreaming('sleep', ['10']); // Send SIGTERM after a short delay setTimeout(() => streaming.terminate(), 100); const result = await streaming.finalPromise; expect(result.exitCode).not.toEqual(0); expect(result.signal).toEqual('SIGTERM'); }); export default tap.start();