fix(qa): cancel Docker health probe bodies

This commit is contained in:
Vincent Koc
2026-06-19 08:33:26 +02:00
parent 5697ab810e
commit ebee101d30
2 changed files with 81 additions and 4 deletions
+62 -1
View File
@@ -1,8 +1,8 @@
import { createServer } from "node:net";
/**
* Tests QA runtime command loading and private CLI gating.
*/
import { Command } from "commander";
import { createServer } from "node:net";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
cleanupTempDirs,
@@ -60,6 +60,21 @@ describe("plugin-sdk qa-runtime", () => {
};
}
function cancelTrackedFetchResponse(ok = true) {
let canceled = false;
return {
response: {
ok,
body: {
cancel: vi.fn(async () => {
canceled = true;
}),
},
},
wasCanceled: () => canceled,
};
}
it("stays cold until the runtime seam is used", async () => {
const module = await import("./qa-runtime.js");
@@ -341,6 +356,52 @@ describe("plugin-sdk qa-runtime", () => {
expect(fetchImpl).toHaveBeenCalledWith("http://172.18.0.4:18789/healthz");
});
it("cancels compose service health probe response bodies", async () => {
const module = await import("./qa-runtime.js");
const runtime = module.createQaDockerRuntime({ auditContext: "qa-test" });
const runCommand = vi.fn(async (_command: string, args: string[]) => {
if (args.includes("ps")) {
return { stdout: "qa-gateway-one\n", stderr: "" };
}
return { stdout: "172.18.0.4\n", stderr: "" };
});
const probe = cancelTrackedFetchResponse(true);
const fetchImpl = vi.fn(async () => probe.response);
await expect(
runtime.resolveComposeServiceUrl(
"gateway",
18789,
"/tmp/docker-compose.yml",
"/repo",
runCommand,
fetchImpl,
),
).resolves.toBe("http://172.18.0.4:18789/");
expect(probe.wasCanceled()).toBe(true);
});
it("cancels waitForHealth response bodies after each probe", async () => {
const module = await import("./qa-runtime.js");
const runtime = module.createQaDockerRuntime({ auditContext: "qa-test" });
const first = cancelTrackedFetchResponse(false);
const second = cancelTrackedFetchResponse(true);
const responses = [first.response, second.response];
const fetchImpl = vi.fn(async () => responses.shift() ?? second.response);
const sleepImpl = vi.fn(async () => {});
await runtime.waitForHealth("http://127.0.0.1:18789/healthz", {
fetchImpl,
sleepImpl,
timeoutMs: 1000,
pollMs: 1,
});
expect(fetchImpl).toHaveBeenCalledTimes(2);
expect(first.wasCanceled()).toBe(true);
expect(second.wasCanceled()).toBe(true);
});
it("resolves an unpinned QA Docker host port away from an occupied loopback default", async () => {
const module = await import("./qa-runtime.js");
const reservation = await occupyLoopbackPort();
+19 -3
View File
@@ -249,7 +249,11 @@ export type QaDockerRunCommand = (
) => Promise<{ stdout: string; stderr: string }>;
/** Minimal fetch-like health probe used by QA Docker runtime helpers. */
export type QaDockerFetchLike = (input: string) => Promise<{ ok: boolean }>;
export type QaDockerFetchResponse = {
ok: boolean;
body?: { cancel?: () => unknown } | null;
};
export type QaDockerFetchLike = (input: string) => Promise<QaDockerFetchResponse>;
const DEFAULT_QA_DOCKER_COMMAND_TIMEOUT_MS = 120_000;
@@ -484,14 +488,23 @@ function parseDockerComposePsRows(stdout: string) {
}
async function isQaDockerHealthy(url: string, fetchImpl: QaDockerFetchLike) {
let response: QaDockerFetchResponse | undefined;
try {
const response = await fetchImpl(url);
response = await fetchImpl(url);
return response.ok;
} catch {
return false;
} finally {
await releaseQaDockerFetchResponse(response);
}
}
async function releaseQaDockerFetchResponse(response: QaDockerFetchResponse | undefined) {
try {
await response?.body?.cancel?.();
} catch {}
}
/** Create Docker command, health-check, and compose helpers for QA harnesses. */
export function createQaDockerRuntime(params: {
auditContext: string;
@@ -548,14 +561,17 @@ export function createQaDockerRuntime(params: {
let lastError: unknown = null;
while (Date.now() < deadline) {
let response: QaDockerFetchResponse | undefined;
try {
const response = await deps.fetchImpl(url);
response = await deps.fetchImpl(url);
if (response.ok) {
return;
}
lastError = new Error(`Health check returned non-OK for ${url}`);
} catch (error) {
lastError = error;
} finally {
await releaseQaDockerFetchResponse(response);
}
await deps.sleepImpl(pollMs);
}