test: regression for streamPodLogsOnce bail timer (FAR-10)
Uses vi.mock on k8s-client and vi.useFakeTimers to prove that when logApi.log() never resolves (the FAR-10 hang shape) and stopSignal fires, streamPodLogsOnce still returns within the bail window (LOG_STREAM_BAIL_TIMEOUT_MS). Exports streamPodLogsOnce so the test can call it directly. Also covers the no-stopSignal happy path. 269/269 passing (+2 new). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,6 +1,21 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import type * as k8s from "@kubernetes/client-node";
|
||||
import { isK8s404, buildPartialRunError, isReattachableOrphan, describePodTerminatedError } from "./execute.js";
|
||||
import type { Writable } from "node:stream";
|
||||
|
||||
// Mock the K8s client before importing execute so streamPodLogsOnce picks up
|
||||
// the mocked getLogApi. The mock's logApi.log never resolves, simulating the
|
||||
// FAR-10 hang: K8s API drops the connection but the client awaits forever.
|
||||
const mockLogFn = vi.fn();
|
||||
vi.mock("./k8s-client.js", () => ({
|
||||
getLogApi: () => ({ log: mockLogFn }),
|
||||
getBatchApi: () => ({}),
|
||||
getCoreApi: () => ({}),
|
||||
getAuthzApi: () => ({}),
|
||||
getSelfPodInfo: vi.fn(),
|
||||
resetCache: vi.fn(),
|
||||
}));
|
||||
|
||||
const { isK8s404, buildPartialRunError, isReattachableOrphan, describePodTerminatedError, streamPodLogsOnce } = await import("./execute.js");
|
||||
|
||||
function makeJob(opts: {
|
||||
runId?: string;
|
||||
@@ -245,3 +260,73 @@ describe("describePodTerminatedError", () => {
|
||||
expect(msg).toContain("Error");
|
||||
});
|
||||
});
|
||||
|
||||
// Regression: FAR-10 hardening — streamPodLogsOnce must not hang forever when
|
||||
// the K8s client's logApi.log call never resolves. When stopSignal fires, the
|
||||
// bail timer must force-return within LOG_STREAM_BAIL_TIMEOUT_MS (3s in the
|
||||
// implementation) so execute() does not get stuck waiting for a dead stream.
|
||||
describe("streamPodLogsOnce bail timer", () => {
|
||||
beforeEach(() => {
|
||||
mockLogFn.mockReset();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns within the bail window when stopSignal fires during a hung log call", async () => {
|
||||
// logApi.log never resolves — simulates the FAR-10 hang where the K8s
|
||||
// response stream stalls without closing the connection.
|
||||
mockLogFn.mockImplementation((_ns, _pod, _ctr, _writable: Writable) => {
|
||||
return new Promise(() => { /* never resolves */ });
|
||||
});
|
||||
|
||||
const stopSignal = { stopped: false };
|
||||
const onLog = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const resultPromise = streamPodLogsOnce(
|
||||
"default",
|
||||
"mypod",
|
||||
onLog,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
stopSignal,
|
||||
);
|
||||
|
||||
// Fire stopSignal; let the 200ms poller tick and start the bail timer.
|
||||
stopSignal.stopped = true;
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
|
||||
// Advance past the 3s bail timeout. streamPodLogsOnce must now resolve
|
||||
// with an empty string (no chunks were captured) rather than hanging.
|
||||
await vi.advanceTimersByTimeAsync(3_100);
|
||||
|
||||
const result = await resultPromise;
|
||||
expect(result).toBe("");
|
||||
expect(mockLogFn).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("returns promptly if logApi.log resolves before stopSignal fires (happy path, no bail involved)", async () => {
|
||||
mockLogFn.mockImplementation(async (_ns, _pod, _ctr, _writable: Writable) => {
|
||||
// Resolve immediately — normal log-stream completion.
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const onLog = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
// No stopSignal → no bail machinery engaged.
|
||||
const result = await streamPodLogsOnce(
|
||||
"default",
|
||||
"mypod",
|
||||
onLog,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result).toBe("");
|
||||
expect(mockLogFn).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user