fix(bash): drained piped output before timeout return
- Delayed reader cancellation so pipeline consumers can flush after producers are terminated. - Kept the JavaScript watchdog behind bounded native timeout cleanup. - Added native and executor regressions for timeout-time output draining. Fixes #5316
This commit is contained in:
@@ -520,19 +520,54 @@ exit 64
|
||||
expect(next.output.trim()).toBe("still_persistent");
|
||||
});
|
||||
|
||||
it("does not abort the native signal when the JavaScript timeout fallback returns streamed output", async () => {
|
||||
// Compress the JS-side fallback timer (floored at 1000ms in the source) so
|
||||
// the safety-net fires deterministically without a real 1s wait. Only long
|
||||
// timers are shrunk — fs/subprocess setup keeps real scheduling — and the
|
||||
// reported "1 seconds" derives from the configured timeout, not the timer.
|
||||
it("waits for native timeout teardown to flush piped output", async () => {
|
||||
const realSetTimeout = globalThis.setTimeout;
|
||||
vi.spyOn(globalThis, "setTimeout").mockImplementation(((handler: () => void, ms?: number, ...rest: unknown[]) =>
|
||||
realSetTimeout(
|
||||
handler,
|
||||
typeof ms === "number" && ms >= 1000 ? 5 : ms,
|
||||
ms === 1000 ? 5 : typeof ms === "number" && ms > 1000 ? 50 : ms,
|
||||
...rest,
|
||||
)) as typeof globalThis.setTimeout);
|
||||
|
||||
let nativeSignal: AbortSignal | undefined;
|
||||
vi.spyOn(piNatives.Shell.prototype, "run").mockImplementation((options, onChunk) => {
|
||||
if (options.signal instanceof AbortSignal) {
|
||||
nativeSignal = options.signal;
|
||||
}
|
||||
const nativeResult = Promise.withResolvers<piNatives.ShellRunResult>();
|
||||
realSetTimeout(() => {
|
||||
onChunk?.(null, "flushed-during-timeout\n");
|
||||
nativeResult.resolve({ exitCode: undefined, cancelled: false, timedOut: true });
|
||||
}, 20);
|
||||
return nativeResult.promise;
|
||||
});
|
||||
const abortSpy = vi.spyOn(piNatives.Shell.prototype, "abort").mockResolvedValue();
|
||||
|
||||
const result = await executeBash("producer | tail -5", {
|
||||
cwd: tempDir,
|
||||
timeout: 1000,
|
||||
sessionKey: "native-timeout-flushes-pipeline",
|
||||
});
|
||||
|
||||
expect(result.cancelled).toBe(true);
|
||||
expect(result.output).toContain("flushed-during-timeout");
|
||||
expect(result.output).toContain("Command timed out after 1 seconds");
|
||||
expect(nativeSignal).toBeDefined();
|
||||
expect(nativeSignal?.aborted).toBe(false);
|
||||
expect(abortSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps a delayed JavaScript fallback for stalled native timeout cleanup", async () => {
|
||||
const realSetTimeout = globalThis.setTimeout;
|
||||
let fallbackDelayMs = 0;
|
||||
vi.spyOn(globalThis, "setTimeout").mockImplementation(((handler: () => void, ms?: number, ...rest: unknown[]) => {
|
||||
if (typeof ms === "number" && ms >= 1000) {
|
||||
fallbackDelayMs = Math.max(fallbackDelayMs, ms);
|
||||
return realSetTimeout(handler, 5, ...rest);
|
||||
}
|
||||
return realSetTimeout(handler, ms, ...rest);
|
||||
}) as typeof globalThis.setTimeout);
|
||||
|
||||
let nativeSignal: AbortSignal | undefined;
|
||||
vi.spyOn(piNatives.Shell.prototype, "run").mockImplementation((options, onChunk) => {
|
||||
if (options.signal instanceof AbortSignal) {
|
||||
@@ -552,6 +587,7 @@ exit 64
|
||||
expect(result.cancelled).toBe(true);
|
||||
expect(result.output).toContain("streamed-before-timeout");
|
||||
expect(result.output).toContain("Command timed out after 1 seconds");
|
||||
expect(fallbackDelayMs).toBeGreaterThan(1000);
|
||||
expect(nativeSignal).toBeDefined();
|
||||
expect(nativeSignal?.aborted).toBe(false);
|
||||
expect(abortSpy).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user