Skip to content

Commit 7e7ea0f

Browse files
committed
fix(qa): validate rpc rtt smoke payloads
1 parent fa614d0 commit 7e7ea0f

2 files changed

Lines changed: 141 additions & 3 deletions

File tree

scripts/measure-rpc-rtt.mjs

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,92 @@ export function summarizeRttSamples(samples) {
474474
};
475475
}
476476

477+
function isRecord(value) {
478+
return value !== null && typeof value === "object" && !Array.isArray(value);
479+
}
480+
481+
function assertPayloadObject(method, payload) {
482+
if (!isRecord(payload)) {
483+
throw new Error(`${method} returned invalid payload: expected object.`);
484+
}
485+
return payload;
486+
}
487+
488+
function assertHealthSmokePayload(payload) {
489+
const summary = assertPayloadObject("health", payload);
490+
if (summary.ok !== true) {
491+
throw new Error("health returned invalid payload: expected ok=true.");
492+
}
493+
if (!Number.isFinite(summary.ts)) {
494+
throw new Error("health returned invalid payload: expected numeric ts.");
495+
}
496+
if (!Number.isFinite(summary.durationMs)) {
497+
throw new Error("health returned invalid payload: expected numeric durationMs.");
498+
}
499+
if (typeof summary.defaultAgentId !== "string" || summary.defaultAgentId.trim() === "") {
500+
throw new Error("health returned invalid payload: expected defaultAgentId.");
501+
}
502+
if (!Array.isArray(summary.agents)) {
503+
throw new Error("health returned invalid payload: expected agents array.");
504+
}
505+
if (!isRecord(summary.channels)) {
506+
throw new Error("health returned invalid payload: expected channels object.");
507+
}
508+
if (!Array.isArray(summary.channelOrder)) {
509+
throw new Error("health returned invalid payload: expected channelOrder array.");
510+
}
511+
if (!isRecord(summary.sessions)) {
512+
throw new Error("health returned invalid payload: expected sessions object.");
513+
}
514+
}
515+
516+
function assertConfigGetSmokePayload(payload) {
517+
const snapshot = assertPayloadObject("config.get", payload);
518+
if (typeof snapshot.path !== "string" || snapshot.path.trim() === "") {
519+
throw new Error("config.get returned invalid payload: expected config path.");
520+
}
521+
if (typeof snapshot.exists !== "boolean") {
522+
throw new Error("config.get returned invalid payload: expected exists boolean.");
523+
}
524+
if (typeof snapshot.valid !== "boolean") {
525+
throw new Error("config.get returned invalid payload: expected valid boolean.");
526+
}
527+
if (!isRecord(snapshot.sourceConfig)) {
528+
throw new Error("config.get returned invalid payload: expected sourceConfig object.");
529+
}
530+
if (!isRecord(snapshot.resolved)) {
531+
throw new Error("config.get returned invalid payload: expected resolved object.");
532+
}
533+
if (!isRecord(snapshot.runtimeConfig)) {
534+
throw new Error("config.get returned invalid payload: expected runtimeConfig object.");
535+
}
536+
if (!isRecord(snapshot.config)) {
537+
throw new Error("config.get returned invalid payload: expected config object.");
538+
}
539+
if (!Array.isArray(snapshot.issues)) {
540+
throw new Error("config.get returned invalid payload: expected issues array.");
541+
}
542+
if (!Array.isArray(snapshot.warnings)) {
543+
throw new Error("config.get returned invalid payload: expected warnings array.");
544+
}
545+
if (!Array.isArray(snapshot.legacyIssues)) {
546+
throw new Error("config.get returned invalid payload: expected legacyIssues array.");
547+
}
548+
}
549+
550+
export function assertRpcSmokeResponse(method, response) {
551+
if (!response?.ok) {
552+
throw new Error(`${method} failed: ${JSON.stringify(response?.error)}`);
553+
}
554+
if (method === "health") {
555+
assertHealthSmokePayload(response.payload);
556+
return;
557+
}
558+
if (method === "config.get") {
559+
assertConfigGetSmokePayload(response.payload);
560+
}
561+
}
562+
477563
function toText(data) {
478564
if (typeof data === "string") {
479565
return data;
@@ -720,9 +806,7 @@ async function main() {
720806
const response = await client.request(method, {}, 10_000);
721807
const durationMs = performance.now() - requestStartedAtMs;
722808
const roundedDurationMs = roundMeasuredMs(durationMs, `${method} durationMs`);
723-
if (!response.ok) {
724-
throw new Error(`${method} failed: ${JSON.stringify(response.error)}`);
725-
}
809+
assertRpcSmokeResponse(method, response);
726810
samples.push({ method, durationMs });
727811
events.push({
728812
event: "gateway-rpc",

test/scripts/measure-rpc-rtt.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { EventEmitter } from "node:events";
33
import { describe, expect, it, vi } from "vitest";
44
import {
5+
assertRpcSmokeResponse,
56
cleanupTempRoot,
67
createGatewayClient,
78
installGatewayParentCleanup,
@@ -134,6 +135,59 @@ describe("scripts/measure-rpc-rtt.mjs", () => {
134135
);
135136
});
136137

138+
it("validates default RPC RTT smoke payloads", () => {
139+
expect(() =>
140+
assertRpcSmokeResponse("health", {
141+
ok: true,
142+
payload: {
143+
agents: [],
144+
channelOrder: [],
145+
channels: {},
146+
defaultAgentId: "codex",
147+
durationMs: 3,
148+
ok: true,
149+
sessions: { count: 0, path: "/state/sessions", recent: [] },
150+
ts: Date.now(),
151+
},
152+
}),
153+
).not.toThrow();
154+
155+
expect(() =>
156+
assertRpcSmokeResponse("config.get", {
157+
ok: true,
158+
payload: {
159+
config: {},
160+
exists: true,
161+
issues: [],
162+
legacyIssues: [],
163+
path: "/tmp/openclaw.json",
164+
resolved: {},
165+
runtimeConfig: {},
166+
sourceConfig: {},
167+
valid: true,
168+
warnings: [],
169+
},
170+
}),
171+
).not.toThrow();
172+
173+
expect(() => assertRpcSmokeResponse("health", { ok: true, payload: {} })).toThrow(
174+
"health returned invalid payload: expected ok=true.",
175+
);
176+
expect(() => assertRpcSmokeResponse("config.get", { ok: true, payload: {} })).toThrow(
177+
"config.get returned invalid payload: expected config path.",
178+
);
179+
});
180+
181+
it("keeps custom RPC RTT methods on the generic ok/error contract", () => {
182+
expect(() => assertRpcSmokeResponse("custom.method", { ok: true })).not.toThrow();
183+
expect(() =>
184+
assertRpcSmokeResponse("custom.method", {
185+
error: { code: "bad_request" },
186+
ok: false,
187+
}),
188+
).toThrow('custom.method failed: {"code":"bad_request"}');
189+
});
190+
137191
it("closes parent gateway log handles after spawning", async () => {
138192
const child = Object.assign(new EventEmitter(), {
139193
exitCode: null,

0 commit comments

Comments
 (0)