feat(smartshell): Add secure spawn APIs, PTY support, interactive/streaming control, timeouts and buffer limits; update README and tests
This commit is contained in:
222
test/test.errorHandling.ts
Normal file
222
test/test.errorHandling.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartshell from '../ts/index.js';
|
||||
|
||||
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 = error.message;
|
||||
}
|
||||
|
||||
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 = error.message;
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeTrue();
|
||||
expect(errorMessage).toContain('terminated by signal');
|
||||
});
|
||||
|
||||
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 = error.message;
|
||||
}
|
||||
|
||||
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 = error.message;
|
||||
}
|
||||
|
||||
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(error.message).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();
|
Reference in New Issue
Block a user