import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartshell from '../ts/index.js'; const getErrorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error); tap.test('should handle EPIPE errors gracefully', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const streaming = await testSmartshell.execStreamingInteractiveControl('head -n 1'); // Send more data after head exits (will cause EPIPE) await streaming.sendLine('Line 1'); // This should not throw even though head has exited let errorThrown = false; try { await streaming.sendLine('Line 2'); await streaming.sendLine('Line 3'); } catch (error) { errorThrown = true; // EPIPE or destroyed stdin is expected } const result = await streaming.finalPromise; expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('Line 1'); }); tap.test('should handle strict mode with non-zero exit codes', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); let errorThrown = false; let errorMessage = ''; try { await testSmartshell.execStrict('exit 42'); } catch (error) { errorThrown = true; errorMessage = getErrorMessage(error); } expect(errorThrown).toBeTrue(); expect(errorMessage).toContain('exited with code 42'); }); tap.test('should handle strict mode with signal termination', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); let errorThrown = false; let errorMessage = ''; try { // Use execSpawn with strict mode and kill it const result = testSmartshell.execSpawn('sleep', ['10'], { strict: true, timeout: 100 // Will cause SIGTERM }); await result; } catch (error) { errorThrown = true; errorMessage = getErrorMessage(error); } expect(errorThrown).toBeTrue(); expect(errorMessage).toContain('terminated by signal'); }); tap.test('strict mode errors should expose command result details', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); let caughtError: smartshell.SmartshellError | null = null; try { await testSmartshell.execSpawn('bash', ['-c', 'echo stdout-value; echo stderr-value >&2; exit 7'], { strict: true, silent: true, }); } catch (error) { caughtError = error as smartshell.SmartshellError; } expect(caughtError).toBeInstanceOf(smartshell.SmartshellError); expect(caughtError!.command).toEqual('bash'); expect(caughtError!.exitCode).toEqual(7); expect(caughtError!.stdout).toContain('stdout-value'); expect(caughtError!.combinedOutput).toContain('stderr-value'); expect(caughtError!.stderr).toContain('stderr-value'); expect(caughtError!.result.exitCode).toEqual(7); }); tap.test('execAndWaitForLine with timeout should reject properly', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); let errorThrown = false; let errorMessage = ''; try { await testSmartshell.execAndWaitForLine( 'sleep 5 && echo "Too late"', /Too late/, false, { timeout: 100 } ); } catch (error) { errorThrown = true; errorMessage = getErrorMessage(error); } expect(errorThrown).toBeTrue(); expect(errorMessage).toContain('Timeout waiting for pattern'); }); tap.test('execAndWaitForLine with terminateOnMatch should stop process', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const start = Date.now(); await testSmartshell.execAndWaitForLine( 'echo "Match this" && sleep 5', /Match this/, false, { terminateOnMatch: true } ); const duration = Date.now() - start; // Should terminate immediately after match, not wait for sleep expect(duration).toBeLessThan(2000); }); tap.test('should handle process ending without matching pattern', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); let errorThrown = false; let errorMessage = ''; try { await testSmartshell.execAndWaitForLine( 'echo "Wrong text"', /Never appears/, false ); } catch (error) { errorThrown = true; errorMessage = getErrorMessage(error); } expect(errorThrown).toBeTrue(); expect(errorMessage).toContain('Process ended without matching pattern'); }); tap.test('passthrough unpipe should handle destroyed stdin gracefully', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); // This should complete without throwing even if stdin operations fail const result = await testSmartshell.execPassthrough('echo "Test passthrough unpipe"'); expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('Test passthrough unpipe'); }); tap.test('should handle write after stream destroyed', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const interactive = await testSmartshell.execInteractiveControl('true'); // Exits immediately // Wait for process to exit await interactive.finalPromise; // Try to write after process has exited let errorThrown = false; try { await interactive.sendLine('This should fail'); } catch (error) { errorThrown = true; expect(getErrorMessage(error)).toContain('destroyed'); } expect(errorThrown).toBeTrue(); }); tap.test('debug mode should log additional information', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); // Capture console.log output const originalLog = console.log; let debugOutput = ''; console.log = (msg: string) => { debugOutput += msg + '\n'; }; try { const streaming = await testSmartshell.execSpawnStreaming('echo', ['Debug test'], { debug: true }); await streaming.terminate(); await streaming.finalPromise; } finally { console.log = originalLog; } // Should have logged debug messages expect(debugOutput).toContain('[smartshell]'); }); tap.test('custom environment variables should be passed correctly', async () => { const testSmartshell = new smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const result = await testSmartshell.execSpawn('bash', ['-c', 'echo $CUSTOM_VAR'], { env: { ...process.env, CUSTOM_VAR: 'test_value_123' } }); expect(result.exitCode).toEqual(0); expect(result.stdout).toContain('test_value_123'); }); export default tap.start();