fix(qa): validate rpc rtt smoke payloads

This commit is contained in:
Vincent Koc
2026-06-07 07:09:01 +02:00
parent fa614d0907
commit 7e7ea0fed1
2 changed files with 141 additions and 3 deletions

View File

@@ -474,6 +474,92 @@ export function summarizeRttSamples(samples) {
};
}
function isRecord(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function assertPayloadObject(method, payload) {
if (!isRecord(payload)) {
throw new Error(`${method} returned invalid payload: expected object.`);
}
return payload;
}
function assertHealthSmokePayload(payload) {
const summary = assertPayloadObject("health", payload);
if (summary.ok !== true) {
throw new Error("health returned invalid payload: expected ok=true.");
}
if (!Number.isFinite(summary.ts)) {
throw new Error("health returned invalid payload: expected numeric ts.");
}
if (!Number.isFinite(summary.durationMs)) {
throw new Error("health returned invalid payload: expected numeric durationMs.");
}
if (typeof summary.defaultAgentId !== "string" || summary.defaultAgentId.trim() === "") {
throw new Error("health returned invalid payload: expected defaultAgentId.");
}
if (!Array.isArray(summary.agents)) {
throw new Error("health returned invalid payload: expected agents array.");
}
if (!isRecord(summary.channels)) {
throw new Error("health returned invalid payload: expected channels object.");
}
if (!Array.isArray(summary.channelOrder)) {
throw new Error("health returned invalid payload: expected channelOrder array.");
}
if (!isRecord(summary.sessions)) {
throw new Error("health returned invalid payload: expected sessions object.");
}
}
function assertConfigGetSmokePayload(payload) {
const snapshot = assertPayloadObject("config.get", payload);
if (typeof snapshot.path !== "string" || snapshot.path.trim() === "") {
throw new Error("config.get returned invalid payload: expected config path.");
}
if (typeof snapshot.exists !== "boolean") {
throw new Error("config.get returned invalid payload: expected exists boolean.");
}
if (typeof snapshot.valid !== "boolean") {
throw new Error("config.get returned invalid payload: expected valid boolean.");
}
if (!isRecord(snapshot.sourceConfig)) {
throw new Error("config.get returned invalid payload: expected sourceConfig object.");
}
if (!isRecord(snapshot.resolved)) {
throw new Error("config.get returned invalid payload: expected resolved object.");
}
if (!isRecord(snapshot.runtimeConfig)) {
throw new Error("config.get returned invalid payload: expected runtimeConfig object.");
}
if (!isRecord(snapshot.config)) {
throw new Error("config.get returned invalid payload: expected config object.");
}
if (!Array.isArray(snapshot.issues)) {
throw new Error("config.get returned invalid payload: expected issues array.");
}
if (!Array.isArray(snapshot.warnings)) {
throw new Error("config.get returned invalid payload: expected warnings array.");
}
if (!Array.isArray(snapshot.legacyIssues)) {
throw new Error("config.get returned invalid payload: expected legacyIssues array.");
}
}
export function assertRpcSmokeResponse(method, response) {
if (!response?.ok) {
throw new Error(`${method} failed: ${JSON.stringify(response?.error)}`);
}
if (method === "health") {
assertHealthSmokePayload(response.payload);
return;
}
if (method === "config.get") {
assertConfigGetSmokePayload(response.payload);
}
}
function toText(data) {
if (typeof data === "string") {
return data;
@@ -720,9 +806,7 @@ async function main() {
const response = await client.request(method, {}, 10_000);
const durationMs = performance.now() - requestStartedAtMs;
const roundedDurationMs = roundMeasuredMs(durationMs, `${method} durationMs`);
if (!response.ok) {
throw new Error(`${method} failed: ${JSON.stringify(response.error)}`);
}
assertRpcSmokeResponse(method, response);
samples.push({ method, durationMs });
events.push({
event: "gateway-rpc",

View File

@@ -2,6 +2,7 @@
import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
import {
assertRpcSmokeResponse,
cleanupTempRoot,
createGatewayClient,
installGatewayParentCleanup,
@@ -134,6 +135,59 @@ describe("scripts/measure-rpc-rtt.mjs", () => {
);
});
it("validates default RPC RTT smoke payloads", () => {
expect(() =>
assertRpcSmokeResponse("health", {
ok: true,
payload: {
agents: [],
channelOrder: [],
channels: {},
defaultAgentId: "codex",
durationMs: 3,
ok: true,
sessions: { count: 0, path: "/state/sessions", recent: [] },
ts: Date.now(),
},
}),
).not.toThrow();
expect(() =>
assertRpcSmokeResponse("config.get", {
ok: true,
payload: {
config: {},
exists: true,
issues: [],
legacyIssues: [],
path: "/tmp/openclaw.json",
resolved: {},
runtimeConfig: {},
sourceConfig: {},
valid: true,
warnings: [],
},
}),
).not.toThrow();
expect(() => assertRpcSmokeResponse("health", { ok: true, payload: {} })).toThrow(
"health returned invalid payload: expected ok=true.",
);
expect(() => assertRpcSmokeResponse("config.get", { ok: true, payload: {} })).toThrow(
"config.get returned invalid payload: expected config path.",
);
});
it("keeps custom RPC RTT methods on the generic ok/error contract", () => {
expect(() => assertRpcSmokeResponse("custom.method", { ok: true })).not.toThrow();
expect(() =>
assertRpcSmokeResponse("custom.method", {
error: { code: "bad_request" },
ok: false,
}),
).toThrow('custom.method failed: {"code":"bad_request"}');
});
it("closes parent gateway log handles after spawning", async () => {
const child = Object.assign(new EventEmitter(), {
exitCode: null,