mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 05:47:27 +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.
242 lines
7.1 KiB
TypeScript
242 lines
7.1 KiB
TypeScript
// Gateway E2E harness starts test gateway processes and HTTP probes.
|
|
import { request as httpRequest } from "node:http";
|
|
import path from "node:path";
|
|
import { GatewayClient } from "../../src/gateway/client.js";
|
|
import { connectGatewayClient } from "../../src/gateway/test-helpers.e2e.js";
|
|
import { loadOrCreateDeviceIdentity } from "../../src/infra/device-identity.js";
|
|
import { extractFirstTextBlock } from "../../src/shared/chat-message-content.js";
|
|
import { sleep } from "../../src/utils.js";
|
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../src/utils/message-channel.js";
|
|
import { createOpenClawTestInstance, type OpenClawTestInstance } from "./openclaw-test-instance.js";
|
|
|
|
export { extractFirstTextBlock };
|
|
|
|
export type GatewayInstance = OpenClawTestInstance;
|
|
|
|
const GATEWAY_CONNECT_STATUS_TIMEOUT_MS = 10_000;
|
|
const GATEWAY_NODE_STATUS_TIMEOUT_MS = 15_000;
|
|
const GATEWAY_NODE_STATUS_POLL_MS = 20;
|
|
const POST_JSON_TIMEOUT_MS = 15_000;
|
|
const POST_JSON_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
|
|
export type PostJsonOptions = {
|
|
maxResponseBytes?: number;
|
|
timeoutMs?: number;
|
|
};
|
|
|
|
export async function spawnGatewayInstance(name: string): Promise<GatewayInstance> {
|
|
const inst = await createOpenClawTestInstance({ name });
|
|
try {
|
|
await inst.startGateway();
|
|
return inst;
|
|
} catch (err) {
|
|
await inst.cleanup();
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export async function stopGatewayInstance(inst: GatewayInstance) {
|
|
await inst.cleanup();
|
|
}
|
|
|
|
export async function postJson(
|
|
url: string,
|
|
body: unknown,
|
|
headers?: Record<string, string>,
|
|
options: PostJsonOptions = {},
|
|
): Promise<{ status: number; json: unknown }> {
|
|
const payload = JSON.stringify(body);
|
|
const parsed = new URL(url);
|
|
const timeoutMs = options.timeoutMs ?? POST_JSON_TIMEOUT_MS;
|
|
const maxResponseBytes = options.maxResponseBytes ?? POST_JSON_MAX_RESPONSE_BYTES;
|
|
return await new Promise<{ status: number; json: unknown }>((resolve, reject) => {
|
|
let settled = false;
|
|
let responseBytes = 0;
|
|
let timeout: NodeJS.Timeout | undefined;
|
|
|
|
const finish = (result: { status: number; json: unknown } | { error: Error }) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
timeout = undefined;
|
|
}
|
|
if ("error" in result) {
|
|
reject(result.error);
|
|
return;
|
|
}
|
|
resolve(result);
|
|
};
|
|
|
|
const req = httpRequest(
|
|
{
|
|
method: "POST",
|
|
hostname: parsed.hostname,
|
|
port: Number(parsed.port),
|
|
path: `${parsed.pathname}${parsed.search}`,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": Buffer.byteLength(payload),
|
|
...headers,
|
|
},
|
|
},
|
|
(res) => {
|
|
let data = "";
|
|
res.setEncoding("utf8");
|
|
res.on("data", (chunk) => {
|
|
responseBytes += Buffer.byteLength(chunk, "utf8");
|
|
if (responseBytes > maxResponseBytes) {
|
|
const error = new Error(`POST ${url} response exceeded ${maxResponseBytes} bytes`);
|
|
req.destroy(error);
|
|
res.destroy(error);
|
|
finish({ error });
|
|
return;
|
|
}
|
|
data += chunk;
|
|
});
|
|
res.on("end", () => {
|
|
let json: unknown = null;
|
|
if (data.trim()) {
|
|
try {
|
|
json = JSON.parse(data);
|
|
} catch {
|
|
json = data;
|
|
}
|
|
}
|
|
finish({ status: res.statusCode ?? 0, json });
|
|
});
|
|
res.on("error", (error) => finish({ error }));
|
|
},
|
|
);
|
|
timeout = setTimeout(() => {
|
|
req.destroy(new Error(`POST ${url} timed out after ${timeoutMs}ms`));
|
|
}, timeoutMs);
|
|
timeout.unref?.();
|
|
req.on("error", (error) => finish({ error }));
|
|
req.write(payload);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
export async function connectNode(
|
|
inst: GatewayInstance,
|
|
label: string,
|
|
): Promise<{ client: GatewayClient; nodeId: string }> {
|
|
const identityPath = path.join(inst.homeDir, `${label}-device.json`);
|
|
const deviceIdentity = loadOrCreateDeviceIdentity(identityPath);
|
|
const nodeId = deviceIdentity.deviceId;
|
|
const client = await connectGatewayClient({
|
|
url: `ws://127.0.0.1:${inst.port}`,
|
|
token: inst.gatewayToken,
|
|
clientName: GATEWAY_CLIENT_NAMES.NODE_HOST,
|
|
clientDisplayName: label,
|
|
clientVersion: "1.0.0",
|
|
platform: "ios",
|
|
mode: GATEWAY_CLIENT_MODES.NODE,
|
|
role: "node",
|
|
scopes: [],
|
|
caps: ["system"],
|
|
commands: ["system.run"],
|
|
deviceIdentity,
|
|
timeoutMessage: `timeout waiting for ${label} to connect`,
|
|
});
|
|
return { client, nodeId };
|
|
}
|
|
|
|
async function connectStatusClient(
|
|
inst: GatewayInstance,
|
|
timeoutMs = GATEWAY_CONNECT_STATUS_TIMEOUT_MS,
|
|
): Promise<GatewayClient> {
|
|
let settled = false;
|
|
let timer: NodeJS.Timeout | null = null;
|
|
|
|
return await new Promise<GatewayClient>((resolve, reject) => {
|
|
const finish = (err?: Error) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
}
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
resolve(client);
|
|
};
|
|
|
|
const client = new GatewayClient({
|
|
url: `ws://127.0.0.1:${inst.port}`,
|
|
connectChallengeTimeoutMs: 0,
|
|
token: inst.gatewayToken,
|
|
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
|
clientDisplayName: `status-${inst.name}`,
|
|
clientVersion: "1.0.0",
|
|
platform: "test",
|
|
mode: GATEWAY_CLIENT_MODES.CLI,
|
|
onHelloOk: () => {
|
|
finish();
|
|
},
|
|
onConnectError: (err) => finish(err),
|
|
onClose: (code, reason) => {
|
|
finish(new Error(`gateway closed (${code}): ${reason}`));
|
|
},
|
|
});
|
|
|
|
timer = setTimeout(() => {
|
|
finish(new Error(`timeout waiting for status client hello for ${inst.name}`));
|
|
}, timeoutMs);
|
|
|
|
client.start();
|
|
});
|
|
}
|
|
|
|
export async function waitForNodeStatus(
|
|
inst: GatewayInstance,
|
|
nodeId: string,
|
|
timeoutMs = GATEWAY_NODE_STATUS_TIMEOUT_MS,
|
|
) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
let lastError: unknown;
|
|
while (Date.now() < deadline) {
|
|
let client: GatewayClient | undefined;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
client = await connectStatusClient(
|
|
inst,
|
|
Math.min(2_000, GATEWAY_CONNECT_STATUS_TIMEOUT_MS, Math.max(1, deadline - Date.now())),
|
|
);
|
|
break;
|
|
} catch (error) {
|
|
lastError = error;
|
|
await sleep(GATEWAY_NODE_STATUS_POLL_MS);
|
|
}
|
|
}
|
|
if (!client) {
|
|
break;
|
|
}
|
|
try {
|
|
while (Date.now() < deadline) {
|
|
const list = (await client.request("node.list", {})) as {
|
|
nodes?: Array<{ nodeId: string; connected?: boolean; paired?: boolean }>;
|
|
};
|
|
const match = list.nodes?.find((n) => n.nodeId === nodeId);
|
|
if (match?.connected && match?.paired) {
|
|
return;
|
|
}
|
|
await sleep(GATEWAY_NODE_STATUS_POLL_MS);
|
|
}
|
|
} catch (error) {
|
|
lastError = error;
|
|
await sleep(GATEWAY_NODE_STATUS_POLL_MS);
|
|
} finally {
|
|
client.stop();
|
|
}
|
|
}
|
|
const suffix = lastError instanceof Error ? `: ${lastError.message}` : "";
|
|
throw new Error(`timeout waiting for node status for ${nodeId}${suffix}`);
|
|
}
|