Skip to content

Commit cf371a4

Browse files
committed
test: fold gateway smoke into qa e2e
1 parent 606494a commit cf371a4

5 files changed

Lines changed: 153 additions & 175 deletions

File tree

packages/gateway-protocol/src/native-protocol-levels.guard.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ describe("native Gateway protocol levels", () => {
149149
});
150150

151151
it("uses the TypeScript source of truth for dev Gateway smoke scripts", async () => {
152-
const devScripts = ["scripts/dev/gateway-smoke.ts", "scripts/dev/ios-node-e2e.ts"];
152+
const devScripts = ["scripts/dev/ios-node-e2e.ts"];
153153
for (const relativePath of devScripts) {
154154
const content = await readRepoFile(relativePath);
155155
assertPattern(

scripts/dev/gateway-smoke.ts

Lines changed: 0 additions & 171 deletions
This file was deleted.

scripts/test-projects.test-support.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,6 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
651651
"scripts/dev/channel-message-flows.ts",
652652
["test/e2e/qa-lab/channels/channel-message-flows.e2e.test.ts"],
653653
],
654-
["scripts/dev/gateway-smoke.ts", ["test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts"]],
655654
["scripts/qa-otel-smoke.ts", ["test/e2e/qa-lab/runtime/qa-otel-smoke.e2e.test.ts"]],
656655
["scripts/bundled-plugin-assets.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],
657656
["scripts/bundle-a2ui.mjs", ["test/scripts/bundled-plugin-assets.test.ts"]],

test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,162 @@
22
import { createServer, type Server } from "node:http";
33
import { afterEach, describe, expect, it } from "vitest";
44
import { WebSocket, WebSocketServer } from "ws";
5-
import { runGatewaySmoke } from "../../../../scripts/dev/gateway-smoke.js";
5+
import {
6+
MIN_CLIENT_PROTOCOL_VERSION,
7+
PROTOCOL_VERSION,
8+
} from "../../../../packages/gateway-protocol/src/version.js";
9+
import {
10+
createGatewayWsClient,
11+
resolveGatewayUrl,
12+
} from "../../../../scripts/lib/gateway-ws-client.ts";
613

714
let server: Server | undefined;
815
let wss: WebSocketServer | undefined;
916

17+
type GatewaySmokeClient = ReturnType<typeof createGatewayWsClient>;
18+
19+
type GatewaySmokeDeps = {
20+
createClient?: typeof createGatewayWsClient;
21+
stderr?: (message: string) => void;
22+
stdout?: (message: string) => void;
23+
};
24+
25+
function writeStdoutLine(message: string): void {
26+
process.stdout.write(`${message}\n`);
27+
}
28+
29+
function writeStderrLine(message: string): void {
30+
process.stderr.write(`${message}\n`);
31+
}
32+
33+
function isRecord(value: unknown): value is Record<string, unknown> {
34+
return value !== null && typeof value === "object" && !Array.isArray(value);
35+
}
36+
37+
function hasHealthSummaryPayload(response: unknown): boolean {
38+
if (!isRecord(response) || !isRecord(response.payload)) {
39+
return false;
40+
}
41+
const { payload } = response;
42+
return (
43+
payload.ok === true &&
44+
typeof payload.ts === "number" &&
45+
typeof payload.durationMs === "number" &&
46+
typeof payload.defaultAgentId === "string" &&
47+
payload.defaultAgentId.trim() !== "" &&
48+
Array.isArray(payload.agents) &&
49+
isRecord(payload.channels) &&
50+
Array.isArray(payload.channelOrder) &&
51+
isRecord(payload.sessions)
52+
);
53+
}
54+
55+
function hasStringArray(value: unknown): value is string[] {
56+
return Array.isArray(value) && value.every((item) => typeof item === "string");
57+
}
58+
59+
function connectHelloScopes(response: unknown): string[] | null {
60+
if (!isRecord(response) || !isRecord(response.payload)) {
61+
return null;
62+
}
63+
const { payload } = response;
64+
if (
65+
payload.type !== "hello-ok" ||
66+
typeof payload.protocol !== "number" ||
67+
!isRecord(payload.features) ||
68+
!hasStringArray(payload.features.methods) ||
69+
!payload.features.methods.includes("health") ||
70+
!isRecord(payload.auth) ||
71+
payload.auth.role !== "operator" ||
72+
!hasStringArray(payload.auth.scopes)
73+
) {
74+
return null;
75+
}
76+
return payload.auth.scopes;
77+
}
78+
79+
function hasConnectHelloPayload(response: unknown): boolean {
80+
return connectHelloScopes(response) !== null;
81+
}
82+
83+
function hasUnpairedOperatorScopes(response: unknown): boolean {
84+
const scopes = connectHelloScopes(response);
85+
if (!scopes) {
86+
return false;
87+
}
88+
return scopes.length > 0;
89+
}
90+
91+
async function runGatewaySmoke(
92+
input: { token: string; urlRaw: string },
93+
deps: GatewaySmokeDeps = {},
94+
): Promise<number> {
95+
const url = resolveGatewayUrl(input.urlRaw);
96+
const createClient = deps.createClient ?? createGatewayWsClient;
97+
const stderr = deps.stderr ?? writeStderrLine;
98+
const stdout = deps.stdout ?? writeStdoutLine;
99+
const client: GatewaySmokeClient = createClient({
100+
url: url.toString(),
101+
onEvent: (evt) => {
102+
// Ignore noisy connect handshakes.
103+
void evt;
104+
},
105+
});
106+
const { request, waitOpen, close } = client;
107+
108+
try {
109+
await waitOpen();
110+
111+
// Match iOS "operator" session defaults: token auth, no device identity.
112+
const connectRes = await request("connect", {
113+
minProtocol: MIN_CLIENT_PROTOCOL_VERSION,
114+
maxProtocol: PROTOCOL_VERSION,
115+
client: {
116+
id: "openclaw-ios",
117+
displayName: "openclaw gateway smoke test",
118+
version: "dev",
119+
platform: "dev",
120+
mode: "ui",
121+
instanceId: "openclaw-dev-smoke",
122+
},
123+
locale: "en-US",
124+
userAgent: "gateway-smoke",
125+
role: "operator",
126+
scopes: ["operator.read", "operator.write", "operator.admin"],
127+
caps: [],
128+
auth: { token: input.token },
129+
});
130+
131+
if (!connectRes.ok) {
132+
stderr(`connect failed: ${String(connectRes.error)}`);
133+
return 2;
134+
}
135+
if (!hasConnectHelloPayload(connectRes)) {
136+
stderr("connect failed: missing hello-ok payload");
137+
return 2;
138+
}
139+
if (hasUnpairedOperatorScopes(connectRes)) {
140+
stderr("connect failed: unpaired iOS smoke unexpectedly received operator scopes");
141+
return 2;
142+
}
143+
144+
const healthRes = await request("health");
145+
if (!healthRes.ok) {
146+
stderr(`health failed: ${String(healthRes.error)}`);
147+
return 3;
148+
}
149+
if (!hasHealthSummaryPayload(healthRes)) {
150+
stderr("health failed: missing health summary payload");
151+
return 3;
152+
}
153+
154+
stdout("ok: connected + health");
155+
return 0;
156+
} finally {
157+
close();
158+
}
159+
}
160+
10161
afterEach(async () => {
11162
await new Promise<void>((resolve) => {
12163
wss?.close(() => resolve());

test/scripts/test-projects.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,6 @@ describe("scripts/test-projects changed-target routing", () => {
615615
"scripts/dev/channel-message-flows.ts",
616616
["test/e2e/qa-lab/channels/channel-message-flows.e2e.test.ts"],
617617
],
618-
["scripts/dev/gateway-smoke.ts", ["test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts"]],
619618
["scripts/qa-otel-smoke.ts", ["test/e2e/qa-lab/runtime/qa-otel-smoke.e2e.test.ts"]],
620619
[
621620
"scripts/e2e/lib/plugin-lifecycle-matrix/sweep.sh",

0 commit comments

Comments
 (0)