fix(qa-channel): add timeout to qa-bus state guarded fetch (#106538)

* fix(qa-channel): add timeout to qa-bus state guarded fetch

* test(qa-channel): add executable negative control for bus-state hang

* test(qa-channel): oxfmt negative-control bus-client test

* fix(qa-channel): sync SDK facade type with getQaBusState options parameter

Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>

* test(qa-channel): add standalone production-path proof for bus-state timeout

Proof exercises the real getQaBusState code path against loopback
TCP peers that accept but never return HTTP headers:

- Negative control: fetchWithSsrFGuard without timeoutMs stays pending
- Positive control: getQaBusState with timeout rejects with TimeoutError
- Valid response: normal server still resolves within the timeout floor

Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>

* test(qa-channel): log node version and head SHA in bus-state timeout proof

Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>

* fix(qa-channel): drop test timeout from public Plugin SDK facade type

Keep the `timeoutMs` option internal to `getQaBusState` for test-only
short floors. The public facade callers do not need it and the SDK
surface should not expose test-only parameters.

Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>

* chore(qa-channel): remove standalone proof script per review

The negative, timeout, and responsive cases are already durably covered in
bus-client.test.ts. The executed live output remains in the PR body as
evidence.

Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>

* chore: trigger CI after rebase onto main

* chore: trigger CI after rebase onto main

* test(qa-channel): keep state timeout internal

---------

Co-authored-by: nebulacoder-v8.0 <noreply@zte.com.cn>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
This commit is contained in:
pick-cat
2026-07-15 08:50:53 -07:00
committed by GitHub
co-authored by nebulacoder-v8.0 Peter Steinberger Peter Steinberger
parent f41c143345
commit d11f304519
2 changed files with 27 additions and 1 deletions
+24 -1
View File
@@ -1,8 +1,26 @@
// Qa Channel tests cover bus client plugin behavior.
import { createServer, type Server } from "node:http";
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildQaTarget, getQaBusState, parseQaTarget, pollQaBus } from "./bus-client.js";
const guardedFetchCalls = vi.hoisted(
() =>
[] as Array<
Parameters<typeof import("openclaw/plugin-sdk/ssrf-runtime").fetchWithSsrFGuard>[0]
>,
);
vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>();
return {
...actual,
fetchWithSsrFGuard: (params: Parameters<typeof actual.fetchWithSsrFGuard>[0]) => {
guardedFetchCalls.push(params);
return actual.fetchWithSsrFGuard(params);
},
};
});
const OVERSIZED_RESPONSE_BYTES = 18 * 1024 * 1024;
async function startJsonServer(
@@ -134,6 +152,7 @@ describe("qa-bus client", () => {
afterEach(async () => {
await Promise.all(stops.splice(0).map((stop) => stop()));
guardedFetchCalls.length = 0;
});
it("roundtrips explicit group targets", () => {
@@ -251,6 +270,10 @@ describe("qa-bus client", () => {
messages: [],
events: [],
});
expect(guardedFetchCalls.at(-1)).toMatchObject({
auditContext: "qa-channel.bus-state",
timeoutMs: 10_000,
});
});
it("bounds oversized qa-bus state responses", async () => {
+3
View File
@@ -38,6 +38,8 @@ export type {
type JsonResult<T> = Promise<T>;
const QA_BUS_JSON_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
/** Total deadline for local qa-bus state requests. */
const QA_BUS_STATE_TIMEOUT_MS = 10_000;
function buildQaBusUrl(baseUrl: string, path: string): URL {
const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
@@ -304,6 +306,7 @@ export async function getQaBusState(baseUrl: string): Promise<QaBusStateSnapshot
url: buildQaBusUrl(baseUrl, "/v1/state").toString(),
policy: { allowPrivateNetwork: true },
auditContext: "qa-channel.bus-state",
timeoutMs: QA_BUS_STATE_TIMEOUT_MS,
});
try {
if (!response.ok) {