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();
|
84
test/test.interactiveControl.ts
Normal file
84
test/test.interactiveControl.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartshell from '../ts/index.js';
|
||||
|
||||
tap.test('should handle programmatic input control with simple commands', async (tools) => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Use cat which works well with pipe mode
|
||||
const interactive = await testSmartshell.execInteractiveControl('cat');
|
||||
|
||||
// Send input programmatically
|
||||
await interactive.sendLine('TestUser');
|
||||
interactive.endInput();
|
||||
|
||||
// Wait for completion
|
||||
const result = await interactive.finalPromise;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('TestUser');
|
||||
});
|
||||
|
||||
tap.test('should handle streaming interactive control with cat', async (tools) => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Use cat for reliable pipe mode operation
|
||||
const streaming = await testSmartshell.execStreamingInteractiveControl('cat');
|
||||
|
||||
// Send multiple inputs
|
||||
await streaming.sendLine('One');
|
||||
await streaming.sendLine('Two');
|
||||
streaming.endInput();
|
||||
|
||||
const result = await streaming.finalPromise;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('One');
|
||||
expect(result.stdout).toContain('Two');
|
||||
});
|
||||
|
||||
tap.test('should handle sendInput without newline', async (tools) => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Use cat for testing input without newline
|
||||
const interactive = await testSmartshell.execInteractiveControl('cat');
|
||||
|
||||
// Send characters without newline, then newline, then EOF
|
||||
await interactive.sendInput('ABC');
|
||||
await interactive.sendInput('DEF');
|
||||
await interactive.sendInput('\n');
|
||||
interactive.endInput();
|
||||
|
||||
const result = await interactive.finalPromise;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('ABCDEF');
|
||||
});
|
||||
|
||||
tap.test('should mix passthrough and interactive control modes', async () => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Test that passthrough still works
|
||||
const passthroughResult = await testSmartshell.execPassthrough('echo "Passthrough works"');
|
||||
expect(passthroughResult.exitCode).toEqual(0);
|
||||
expect(passthroughResult.stdout).toContain('Passthrough works');
|
||||
|
||||
// Test that interactive control works
|
||||
const interactiveResult = await testSmartshell.execInteractiveControl('echo "Interactive control works"');
|
||||
const finalResult = await interactiveResult.finalPromise;
|
||||
expect(finalResult.exitCode).toEqual(0);
|
||||
expect(finalResult.stdout).toContain('Interactive control works');
|
||||
});
|
||||
|
||||
// Note: Tests requiring bash read with prompts should use PTY mode
|
||||
// See test.pty.ts for examples of testing commands that require terminal features
|
||||
|
||||
export default tap.start();
|
146
test/test.pty.ts
Normal file
146
test/test.pty.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartshell from '../ts/index.js';
|
||||
|
||||
// Helper to check if node-pty is available
|
||||
const isPtyAvailable = async (): Promise<boolean> => {
|
||||
try {
|
||||
await import('node-pty');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
tap.test('PTY: should handle bash read with prompts correctly', async (tools) => {
|
||||
const ptyAvailable = await isPtyAvailable();
|
||||
if (!ptyAvailable) {
|
||||
console.log('Skipping PTY test - node-pty not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// This test should work with PTY (bash read with prompt)
|
||||
const interactive = await testSmartshell.execInteractiveControlPty("bash -c 'read -p \"Enter name: \" name && echo \"Hello, $name\"'");
|
||||
|
||||
// Send input programmatically
|
||||
await interactive.sendLine('TestUser');
|
||||
|
||||
// Wait for completion
|
||||
const result = await interactive.finalPromise;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('Enter name:'); // Prompt should be visible with PTY
|
||||
expect(result.stdout).toContain('Hello, TestUser');
|
||||
});
|
||||
|
||||
tap.test('PTY: should handle terminal colors and escape sequences', async (tools) => {
|
||||
const ptyAvailable = await isPtyAvailable();
|
||||
if (!ptyAvailable) {
|
||||
console.log('Skipping PTY test - node-pty not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// ls --color=auto should produce colors in PTY mode
|
||||
const result = await testSmartshell.execInteractiveControlPty('ls --color=always /tmp');
|
||||
const finalResult = await result.finalPromise;
|
||||
|
||||
expect(finalResult.exitCode).toEqual(0);
|
||||
// Check for ANSI escape sequences (colors) in output
|
||||
const hasColors = /\x1b\[[0-9;]*m/.test(finalResult.stdout);
|
||||
expect(hasColors).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('PTY: should handle interactive password prompt simulation', async (tools) => {
|
||||
const ptyAvailable = await isPtyAvailable();
|
||||
if (!ptyAvailable) {
|
||||
console.log('Skipping PTY test - node-pty not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Simulate a password prompt scenario
|
||||
const interactive = await testSmartshell.execStreamingInteractiveControlPty(
|
||||
"bash -c 'read -s -p \"Password: \" pass && echo && echo \"Got password of length ${#pass}\"'"
|
||||
);
|
||||
|
||||
await tools.delayFor(100);
|
||||
await interactive.sendLine('secretpass');
|
||||
|
||||
const result = await interactive.finalPromise;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('Password:');
|
||||
expect(result.stdout).toContain('Got password of length 10');
|
||||
});
|
||||
|
||||
tap.test('PTY: should handle terminal size options', async (tools) => {
|
||||
const ptyAvailable = await isPtyAvailable();
|
||||
if (!ptyAvailable) {
|
||||
console.log('Skipping PTY test - node-pty not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Check terminal size using stty
|
||||
const result = await testSmartshell.execInteractiveControlPty('stty size');
|
||||
const finalResult = await result.finalPromise;
|
||||
|
||||
expect(finalResult.exitCode).toEqual(0);
|
||||
// Default size should be 30 rows x 120 cols as set in _execCommandPty
|
||||
expect(finalResult.stdout).toContain('30 120');
|
||||
});
|
||||
|
||||
tap.test('PTY: should handle Ctrl+C (SIGINT) properly', async (tools) => {
|
||||
const ptyAvailable = await isPtyAvailable();
|
||||
if (!ptyAvailable) {
|
||||
console.log('Skipping PTY test - node-pty not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Start a long-running process
|
||||
const streaming = await testSmartshell.execStreamingInteractiveControlPty('sleep 10');
|
||||
|
||||
// Send interrupt after a short delay
|
||||
await tools.delayFor(100);
|
||||
await streaming.keyboardInterrupt();
|
||||
|
||||
const result = await streaming.finalPromise;
|
||||
// Process should exit with non-zero code due to interrupt
|
||||
expect(result.exitCode).not.toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('Regular pipe mode should still work alongside PTY', async () => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Regular mode should work without PTY
|
||||
const interactive = await testSmartshell.execInteractiveControl('echo "Pipe mode works"');
|
||||
const result = await interactive.finalPromise;
|
||||
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('Pipe mode works');
|
||||
});
|
||||
|
||||
export default tap.start();
|
54
test/test.simpleInteractive.ts
Normal file
54
test/test.simpleInteractive.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartshell from '../ts/index.js';
|
||||
|
||||
tap.test('should send input to cat command', async (tools) => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Use cat which simply echoes what it receives
|
||||
const interactive = await testSmartshell.execInteractiveControl('cat');
|
||||
|
||||
// Send some text and close stdin
|
||||
await interactive.sendLine('Hello World');
|
||||
interactive.endInput(); // Close stdin properly
|
||||
|
||||
const result = await interactive.finalPromise;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('Hello World');
|
||||
});
|
||||
|
||||
tap.test('should work with simple echo', async () => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// This should work without any input
|
||||
const interactive = await testSmartshell.execInteractiveControl('echo "Direct test"');
|
||||
const result = await interactive.finalPromise;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('Direct test');
|
||||
});
|
||||
|
||||
tap.test('should handle streaming with input control', async (tools) => {
|
||||
const testSmartshell = new smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
sourceFilePaths: [],
|
||||
});
|
||||
|
||||
// Test with streaming and cat
|
||||
const streaming = await testSmartshell.execStreamingInteractiveControl('cat');
|
||||
|
||||
await streaming.sendLine('Line 1');
|
||||
await streaming.sendLine('Line 2');
|
||||
streaming.endInput(); // Close stdin
|
||||
|
||||
const result = await streaming.finalPromise;
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.stdout).toContain('Line 1');
|
||||
expect(result.stdout).toContain('Line 2');
|
||||
});
|
||||
|
||||
export default tap.start();
|
150
test/test.spawn.ts
Normal file
150
test/test.spawn.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartshell from '../ts/index.js';
|
||||
|
||||
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 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.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 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();
|
Reference in New Issue
Block a user