mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(qa-lab): keep suite HTTP probes within deadlines (#108064)
* fix(qa-lab): bound suite HTTP probes * style(qa-lab): format gateway timeout proof * test(qa-lab): pin media deadline request count
This commit is contained in:
@@ -93,6 +93,9 @@ describe("qa suite runtime agent media helpers", () => {
|
||||
timeoutMs: 2_000,
|
||||
}),
|
||||
).resolves.toBe("/tmp/generated.png");
|
||||
expect(fetchJsonMock).toHaveBeenCalledOnce();
|
||||
expect(fetchJsonMock).toHaveBeenCalledWith(expect.any(String), expect.any(Number));
|
||||
expect(fetchJsonMock.mock.calls[0]?.[1]).toBeLessThanOrEqual(2_000);
|
||||
});
|
||||
|
||||
it("falls back to generated image files under the gateway temp root", async () => {
|
||||
|
||||
@@ -86,12 +86,13 @@ async function resolveGeneratedImagePath(params: {
|
||||
startedAtMs: number;
|
||||
timeoutMs: number;
|
||||
}) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < params.timeoutMs) {
|
||||
const deadline = Date.now() + params.timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (params.env.mock) {
|
||||
try {
|
||||
const requests = await fetchJson<Array<{ allInputText?: string; toolOutput?: string }>>(
|
||||
`${params.env.mock.baseUrl}/debug/requests`,
|
||||
Math.max(1, deadline - Date.now()),
|
||||
);
|
||||
for (const request of requests.toReversed()) {
|
||||
if (!(request.allInputText ?? "").includes(params.promptSnippet)) {
|
||||
@@ -135,9 +136,12 @@ async function resolveGeneratedImagePath(params: {
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 250);
|
||||
});
|
||||
const remainingMs = deadline - Date.now();
|
||||
if (remainingMs > 0) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, Math.min(250, remainingMs));
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new Error(`timed out after ${params.timeoutMs}ms`);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
patchConfig,
|
||||
restartGatewayWithConfigPatch,
|
||||
waitForConfigRestartSettle,
|
||||
waitForGatewayHealthy,
|
||||
} from "./suite-runtime-gateway.js";
|
||||
import type { QaSuiteRuntimeEnv } from "./suite-runtime-types.js";
|
||||
|
||||
@@ -122,9 +123,63 @@ describe("qa suite gateway helpers", () => {
|
||||
await expect(fetchJson("http://127.0.0.1:43123/config")).rejects.toThrow(
|
||||
"qa-lab-suite-fetch-json: JSON response exceeds 16777216 bytes",
|
||||
);
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeoutMs: 15_000 }),
|
||||
);
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("bounds stalled suite gateway JSON response bodies", async () => {
|
||||
vi.useFakeTimers();
|
||||
const release = vi.fn(async () => {});
|
||||
fetchWithSsrFGuardMock.mockImplementation(async ({ timeoutMs }: { timeoutMs: number }) => {
|
||||
let bodyController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
||||
const response = new Response(
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
bodyController = controller;
|
||||
controller.enqueue(new TextEncoder().encode('{"pending":'));
|
||||
},
|
||||
}),
|
||||
{ headers: { "content-type": "application/json" } },
|
||||
);
|
||||
setTimeout(() => bodyController?.error(new Error("request timed out")), timeoutMs);
|
||||
return { response, release };
|
||||
});
|
||||
|
||||
const request = fetchJson("http://127.0.0.1:43123/config", 1_000);
|
||||
const rejection = expect(request).rejects.toThrow("request timed out");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await rejection;
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeoutMs: 1_000 }),
|
||||
);
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("bounds a hung gateway health request by the remaining readiness deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
fetchWithSsrFGuardMock.mockImplementation(
|
||||
async ({ timeoutMs }: { timeoutMs: number }) =>
|
||||
await new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("request timed out")), timeoutMs);
|
||||
}),
|
||||
);
|
||||
|
||||
const readiness = waitForGatewayHealthy(
|
||||
{ gateway: { baseUrl: "http://127.0.0.1:43123" } } as never,
|
||||
1_000,
|
||||
);
|
||||
const rejection = expect(readiness).rejects.toThrow("timed out after 1000ms");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await rejection;
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeoutMs: 1_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips config mutations that would not change the snapshot", async () => {
|
||||
const config = {
|
||||
tools: {
|
||||
|
||||
@@ -16,10 +16,13 @@ type QaGatewayMutationEnv = Pick<
|
||||
"gateway" | "transport" | "providerMode" | "primaryModel" | "alternateModel"
|
||||
>;
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const QA_SUITE_FETCH_JSON_TIMEOUT_MS = 15_000;
|
||||
|
||||
async function fetchJson<T>(url: string, timeoutMs = QA_SUITE_FETCH_JSON_TIMEOUT_MS): Promise<T> {
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url,
|
||||
policy: { allowPrivateNetwork: true },
|
||||
timeoutMs,
|
||||
auditContext: "qa-lab-suite-fetch-json",
|
||||
});
|
||||
try {
|
||||
@@ -33,12 +36,13 @@ async function fetchJson<T>(url: string): Promise<T> {
|
||||
}
|
||||
|
||||
async function waitForGatewayHealthy(env: Pick<QaSuiteRuntimeEnv, "gateway">, timeoutMs = 45_000) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: `${env.gateway.baseUrl}/readyz`,
|
||||
policy: { allowPrivateNetwork: true },
|
||||
timeoutMs: Math.max(1, deadline - Date.now()),
|
||||
auditContext: "qa-lab-suite-wait-for-gateway-healthy",
|
||||
});
|
||||
try {
|
||||
@@ -51,7 +55,10 @@ async function waitForGatewayHealthy(env: Pick<QaSuiteRuntimeEnv, "gateway">, ti
|
||||
} catch {
|
||||
// retry
|
||||
}
|
||||
await sleep(250);
|
||||
const remainingMs = deadline - Date.now();
|
||||
if (remainingMs > 0) {
|
||||
await sleep(Math.min(250, remainingMs));
|
||||
}
|
||||
}
|
||||
throw new QaSuiteInfraError("gateway_ready_timeout", `timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
@@ -213,6 +213,36 @@ describe("qa suite", () => {
|
||||
expect(stop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("bounds a hung lab readiness request by the remaining startup deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
const stop = vi.fn(async () => {});
|
||||
fetchWithSsrFGuardMock.mockImplementation(
|
||||
async ({ timeoutMs }: { timeoutMs: number }) =>
|
||||
await new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("request timed out")), timeoutMs);
|
||||
}),
|
||||
);
|
||||
|
||||
const readiness = qaSuiteProgressTesting.waitForQaLabReadyOrStopOwned({
|
||||
lab: {
|
||||
listenUrl: "http://127.0.0.1:43123",
|
||||
stop,
|
||||
},
|
||||
ownsLab: true,
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
const rejection = expect(readiness).rejects.toThrow(
|
||||
"timed out after 1000ms waiting for qa-lab ready",
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await rejection;
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeoutMs: 1_000 }),
|
||||
);
|
||||
expect(stop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("leaves caller-owned labs running when readiness never becomes healthy", async () => {
|
||||
const stop = vi.fn(async () => {});
|
||||
fetchWithSsrFGuardMock.mockResolvedValue({
|
||||
|
||||
@@ -264,12 +264,13 @@ function formatQaSuiteRunStartProgress(params: {
|
||||
}
|
||||
|
||||
async function waitForQaLabReady(baseUrl: string, timeoutMs = 10_000) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: `${baseUrl}/readyz`,
|
||||
policy: { allowPrivateNetwork: true },
|
||||
timeoutMs: Math.max(1, deadline - Date.now()),
|
||||
auditContext: "qa-lab-suite-wait-for-lab-ready",
|
||||
});
|
||||
try {
|
||||
@@ -282,7 +283,10 @@ async function waitForQaLabReady(baseUrl: string, timeoutMs = 10_000) {
|
||||
} catch {
|
||||
// retry
|
||||
}
|
||||
await sleep(100);
|
||||
const remainingMs = deadline - Date.now();
|
||||
if (remainingMs > 0) {
|
||||
await sleep(Math.min(100, remainingMs));
|
||||
}
|
||||
}
|
||||
throw new Error(`timed out after ${timeoutMs}ms waiting for qa-lab ready`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user