feat(smartshell): Add secure spawn APIs, PTY support, interactive/streaming control, timeouts and buffer limits; update README and tests
This commit is contained in:
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();
|
Reference in New Issue
Block a user