Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions packages/runtime/src/__tests__/pty-process-driver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';

import { PtyProcessDriver, type PtyProcessExit } from '../pty-process-driver.js';
import { loadPtyStack } from '../pty-stack.js';

const TEST_TIMEOUT_MS = 5_000;

test('PTY driver exposes the live child PID and acknowledges forced exit', async () => {
const stack = await loadPtyStack();
let resolveReady!: () => void;
let resolveExit!: (exit: PtyProcessExit) => void;
let rejectInvariant!: (error: Error) => void;
const ready = new Promise<void>((resolve) => {
resolveReady = resolve;
});
const invariantFailure = new Promise<never>((_, reject) => {
rejectInvariant = reject;
});
const exited = new Promise<PtyProcessExit>((resolve) => {
resolveExit = resolve;
});
const driver = new PtyProcessDriver({
stack,
file: process.execPath,
args: ['-e', "process.stdout.write('READY\\n'); setInterval(() => {}, 1000)"],
cwd: process.cwd(),
env: process.env,
cols: 80,
rows: 24,
onData: (data) => {
if (data.includes('READY')) resolveReady();
},
onExit: resolveExit,
onInvariantFailure: rejectInvariant,
});

try {
await withTimeout(Promise.race([ready, invariantFailure]), 'PTY child did not become ready');
assert.ok(Number.isSafeInteger(driver.pid) && driver.pid > 0);
driver.kill('SIGKILL');
await withTimeout(
Promise.race([exited, invariantFailure]),
'PTY child did not acknowledge exit',
);
} finally {
try {
driver.kill('SIGKILL');
} catch {
// The PTY may already have closed after acknowledging exit.
}
driver.dispose();
}
});

function withTimeout<T>(promise: Promise<T>, message: string): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) => {
const timer = setTimeout(() => reject(new Error(message)), TEST_TIMEOUT_MS);
timer.unref();
}),
]);
}
9 changes: 4 additions & 5 deletions packages/runtime/src/__tests__/shell-exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,10 @@ describe('runShellWithBoundedTail', () => {
}
});

test('on abort, kills descendants that detach from the shell process group', async () => {
test('on abort, kills detached descendants of a directly spawned process', async () => {
const dir = await fs.mkdtemp(join(tmpdir(), 'shell-exec-tree-'));
const pidFile = join(dir, 'child.pid');
const parentFile = join(dir, 'parent.cjs');
const runtime = JSON.stringify(process.execPath);
const childScript = 'setInterval(() => {}, 1000)';
await fs.writeFile(
parentFile,
Expand All @@ -200,10 +199,10 @@ describe('runShellWithBoundedTail', () => {
setInterval(() => {}, 1000);
`,
);
const cmd = `${runtime} ${JSON.stringify(parentFile)}`;
const abort = new AbortController();
const running = runShellWithBoundedTail(
cmd,
const running = runProcessWithBoundedTail(
process.execPath,
[parentFile],
base({
timeoutMs: 10_000,
killGraceMs: 150,
Expand Down
Loading