test gateway nonblocking shell dispatch
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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/);
|
||||
|
||||
Reference in New Issue
Block a user