From 5c83f805b7b3fd2ed1d28c0b1b2bc36b71e2e11c Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 25 May 2026 03:20:16 +0000 Subject: [PATCH] test gateway nonblocking shell dispatch --- .../gateway-outbound-nonblocking-probe.mjs | 140 ++++++++++++++++++ web/hwlab-cloud-web/scripts/check.mjs | 2 + 2 files changed, 142 insertions(+) create mode 100644 scripts/gateway-outbound-nonblocking-probe.mjs diff --git a/scripts/gateway-outbound-nonblocking-probe.mjs b/scripts/gateway-outbound-nonblocking-probe.mjs new file mode 100644 index 00000000..b2e6326e --- /dev/null +++ b/scripts/gateway-outbound-nonblocking-probe.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { setTimeout as delay } from "node:timers/promises"; + +const args = parseArgs(process.argv.slice(2)); +const apiBaseUrl = stripTrailingSlash(args.apiBaseUrl || process.env.HWLAB_GATEWAY_PROBE_API_BASE_URL || "http://127.0.0.1:6667"); +const gatewaySessionId = args.gatewaySessionId || process.env.HWLAB_GATEWAY_PROBE_SESSION_ID || "gws_gtw_local_demo"; +const cwd = args.cwd || process.cwd(); +const slowCommand = args.slowCommand || (process.platform === "win32" ? "ping -n 4 127.0.0.1 >NUL" : "sleep 3"); +const quickCommand = args.quickCommand || "echo hwlab-nonblocking-quick"; +const timeoutMs = positiveInteger(args.timeoutMs, 10000); +const quickMaxMs = positiveInteger(args.quickMaxMs, 2500); + +await waitForGatewaySession(); + +const slow = postRpc({ + id: "req_gateway_nonblocking_slow", + traceId: "trc_gateway_nonblocking_slow", + command: slowCommand +}); +await delay(250); + +const quickStartedAt = Date.now(); +const quick = await postRpc({ + id: "req_gateway_nonblocking_quick", + traceId: "trc_gateway_nonblocking_quick", + command: quickCommand +}); +const quickDurationMs = Date.now() - quickStartedAt; +const slowResult = await slow; + +assert.equal(quick.result?.dispatch?.shellExecuted, true, JSON.stringify(quick, null, 2)); +assert.equal(quick.result?.dispatch?.exitCode, 0, JSON.stringify(quick, null, 2)); +assert.equal(slowResult.result?.dispatch?.shellExecuted, true, JSON.stringify(slowResult, null, 2)); +assert.equal(slowResult.result?.dispatch?.exitCode, 0, JSON.stringify(slowResult, null, 2)); +assert.ok(quickDurationMs < quickMaxMs, `quick request was blocked by slow request for ${quickDurationMs}ms`); + +console.log(JSON.stringify({ + status: "pass", + apiBaseUrl, + gatewaySessionId, + quickDurationMs, + quickOperationId: quick.result?.operationId ?? null, + slowOperationId: slowResult.result?.operationId ?? null, + quickStdout: String(quick.result?.dispatch?.stdout ?? "").trim() +}, null, 2)); + +async function waitForGatewaySession() { + const deadline = Date.now() + 8000; + let last; + while (Date.now() < deadline) { + try { + const body = await getJson("/v1/gateway/sessions"); + last = body; + const session = body.sessions?.find((item) => item.gatewaySessionId === gatewaySessionId); + if (session?.status === "online") return session; + } catch (error) { + last = error; + } + await delay(100); + } + throw new Error(`gateway session ${gatewaySessionId} did not become online: ${stringifyLast(last)}`); +} + +async function getJson(path) { + const response = await fetch(`${apiBaseUrl}${path}`, { + headers: { accept: "application/json" } + }); + const text = await response.text(); + if (!response.ok) throw new Error(`GET ${path} HTTP ${response.status}: ${text}`); + return text ? JSON.parse(text) : null; +} + +async function postRpc({ id, traceId, command }) { + const response = await fetch(`${apiBaseUrl}/json-rpc`, { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json" + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id, + method: "hardware.invoke.shell", + params: { + projectId: "prj_mvp_topology", + gatewaySessionId, + resourceId: "res_windows_host", + capabilityId: "cap_windows_cmd_exec", + input: { + command, + cwd, + timeoutMs + } + }, + meta: { + traceId, + actorId: "usr_gateway_nonblocking_probe", + serviceId: "hwlab-cloud-api", + environment: "dev" + } + }) + }); + const text = await response.text(); + if (!response.ok) throw new Error(`RPC ${id} HTTP ${response.status}: ${text}`); + return text ? JSON.parse(text) : null; +} + +function parseArgs(argv) { + const result = {}; + for (let index = 0; index < argv.length; index += 1) { + const item = argv[index]; + if (item.startsWith("--") && item.includes("=")) { + const [key, ...rest] = item.slice(2).split("="); + result[toCamel(key)] = rest.join("="); + } else if (item.startsWith("--")) { + result[toCamel(item.slice(2))] = argv[index + 1] ?? ""; + index += 1; + } + } + return result; +} + +function toCamel(value) { + return String(value).replace(/-([a-z])/gu, (_, char) => char.toUpperCase()); +} + +function positiveInteger(value, fallback) { + const parsed = Number.parseInt(value ?? "", 10); + return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; +} + +function stripTrailingSlash(value) { + return String(value).replace(/\/+$/u, ""); +} + +function stringifyLast(value) { + if (value instanceof Error) return value.message; + return JSON.stringify(value); +} diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 4e167790..47b03087 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -71,6 +71,7 @@ const cloudJsonRpc = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/json const gatewayDemoRegistry = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/gateway-demo-registry.mjs"), "utf8"); const gatewayMain = fs.readFileSync(path.resolve(repoRoot, "cmd/hwlab-gateway/main.mjs"), "utf8"); const gatewayDemoSmoke = fs.readFileSync(path.resolve(repoRoot, "scripts/gateway-outbound-demo-smoke.mjs"), "utf8"); +const gatewayNonblockingProbe = fs.readFileSync(path.resolve(repoRoot, "scripts/gateway-outbound-nonblocking-probe.mjs"), "utf8"); const codexStdioSession = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/codex-stdio-session.mjs"), "utf8"); const gatewayShellTool = fs.readFileSync(path.resolve(repoRoot, "tools/hwlab-gateway-shell.mjs"), "utf8"); const frontendSource = `${html}\n${auth}\n${app}\n${artifactPublisher}`; @@ -1162,6 +1163,7 @@ assert.match(gatewayMain, /function startCloudRequest/); assert.match(gatewayMain, /Promise\.resolve\(\)\s*\n\s*\.then\(\(\) => handleCloudRequest/); assert.doesNotMatch(gatewayMain, /await handleCloudRequest\(response\.body\.request\)/); assert.match(gatewayDemoSmoke, /quick request was blocked by slow request/); +assert.match(gatewayNonblockingProbe, /quick request was blocked by slow request/); assert.match(cloudApiServer, /HWLAB_GATEWAY_DISPATCH_TIMEOUT_MS,\s*120000/); assert.match(codexStdioSession, /detached:\s*childDetached/); assert.match(codexStdioSession, /function terminateChildProcess/);