test gateway nonblocking shell dispatch

This commit is contained in:
Codex
2026-05-25 03:20:16 +00:00
parent a59bed9a2a
commit 5c83f805b7
2 changed files with 142 additions and 0 deletions
@@ -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);
}
+2
View File
@@ -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/);