mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
* fix(test): load compiled-subprocess declarations at collection The first load of a compiled-subprocess declaration in a Vitest invocation prepares the whole compiled worker generation (tens of seconds warm, minutes cold). 387 test files first reached a declaration through an await import() inside a test body or hook, so their first test or hook absorbed that preparation and could time out. Move those loads to collection at their owner: static subject imports, static imports in the shared helpers that owned the lazy load, and a named side-effect preload (src/test-utils/prepare-compiled-subprocesses.ts) for suites that re-import their subject per test or whose first load happens in production code they call. plugin-test-runtime keeps the host-capability fixture lazy so its other consumers do not start loading that graph. Document the rule in docs/help/testing/writing-tests.md. 335 of the 387 files now load their first declaration before collection ends; 52 remain (48 extension tests needing an SDK preload, one package test, three files over the line cap). * fix(test): retain npm install fixture reuse Preserve the existing failedSpawn reuse after merging main so the collection-time preload stays within the line-cap ratchet. Assertions and import ordering are unchanged. * fix(test): keep memory-core facade cold-import assertion meaningful The static subject import ran before beforeEach reset the loader mock, so the cold-import assertion passed vacuously. Restore the in-test subject imports after mock setup and preload the compiled-subprocess declaration during collection instead.
341 lines
14 KiB
TypeScript
341 lines
14 KiB
TypeScript
import "../src/test-utils/prepare-compiled-subprocesses.js";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { withTempHome } from "openclaw/plugin-sdk/test-env";
|
|
import { describe, expect, it } from "vitest";
|
|
import { runBuiltCli } from "./cli-json-stdout.test-support.js";
|
|
|
|
describe("cli json stdout contract", () => {
|
|
it.each([
|
|
{
|
|
name: "invalid call params in JSON mode",
|
|
args: ["gateway", "call", "system-presence", "--params", "not-json", "--json"],
|
|
message: "--params must be valid JSON.",
|
|
},
|
|
{
|
|
name: "invalid call params in human mode",
|
|
args: ["gateway", "call", "system-presence", "--params", "not-json"],
|
|
message: "--params must be valid JSON.",
|
|
human: true,
|
|
},
|
|
{
|
|
name: "invalid call params through forced Commander",
|
|
args: ["gateway", "call", "system-presence", "--params", "not-json", "--json"],
|
|
message: "--params must be valid JSON.",
|
|
commander: true,
|
|
},
|
|
{
|
|
name: "invalid call params with dual TTYs",
|
|
args: ["gateway", "call", "system-presence", "--params", "not-json", "--json"],
|
|
message: "--params must be valid JSON.",
|
|
tty: true,
|
|
},
|
|
{
|
|
name: "contradictory usage options in JSON mode",
|
|
args: ["gateway", "usage-cost", "--agent", "alpha", "--all-agents", "--json"],
|
|
message: "Use --agent or --all-agents, not both",
|
|
},
|
|
{
|
|
name: "contradictory usage options in human mode",
|
|
args: ["gateway", "usage-cost", "--agent", "alpha", "--all-agents"],
|
|
message: "Use --agent or --all-agents, not both",
|
|
human: true,
|
|
},
|
|
{
|
|
name: "contradictory usage options through forced Commander",
|
|
args: ["gateway", "usage-cost", "--agent", "alpha", "--all-agents", "--json"],
|
|
message: "Use --agent or --all-agents, not both",
|
|
commander: true,
|
|
},
|
|
{
|
|
name: "contradictory usage options with dual TTYs",
|
|
args: ["gateway", "usage-cost", "--agent", "alpha", "--all-agents", "--json"],
|
|
message: "Use --agent or --all-agents, not both",
|
|
tty: true,
|
|
},
|
|
{
|
|
name: "routed Gateway health config failure in JSON mode",
|
|
args: ["gateway", "health", "--port", "29793", "--json"],
|
|
message: "AUTOQA_ROUTE_CONFIG_READ_FAILURE",
|
|
configReadFailure: true,
|
|
},
|
|
{
|
|
name: "Gateway health config failure in human mode",
|
|
args: ["gateway", "health", "--port", "29793"],
|
|
message: "AUTOQA_ROUTE_CONFIG_READ_FAILURE",
|
|
configReadFailure: true,
|
|
human: true,
|
|
},
|
|
{
|
|
name: "Gateway health config failure through forced Commander",
|
|
args: ["gateway", "health", "--port", "29793", "--json"],
|
|
message: "AUTOQA_ROUTE_CONFIG_READ_FAILURE",
|
|
configReadFailure: true,
|
|
commander: true,
|
|
},
|
|
{
|
|
name: "routed Gateway health config failure with dual TTYs",
|
|
args: ["gateway", "health", "--port", "29793", "--json"],
|
|
message: "AUTOQA_ROUTE_CONFIG_READ_FAILURE",
|
|
configReadFailure: true,
|
|
tty: true,
|
|
},
|
|
{
|
|
name: "specialized explicit Gateway authentication failure",
|
|
args: ["gateway", "call", "system-presence", "--url", "ws://127.0.0.1:29793", "--json"],
|
|
specializedAuth: true,
|
|
},
|
|
])("renders Gateway query failures through the Gateway owner for $name", async (testCase) => {
|
|
await withTempHome(
|
|
async (tempHome) => {
|
|
const configPath = path.join(tempHome, "missing-openclaw.json");
|
|
const stateDir = path.join(tempHome, "isolated-state");
|
|
const gatewayError = "AUTOQA_INJECTED_GATEWAY_FAILURE";
|
|
const preload = Buffer.from(
|
|
[
|
|
'import net from "node:net";',
|
|
`net.Socket.prototype.connect = function () { throw new Error(${JSON.stringify(gatewayError)}); };`,
|
|
...("configReadFailure" in testCase
|
|
? [
|
|
'import fs from "node:fs";',
|
|
"Error.stackTraceLimit = 50;",
|
|
"const originalReadFileSync = fs.readFileSync;",
|
|
"fs.readFileSync = function (target, ...args) {",
|
|
' if (String(target) === process.env.OPENCLAW_CONFIG_PATH && new Error().stack?.includes("readNonObservingHealthConfig")) {',
|
|
` throw Object.assign(new Error(${JSON.stringify(testCase.message)}), { code: "EIO" });`,
|
|
" }",
|
|
" return originalReadFileSync.call(this, target, ...args);",
|
|
"};",
|
|
]
|
|
: []),
|
|
...("tty" in testCase
|
|
? [
|
|
'Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });',
|
|
'Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });',
|
|
]
|
|
: []),
|
|
].join("\n"),
|
|
).toString("base64");
|
|
const result = runBuiltCli(
|
|
tempHome,
|
|
testCase.args,
|
|
{
|
|
OPENCLAW_CONFIG_PATH: configPath,
|
|
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
|
|
OPENCLAW_STATE_DIR: stateDir,
|
|
...("commander" in testCase ? { OPENCLAW_DISABLE_ROUTE_FIRST: "1" } : {}),
|
|
...("tty" in testCase ? { FORCE_COLOR: "1", NO_COLOR: undefined } : {}),
|
|
},
|
|
{ execArgv: [`--import=data:text/javascript;base64,${preload}`] },
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stdout, result.stderr).not.toContain("\u001B");
|
|
expect(result.stdout, result.stderr).not.toContain("\u0007");
|
|
expect(result.stdout).not.toContain(gatewayError);
|
|
expect(result.stderr).not.toContain(gatewayError);
|
|
if ("human" in testCase) {
|
|
expect(result.stdout).toBe("");
|
|
expect(result.stderr).toContain(testCase.message);
|
|
} else if ("specializedAuth" in testCase) {
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
ok: false,
|
|
error: {
|
|
type: "gateway_credentials_required",
|
|
message: [
|
|
"gateway url override requires explicit credentials",
|
|
"Fix: pass --token or --password with --url (or gatewayToken in tools).",
|
|
"For the default local or SSH-tunneled Gateway, remove --url to use the configured target.",
|
|
`Config: ${configPath}`,
|
|
].join("\n"),
|
|
},
|
|
});
|
|
expect(result.stderr).toBe("");
|
|
} else {
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
ok: false,
|
|
error: {
|
|
type: "cli_error",
|
|
message:
|
|
"configReadFailure" in testCase
|
|
? `Config could not be read at ${configPath}:\n- <root>: read failed: Error: ${testCase.message}`
|
|
: testCase.message,
|
|
},
|
|
});
|
|
if ("configReadFailure" in testCase && !("commander" in testCase)) {
|
|
expect(result.stderr).toContain(testCase.message);
|
|
} else {
|
|
expect(result.stderr).not.toContain(testCase.message);
|
|
}
|
|
}
|
|
if ("tty" in testCase) {
|
|
expect(result.stderr).toContain("\u001B[?25h");
|
|
}
|
|
await expect(fs.stat(stateDir)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(fs.stat(configPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
},
|
|
{ prefix: "openclaw-gateway-query-json-failure-e2e-" },
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "routed status with JSON before its timeout",
|
|
args: ["status", "--json", "--timeout", "nope"],
|
|
},
|
|
{
|
|
name: "routed health with JSON after its timeout",
|
|
args: ["health", "--timeout", "0", "--json"],
|
|
},
|
|
{
|
|
name: "Commander status with JSON after its timeout",
|
|
args: ["status", "--timeout", "nope", "--json"],
|
|
commander: true,
|
|
},
|
|
{
|
|
name: "Commander health with JSON before its timeout",
|
|
args: ["health", "--json", "--timeout", "0"],
|
|
commander: true,
|
|
},
|
|
{
|
|
name: "routed status through dual-TTY finalization",
|
|
args: ["status", "--json", "--timeout", "nope"],
|
|
tty: true,
|
|
},
|
|
])("renders invalid status/health timeouts as canonical JSON for $name", async (testCase) => {
|
|
await withTempHome(
|
|
async (tempHome) => {
|
|
const preload = `data:text/javascript,${encodeURIComponent(
|
|
'Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true }); Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });',
|
|
)}`;
|
|
const result = runBuiltCli(
|
|
tempHome,
|
|
testCase.args,
|
|
{
|
|
OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"),
|
|
OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"),
|
|
OPENCLAW_GATEWAY_PORT: "29791",
|
|
...("commander" in testCase ? { OPENCLAW_DISABLE_ROUTE_FIRST: "1" } : {}),
|
|
...("tty" in testCase ? { FORCE_COLOR: "1" } : {}),
|
|
},
|
|
{ execArgv: "tty" in testCase ? [`--import=${preload}`] : [] },
|
|
);
|
|
const message = "--timeout must be a positive integer (milliseconds)";
|
|
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stdout, result.stderr).not.toContain("\u001B");
|
|
expect(result.stdout, result.stderr).not.toContain("\u0007");
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
ok: false,
|
|
error: { type: "cli_error", message },
|
|
});
|
|
expect(result.stderr).toContain(message);
|
|
if ("tty" in testCase) {
|
|
expect(result.stderr).toContain("\u001B[?25h");
|
|
}
|
|
},
|
|
{ prefix: "openclaw-status-health-json-timeout-e2e-" },
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "status with an invalid duration in human mode",
|
|
args: ["nodes", "status", "--last-connected", "not-a-duration"],
|
|
message: "Invalid --last-connected: Invalid duration",
|
|
human: true,
|
|
},
|
|
{
|
|
name: "status with JSON before its invalid duration",
|
|
args: ["nodes", "status", "--json", "--last-connected", "not-a-duration"],
|
|
message: "Invalid --last-connected: Invalid duration",
|
|
},
|
|
{
|
|
name: "status with JSON after its invalid duration",
|
|
args: ["nodes", "status", "--last-connected", "not-a-duration", "--json"],
|
|
message: "Invalid --last-connected: Invalid duration",
|
|
},
|
|
{
|
|
name: "list with an invalid duration in human mode",
|
|
args: ["nodes", "list", "--last-connected", "not-a-duration"],
|
|
message: "Invalid --last-connected: Invalid duration",
|
|
human: true,
|
|
},
|
|
{
|
|
name: "list with JSON before its invalid duration",
|
|
args: ["nodes", "list", "--json", "--last-connected", "not-a-duration"],
|
|
message: "Invalid --last-connected: Invalid duration",
|
|
},
|
|
{
|
|
name: "list with JSON after its invalid duration",
|
|
args: ["nodes", "list", "--last-connected", "not-a-duration", "--json"],
|
|
message: "Invalid --last-connected: Invalid duration",
|
|
},
|
|
{
|
|
name: "invoke with an explicitly JSON blank node",
|
|
args: ["nodes", "invoke", "--node", " ", "--command", "canvas.eval", "--json"],
|
|
message: "--node and --command required",
|
|
},
|
|
{
|
|
name: "invoke with an implicitly JSON blank node",
|
|
args: ["nodes", "invoke", "--node", " ", "--command", "canvas.eval"],
|
|
message: "--node and --command required",
|
|
},
|
|
{
|
|
name: "invoke with an explicitly JSON blank command",
|
|
args: ["nodes", "invoke", "--node", "mac-1", "--command", " ", "--json"],
|
|
message: "--node and --command required",
|
|
},
|
|
{
|
|
name: "invoke with an implicitly JSON blank command",
|
|
args: ["nodes", "invoke", "--node", "mac-1", "--command", " "],
|
|
message: "--node and --command required",
|
|
},
|
|
{
|
|
name: "rename with a blank name",
|
|
args: ["nodes", "rename", "--node", "mac-1", "--name", " ", "--json"],
|
|
message: "--name must not be empty",
|
|
},
|
|
])("renders nodes $name through the shared validation owner", async (testCase) => {
|
|
await withTempHome(
|
|
async (tempHome) => {
|
|
const denyNetwork = Buffer.from(
|
|
`import net from "node:net";
|
|
net.Socket.prototype.connect = function () { throw new Error("AUTOQA_NETWORK_FORBIDDEN"); };
|
|
globalThis.fetch = async () => { throw new Error("AUTOQA_NETWORK_FORBIDDEN"); };`,
|
|
).toString("base64");
|
|
const result = runBuiltCli(
|
|
tempHome,
|
|
testCase.args,
|
|
{
|
|
// Startup probes SQLite in a worker; filesystem writes remain denied in the CLI.
|
|
NODE_OPTIONS: "--permission --allow-fs-read=* --allow-worker",
|
|
NODE_DISABLE_COMPILE_CACHE: "1",
|
|
OPENCLAW_NO_RESPAWN: "1",
|
|
OPENCLAW_LOG_LEVEL: "silent",
|
|
OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"),
|
|
OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"),
|
|
},
|
|
{ execArgv: [`--import=data:text/javascript;base64,${denyNetwork}`] },
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(1);
|
|
if ("human" in testCase && testCase.human) {
|
|
expect(result.stdout).toBe("");
|
|
expect(result.stderr).toContain(`nodes ${testCase.args[1]} failed:`);
|
|
} else {
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
ok: false,
|
|
error: {
|
|
type: "cli_error",
|
|
message: expect.stringContaining(testCase.message),
|
|
},
|
|
});
|
|
}
|
|
expect(result.stderr).toContain(testCase.message);
|
|
expect(result.stderr).not.toContain("AUTOQA_NETWORK_FORBIDDEN");
|
|
},
|
|
{ prefix: "openclaw-nodes-json-failure-e2e-" },
|
|
);
|
|
});
|
|
});
|