// @lifecycle canonical - Unit tests for ShellVerifyExecutor service.
/**
* ShellVerifyExecutor Unit Tests
*
* Tests the shell verification executor including:
* - Command execution with exit codes
* - Timeout handling and process cleanup
* - Output capture and truncation
* - Environment variable security
* - Edge case handling
*/
import {
ShellVerifyExecutor,
createShellVerifyExecutor,
resetDefaultShellVerifyExecutor,
} from '../../../../src/engine/gates/shell/shell-verify-executor.js';
import { SHELL_OUTPUT_MAX_CHARS } from '../../../../src/engine/gates/shell/types.js';
import {
SHELL_VERIFY_DEFAULT_TIMEOUT,
SHELL_VERIFY_MAX_TIMEOUT,
} from '../../../../src/engine/gates/constants.js';
describe('ShellVerifyExecutor', () => {
let executor: ShellVerifyExecutor;
beforeEach(() => {
resetDefaultShellVerifyExecutor();
executor = createShellVerifyExecutor({ debug: false, defaultTimeout: 5000 });
});
afterEach(() => {
resetDefaultShellVerifyExecutor();
});
describe('constructor and configuration', () => {
it('should use default timeout when not specified', () => {
const defaultExecutor = createShellVerifyExecutor();
// Verify by running a command and checking it doesn't use 0 timeout
expect(defaultExecutor).toBeDefined();
});
it('should accept custom configuration', () => {
const customExecutor = createShellVerifyExecutor({
defaultTimeout: 10000,
maxTimeout: 30000,
defaultWorkingDir: '/tmp',
debug: true,
});
expect(customExecutor).toBeDefined();
});
});
describe('command execution - pass/fail logic', () => {
it('should return passed=true and exitCode=0 for successful command', async () => {
const result = await executor.execute({ command: 'true' });
expect(result.passed).toBe(true);
expect(result.exitCode).toBe(0);
expect(result.command).toBe('true');
expect(result.durationMs).toBeGreaterThanOrEqual(0);
});
it('should return passed=false and exitCode=1 for failing command', async () => {
const result = await executor.execute({ command: 'false' });
expect(result.passed).toBe(false);
expect(result.exitCode).toBe(1);
});
it('should return specific exit codes from commands', async () => {
const result = await executor.execute({ command: 'exit 42' });
expect(result.passed).toBe(false);
expect(result.exitCode).toBe(42);
});
it('should handle command not found gracefully', async () => {
const result = await executor.execute({ command: 'nonexistent_command_xyz_123' });
expect(result.passed).toBe(false);
expect(result.exitCode).not.toBe(0);
// stderr should contain error about command not found
expect(result.stderr.length > 0 || result.exitCode === 127).toBe(true);
});
});
describe('output handling', () => {
it('should capture stdout from command', async () => {
const result = await executor.execute({ command: 'echo "hello world"' });
expect(result.passed).toBe(true);
expect(result.stdout.trim()).toBe('hello world');
});
it('should capture stderr from command', async () => {
const result = await executor.execute({ command: 'echo "error message" >&2' });
expect(result.passed).toBe(true);
expect(result.stderr.trim()).toBe('error message');
});
it('should capture both stdout and stderr', async () => {
const result = await executor.execute({
command: 'echo "out" && echo "err" >&2',
});
expect(result.stdout).toContain('out');
expect(result.stderr).toContain('err');
});
it('should truncate output exceeding max chars limit', async () => {
// Generate output larger than SHELL_OUTPUT_MAX_CHARS (5000)
const largeOutput = 'x'.repeat(6000);
const result = await executor.execute({
command: `echo "${largeOutput}"`,
});
expect(result.stdout.length).toBeLessThanOrEqual(SHELL_OUTPUT_MAX_CHARS + 100); // Allow for truncation prefix
expect(result.stdout).toContain('[...truncated');
});
it('should prefer keeping end of output when truncating', async () => {
// Generate output with identifiable end marker
const result = await executor.execute({
command: `echo "${'a'.repeat(6000)}END_MARKER"`,
});
// The end marker should be preserved
expect(result.stdout).toContain('END_MARKER');
});
});
describe('timeout handling', () => {
it('should set timedOut flag when command exceeds timeout', async () => {
const shortTimeoutExecutor = createShellVerifyExecutor({
defaultTimeout: 100, // Very short timeout
});
const result = await shortTimeoutExecutor.execute({
command: 'sleep 5', // Much longer than timeout
});
expect(result.passed).toBe(false);
expect(result.timedOut).toBe(true);
}, 10000); // Extended test timeout
it('should clamp timeout to minimum 1000ms', async () => {
// Even with very short timeout, should not fail immediately
const result = await executor.execute({
command: 'echo "fast"',
timeout: 10, // Less than 1000ms minimum
});
// Command should still execute (timeout clamped to 1000ms)
expect(result.passed).toBe(true);
});
it('should clamp timeout to maximum allowed', async () => {
const executor = createShellVerifyExecutor({ maxTimeout: 1000 });
// Even with very long timeout, should be clamped
const result = await executor.execute({
command: 'echo "test"',
timeout: 9999999, // Way above max
});
expect(result.passed).toBe(true);
});
it('should use default timeout from config when not specified', async () => {
const customExecutor = createShellVerifyExecutor({
defaultTimeout: 2000,
});
const result = await customExecutor.execute({
command: 'echo "using default"',
// No timeout specified, should use 2000ms default
});
expect(result.passed).toBe(true);
});
});
describe('environment variable security', () => {
it('should pass PATH environment variable', async () => {
const result = await executor.execute({
command: 'echo $PATH',
});
expect(result.passed).toBe(true);
expect(result.stdout.length).toBeGreaterThan(0);
});
it('should pass HOME environment variable', async () => {
const result = await executor.execute({
command: 'echo $HOME',
});
expect(result.passed).toBe(true);
expect(result.stdout.trim().length).toBeGreaterThan(0);
});
it('should NOT pass sensitive AWS variables', async () => {
// Set a fake AWS key in current process (for test only)
const originalKey = process.env.AWS_SECRET_ACCESS_KEY;
process.env.AWS_SECRET_ACCESS_KEY = 'test-secret-key';
try {
const result = await executor.execute({
command: 'echo "$AWS_SECRET_ACCESS_KEY"',
});
// Should be empty or undefined in subprocess
expect(result.stdout.trim()).toBe('');
} finally {
// Restore original value
if (originalKey) {
process.env.AWS_SECRET_ACCESS_KEY = originalKey;
} else {
delete process.env.AWS_SECRET_ACCESS_KEY;
}
}
});
it('should NOT pass GITHUB_TOKEN variable', async () => {
const originalToken = process.env.GITHUB_TOKEN;
process.env.GITHUB_TOKEN = 'ghp_test_token_12345';
try {
const result = await executor.execute({
command: 'echo "$GITHUB_TOKEN"',
});
// Should be empty in subprocess
expect(result.stdout.trim()).toBe('');
} finally {
if (originalToken) {
process.env.GITHUB_TOKEN = originalToken;
} else {
delete process.env.GITHUB_TOKEN;
}
}
});
it('should allow custom env vars via override', async () => {
const result = await executor.execute({
command: 'echo "$CUSTOM_VAR"',
env: { CUSTOM_VAR: 'custom_value' },
});
expect(result.passed).toBe(true);
expect(result.stdout.trim()).toBe('custom_value');
});
});
describe('working directory', () => {
it('should execute in specified working directory', async () => {
const result = await executor.execute({
command: 'pwd',
workingDir: '/tmp',
});
expect(result.passed).toBe(true);
expect(result.stdout.trim()).toBe('/tmp');
});
it('should use default working directory when not specified', async () => {
const executor = createShellVerifyExecutor({
defaultWorkingDir: '/tmp',
});
const result = await executor.execute({
command: 'pwd',
});
expect(result.stdout.trim()).toBe('/tmp');
});
});
describe('edge cases', () => {
it('should reject empty command with error', async () => {
const result = await executor.execute({ command: '' });
expect(result.passed).toBe(false);
expect(result.exitCode).toBe(-1);
expect(result.stderr).toBe('Empty command provided');
});
it('should reject whitespace-only command with error', async () => {
const result = await executor.execute({ command: ' ' });
expect(result.passed).toBe(false);
expect(result.exitCode).toBe(-1);
expect(result.stderr).toBe('Empty command provided');
});
it('should handle command with special characters', async () => {
const result = await executor.execute({
command: 'echo "hello\'s world" && echo "done"',
});
expect(result.passed).toBe(true);
expect(result.stdout).toContain("hello's world");
});
it('should report duration in milliseconds', async () => {
const result = await executor.execute({
command: 'sleep 0.1',
});
expect(result.durationMs).toBeGreaterThanOrEqual(50); // At least 50ms
expect(result.durationMs).toBeLessThan(5000); // Less than 5 seconds
});
it('should include command in result', async () => {
const command = 'echo "test command"';
const result = await executor.execute({ command });
expect(result.command).toBe(command);
});
});
describe('npm script support', () => {
it('should support npm-style commands', async () => {
// This tests that npm would work if available
// We use a simple echo as proxy since npm might not be in test env
const result = await executor.execute({
command: 'which npm || echo "npm_path"',
});
expect(result.passed).toBe(true);
});
it('should capture npm test output format', async () => {
// Simulate npm test output format
const result = await executor.execute({
command: 'echo "PASS src/test.ts" && echo "Tests: 5 passed"',
});
expect(result.passed).toBe(true);
expect(result.stdout).toContain('PASS');
expect(result.stdout).toContain('Tests:');
});
});
});