mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 05:36:10 +00:00
* chore(types): add declaration files for scripts/lib and scripts/e2e modules
* chore(types): add declaration files for top-level script modules (a-m)
* chore(types): add declaration files for top-level script modules (n-z)
* test: use a non-secret-shaped gateway token fixture
* test: type ci workflow guard helpers for the root test lane
* chore(tooling): typecheck root test/** with a dedicated tsgo lane
- test/tsconfig/tsconfig.test.root.json: root-test program (strict unused checks,
fixtures excluded; two Docker E2E clients that import built dist/** stay out,
same rationale as the scripts/e2e exclusion in tsconfig.scripts.json)
- tsgo:test:root wired into tsgo:test, check:test-types, scripts/check.mjs, and
the ci.yml test-types shard, mirroring the tsgo:scripts lane (#104348)
- changed-lane routing: test/**/*.ts (excluding fixtures) and the lane tsconfig
now trigger 'typecheck test root' in check:changed; previously test/ paths ran
lint only, so harness type errors surfaced first in CI (#104287 envDir case)
- burn down all 1071 latent type errors in the program: precise param/local
types across test/scripts, test/vitest, test/e2e, and transitive scripts/e2e
program members; 205 sibling .d.mts declaration files for imported .mjs
modules (committed separately); zero any, zero ts-expect-error
- resolve the pre-existing testing star-export ambiguity in
scripts/e2e/parallels/common.ts with an explicit re-export
Closes #104388
* chore(types): correct declaration fidelity per structured review
- re-derive 51 .d.mts files from implementation data flow instead of
initializers: fix a wrong never return (runTestProjectsDelegation returns
the child), add encoding-sensitive exec/spawn overloads (plain-gh), restore
the full release profile union, make parsed paths string | null, add missing
parseArgs fields via help/non-help unions, add a missing sibling declaration
(budget-number-args), drop 15 unused lint directives
- precise install-record/tuple typing removes the type-aware oxlint
regressions the first declarations caused in scripts/e2e implementations
- route .mts declaration edits under test/ to the testRoot lane and reference
the test-root project from tsconfig.projects.json so tsgo:all covers it
(closes both review findings against the lane wiring)
* chore(scripts): keep telegram runner dist typing structural for the boundary guard
* chore(types): declare runtime pack and gateway readiness exports added on main
* test: pin the importTargetPlan form of the plugin-contract plan import
The guard expectation still referenced the raw await import( form that
7ae5996bb3 (#103975) replaced with the importTargetPlan fallback helper;
the assertion fails on current main.
183 lines
5.3 KiB
TypeScript
183 lines
5.3 KiB
TypeScript
// Gateway Ws Client tests cover gateway ws client script behavior.
|
|
import { createServer, type Server } from "node:http";
|
|
import type { Duplex } from "node:stream";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { WebSocket, WebSocketServer } from "ws";
|
|
import { createGatewayWsClient } from "../../scripts/dev/gateway-ws-client.js";
|
|
|
|
let server: Server | undefined;
|
|
let wss: WebSocketServer | undefined;
|
|
|
|
afterEach(async () => {
|
|
await new Promise<void>((resolve) => {
|
|
wss?.close(() => resolve());
|
|
if (!wss) {
|
|
resolve();
|
|
}
|
|
});
|
|
wss = undefined;
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
server?.close((error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
if (!server) {
|
|
resolve();
|
|
}
|
|
});
|
|
server = undefined;
|
|
});
|
|
|
|
async function listen(handler: (ws: WebSocket) => void): Promise<string> {
|
|
server = createServer();
|
|
wss = new WebSocketServer({ server });
|
|
wss.on("connection", handler);
|
|
await new Promise<void>((resolve) => {
|
|
server?.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") {
|
|
throw new Error("test websocket server did not get a TCP address");
|
|
}
|
|
return `ws://127.0.0.1:${address.port}`;
|
|
}
|
|
|
|
async function listenStalledUpgrade(): Promise<{ close: () => Promise<void>; url: string }> {
|
|
const stalledServer = createServer();
|
|
const sockets = new Set<Duplex>();
|
|
let closing = false;
|
|
stalledServer.on("connection", (socket) => {
|
|
sockets.add(socket);
|
|
socket.once("close", () => {
|
|
sockets.delete(socket);
|
|
});
|
|
if (closing) {
|
|
socket.destroy();
|
|
}
|
|
});
|
|
stalledServer.on("upgrade", (_req, socket) => {
|
|
// Keep the socket open without completing the websocket handshake.
|
|
sockets.add(socket);
|
|
if (closing) {
|
|
socket.destroy();
|
|
}
|
|
});
|
|
await new Promise<void>((resolve) => {
|
|
stalledServer.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
const address = stalledServer.address();
|
|
if (!address || typeof address === "string") {
|
|
throw new Error("test websocket server did not get a TCP address");
|
|
}
|
|
return {
|
|
close: async () => {
|
|
closing = true;
|
|
for (const socket of sockets) {
|
|
socket.destroy();
|
|
}
|
|
await new Promise<void>((resolve, reject) => {
|
|
stalledServer.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
},
|
|
url: `ws://127.0.0.1:${address.port}`,
|
|
};
|
|
}
|
|
|
|
describe("createGatewayWsClient", () => {
|
|
it("rejects pending RPC requests when the client closes", async () => {
|
|
const url = await listen(() => {});
|
|
const client = createGatewayWsClient({ url });
|
|
await client.waitOpen();
|
|
|
|
const pending = client.request("health", {}, 1000);
|
|
client.close();
|
|
|
|
await expect(pending).rejects.toThrow("gateway websocket client closed");
|
|
});
|
|
|
|
it("rejects pending RPC requests when the gateway closes the socket", async () => {
|
|
const url = await listen((ws) => {
|
|
ws.on("message", () => {
|
|
ws.close(1011, "boom");
|
|
});
|
|
});
|
|
const client = createGatewayWsClient({ url });
|
|
await client.waitOpen();
|
|
|
|
await expect(client.request("health", {}, 1000)).rejects.toThrow(
|
|
"gateway websocket closed (1011): boom",
|
|
);
|
|
client.close();
|
|
});
|
|
|
|
it("rejects pending RPC requests when the socket errors", async () => {
|
|
const url = await listen(() => {});
|
|
const client = createGatewayWsClient({ url });
|
|
await client.waitOpen();
|
|
|
|
const pending = client.request("health", {}, 1000);
|
|
client.ws.emit("error", new Error("socket exploded"));
|
|
|
|
await expect(pending).rejects.toThrow("socket exploded");
|
|
client.close();
|
|
});
|
|
|
|
it("rejects websocket closes before opening", async () => {
|
|
const stalled = await listenStalledUpgrade();
|
|
const client = createGatewayWsClient({ openTimeoutMs: 1000, url: stalled.url });
|
|
const opened = client.waitOpen();
|
|
|
|
client.ws.emit("close", 1006, Buffer.from("bye"));
|
|
|
|
try {
|
|
await expect(opened).rejects.toThrow("closed before open (1006): bye");
|
|
} finally {
|
|
client.close();
|
|
await stalled.close();
|
|
}
|
|
});
|
|
|
|
it("terminates stalled websocket handshakes after the open timeout", async () => {
|
|
const stalled = await listenStalledUpgrade();
|
|
const client = createGatewayWsClient({ openTimeoutMs: 5, url: stalled.url });
|
|
try {
|
|
await expect(client.waitOpen()).rejects.toThrow("ws open timeout");
|
|
await waitFor(() => client.ws.readyState === WebSocket.CLOSED);
|
|
} finally {
|
|
client.close();
|
|
await stalled.close();
|
|
}
|
|
});
|
|
|
|
it("uses caller-specific websocket open timeout messages", async () => {
|
|
const stalled = await listenStalledUpgrade();
|
|
const client = createGatewayWsClient({
|
|
openTimeoutMessage: "gateway ws open timeout",
|
|
openTimeoutMs: 5,
|
|
url: stalled.url,
|
|
});
|
|
try {
|
|
await expect(client.waitOpen()).rejects.toThrow("gateway ws open timeout");
|
|
} finally {
|
|
client.close();
|
|
await stalled.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
async function waitFor(condition: () => boolean, timeoutMs = 1_000) {
|
|
const startedAt = Date.now();
|
|
while (!condition()) {
|
|
if (Date.now() - startedAt > timeoutMs) {
|
|
throw new Error("timed out waiting for condition");
|
|
}
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, 10);
|
|
});
|
|
}
|
|
}
|