openclaw/test/cli-json-stdout.config.e2e.test.ts
Peter Steinberger 931c7b40b2
fix(test): first test or hook times out while compiled worker subprocesses prepare (#162792)
* 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.
2026-10-01 11:42:59 -07:00

431 lines
16 KiB
TypeScript

import "../src/test-utils/prepare-compiled-subprocesses.js";
import { randomBytes } from "node:crypto";
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: "routed agents list", overrides: {} },
{ name: "Commander agents list", overrides: { OPENCLAW_DISABLE_ROUTE_FIRST: "1" } },
])("drains large invalid-config JSON for $name", async ({ overrides }) => {
await withTempHome(
async (tempHome) => {
const configPath = path.join(tempHome, "openclaw.json");
const agentIds = Array.from({ length: 16_384 }, (_, index) => `agent_${index}`);
const config = JSON.stringify({
agents: {
ownership: "explicit",
entries: Object.fromEntries(agentIds.map((id) => [id, { workspace: 42 }])),
},
});
await fs.writeFile(configPath, config);
const result = runBuiltCli(
tempHome,
["agents", "list", "--json"],
{
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: path.join(tempHome, "state"),
...overrides,
},
{ inheritEnvironment: false },
);
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(1);
expect(JSON.parse(result.stdout)).toEqual({
ok: false,
error: {
type: "cli_error",
message: expect.stringContaining("OpenClaw config is invalid:"),
},
issues: agentIds.map((id) => ({
path: `agents.entries.${id}.workspace`,
message: "Invalid input: expected string, received number",
})),
});
expect(Buffer.byteLength(result.stdout)).toBeGreaterThan(1024 * 1024);
expect(result.stderr).toBe("");
expect(await fs.readFile(configPath, "utf8")).toBe(config);
},
{ prefix: "openclaw-agents-large-invalid-config-json-" },
);
});
it.each([
{ name: "routed agents list", overrides: {} },
{ name: "Commander agents list", overrides: { OPENCLAW_DISABLE_ROUTE_FIRST: "1" } },
])("reports shared invalid config as JSON for $name", async ({ overrides }) => {
await withTempHome(
async (tempHome) => {
const configPath = path.join(tempHome, "openclaw.json");
const config = `${JSON.stringify({ gateway: { port: "invalid-port" } })}\n`;
await fs.writeFile(configPath, config);
const result = runBuiltCli(
tempHome,
["agents", "list", "--json"],
{
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: path.join(tempHome, "state"),
...overrides,
},
{ inheritEnvironment: false },
);
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(1);
expect(JSON.parse(result.stdout)).toEqual({
ok: false,
error: {
type: "cli_error",
message: expect.stringContaining("OpenClaw config is invalid:"),
},
issues: [{ path: "gateway.port", message: expect.stringContaining("expected number") }],
});
expect(result.stderr).toBe("");
expect(await fs.readFile(configPath, "utf8")).toBe(config);
},
{ prefix: "openclaw-agents-invalid-config-json-" },
);
});
it.each([
["memory", "status", "--json"],
["nodes", "canvas", "snapshot", "--json"],
])("reports invalid config before discovering plugin command %s", async (...args) => {
await withTempHome(
async (tempHome) => {
const configPath = path.join(tempHome, "openclaw.json");
const stateDir = path.join(tempHome, "state");
await fs.writeFile(configPath, JSON.stringify({ gateway: { port: "invalid-port" } }));
const result = runBuiltCli(
tempHome,
args,
{
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: stateDir,
},
{ inheritEnvironment: false },
);
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe(1);
expect(JSON.parse(result.stdout)).toMatchObject({
ok: false,
error: { type: "cli_error", message: expect.stringContaining("Invalid config at") },
});
expect(result.stdout).toContain("gateway.port");
expect(result.stderr).toContain("openclaw doctor");
await expect(
fs.access(path.join(stateDir, "state", "openclaw.sqlite")),
).rejects.toMatchObject({ code: "ENOENT" });
},
{ prefix: "openclaw-plugin-invalid-config-" },
);
});
it.each([
{
name: "node identity",
args: ["node", "identity", "--json"],
overrides: {},
error: "no node device identity found",
},
{
name: "routed config get",
args: ["config", "get", "gateway.port", "--json"],
overrides: {},
},
{
name: "Commander config get",
args: ["config", "get", "gateway.port", "--json"],
overrides: { OPENCLAW_DISABLE_ROUTE_FIRST: "1" },
},
{
name: "Nix config get",
args: ["config", "get", "gateway.port", "--json"],
overrides: { OPENCLAW_NIX_MODE: "1" },
},
{ name: "config schema", args: ["config", "schema"], overrides: {} },
{
name: "Nix config schema",
args: ["config", "schema"],
overrides: { OPENCLAW_NIX_MODE: "1" },
},
{ name: "config validate", args: ["config", "validate", "--json"], overrides: {} },
{
name: "Nix config validate",
args: ["config", "validate", "--json"],
overrides: { OPENCLAW_NIX_MODE: "1" },
},
])("does not initialize shared SQLite for $name", async (testCase) => {
await withTempHome(
async (tempHome) => {
const stateDir = path.join(tempHome, "read-only-state");
const configPath = path.join(tempHome, "read-only-openclaw.json");
const config = `${JSON.stringify({
gateway: {
mode: "local",
port: 18789,
auth: { mode: "token", token: randomBytes(32).toString("hex") },
},
plugins: { enabled: false },
browser: { enabled: false },
discovery: { mdns: { mode: "off" } },
logging: { file: path.join(stateDir, "openclaw.log") },
})}\n`;
await fs.writeFile(configPath, config, "utf8");
const configBefore = await fs.stat(configPath);
const tmpDir = path.join(tempHome, "tmp");
await fs.mkdir(tmpDir);
const result = runBuiltCli(
tempHome,
testCase.args,
{
OPENCLAW_HOME: tempHome,
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: stateDir,
OPENCLAW_TEST_FAST: undefined,
TMPDIR: tmpDir,
TMP: tmpDir,
TEMP: tmpDir,
PATH: path.dirname(process.execPath),
...testCase.overrides,
},
{ inheritEnvironment: false },
);
expect(result.error).toBeUndefined();
expect(result.status, result.stderr).toBe("error" in testCase ? 1 : 0);
if ("error" in testCase) {
expect(result.stderr).toContain(testCase.error);
expect(result.stdout).toBe("");
} else {
expect(() => JSON.parse(result.stdout)).not.toThrow();
}
await expect(
fs.access(path.join(stateDir, "state", "openclaw.sqlite")),
).rejects.toMatchObject({
code: "ENOENT",
});
expect((await fs.readFile(configPath, "utf8")) === config).toBe(true);
expect(await fs.stat(configPath)).toMatchObject({
ino: configBefore.ino,
mode: configBefore.mode,
mtimeMs: configBefore.mtimeMs,
ctimeMs: configBefore.ctimeMs,
});
},
{ prefix: "openclaw-read-only-config-e2e-" },
);
});
it.each([
{ name: "routed malformed config get", overrides: {} },
{
name: "Commander malformed config get",
overrides: { OPENCLAW_DISABLE_ROUTE_FIRST: "1" },
},
])("returns actionable JSON without creating state for $name", async (testCase) => {
await withTempHome(
async (tempHome) => {
const stateDir = path.join(tempHome, "read-only-state");
const configPath = path.join(tempHome, "read-only-openclaw.json");
await fs.writeFile(configPath, "{}\n", "utf8");
const result = runBuiltCli(
tempHome,
["config", "get", "gateway.__proto__.token", "--json"],
{
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: stateDir,
...testCase.overrides,
},
);
expect(result.status, result.stderr).toBe(1);
expect(JSON.parse(result.stdout)).toMatchObject({
ok: false,
error: {
type: "cli_error",
message: expect.stringContaining("Invalid path segment: __proto__"),
},
});
expect(result.stderr).toBe("");
await expect(
fs.access(path.join(stateDir, "state", "openclaw.sqlite")),
).rejects.toMatchObject({
code: "ENOENT",
});
},
{ prefix: "openclaw-read-only-invalid-config-e2e-" },
);
});
it.each([
{ name: "routed invalid config get", overrides: {} },
{
name: "Commander invalid config get",
overrides: { OPENCLAW_DISABLE_ROUTE_FIRST: "1" },
},
])("reports invalid configuration as JSON without creating state for $name", async (testCase) => {
await withTempHome(
async (tempHome) => {
const stateDir = path.join(tempHome, "read-only-state");
const configPath = path.join(tempHome, "read-only-openclaw.json");
await fs.writeFile(
configPath,
`${JSON.stringify({ gateway: { bind: "not-a-supported-mode" } })}\n`,
"utf8",
);
const result = runBuiltCli(tempHome, ["config", "get", "gateway.port", "--json"], {
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_STATE_DIR: stateDir,
...testCase.overrides,
});
expect(result.status, result.stderr).toBe(1);
expect(JSON.parse(result.stdout)).toMatchObject({
ok: false,
error: {
type: "cli_error",
message: expect.stringContaining("OpenClaw config is invalid"),
},
issues: expect.arrayContaining([
expect.objectContaining({ path: "gateway.bind", message: expect.any(String) }),
]),
});
expect(result.stderr).toBe("");
await expect(
fs.access(path.join(stateDir, "state", "openclaw.sqlite")),
).rejects.toMatchObject({
code: "ENOENT",
});
},
{ prefix: "openclaw-read-only-invalid-snapshot-e2e-" },
);
});
it.each([
{ name: "default service", inheritedProfile: undefined, inheritedStateName: ".openclaw" },
{ name: "named service", inheritedProfile: "main", inheritedStateName: ".openclaw-main" },
])("resolves the requested profile from inherited $name state", async (inherited) => {
await withTempHome(
async (tempHome) => {
const inheritedStateDir = path.join(tempHome, inherited.inheritedStateName);
const result = runBuiltCli(tempHome, ["--profile", "work", "config", "file"], {
OPENCLAW_PROFILE: inherited.inheritedProfile,
OPENCLAW_STATE_DIR: inheritedStateDir,
OPENCLAW_CONFIG_PATH: path.join(inheritedStateDir, "openclaw.json"),
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe(path.join(tempHome, ".openclaw-work", "openclaw.json"));
await expect(fs.access(path.join(tempHome, ".openclaw-work"))).rejects.toMatchObject({
code: "ENOENT",
});
},
{ prefix: "openclaw-profile-isolation-e2e-" },
);
});
it("keeps default-profile exec approvals untouched for a scratch-state config query", async () => {
await withTempHome(
async (tempHome) => {
const defaultStateDir = path.join(tempHome, ".openclaw");
const scratchStateDir = path.join(tempHome, "scratch-state");
const approvalsPath = path.join(defaultStateDir, "exec-approvals.json");
const approvals = '{"version":1,"approvals":{"demo":true}}\n';
await fs.mkdir(defaultStateDir, { recursive: true });
await fs.mkdir(scratchStateDir, { recursive: true });
await fs.writeFile(approvalsPath, approvals, "utf8");
const result = runBuiltCli(tempHome, ["config", "file"], {
OPENCLAW_STATE_DIR: scratchStateDir,
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout.trim()).toBe(path.join(scratchStateDir, "openclaw.json"));
await expect(fs.readFile(approvalsPath, "utf8")).resolves.toBe(approvals);
await expect(fs.access(`${approvalsPath}.migrated`)).rejects.toMatchObject({
code: "ENOENT",
});
await expect(
fs.access(path.join(scratchStateDir, "exec-approvals.json")),
).rejects.toMatchObject({ code: "ENOENT" });
await expect(
fs.access(path.join(scratchStateDir, "state", "openclaw.sqlite")),
).rejects.toMatchObject({ code: "ENOENT" });
},
{ prefix: "openclaw-read-only-state-e2e-" },
);
});
it("keeps representative success payload bytes unchanged", async () => {
await withTempHome(
async (tempHome) => {
const configPath = path.join(tempHome, "openclaw.json");
await fs.writeFile(configPath, '{"gateway":{"port":28789}}\n', "utf8");
const env = { OPENCLAW_CONFIG_PATH: configPath };
const getResult = runBuiltCli(tempHome, ["config", "get", "gateway.port", "--json"], env);
const validateResult = runBuiltCli(tempHome, ["config", "validate", "--json"], env);
expect(getResult.status, getResult.stderr).toBe(0);
expect(getResult.stdout).toBe("28789\n");
expect(validateResult.status, validateResult.stderr).toBe(0);
expect(validateResult.stdout).toBe(
`${JSON.stringify({ valid: true, path: configPath, warnings: [] })}\n`,
);
},
{ prefix: "openclaw-json-success-bytes-e2e-" },
);
});
it("keeps `config schema` stdout parseable at debug log level", async () => {
await withTempHome(
async (tempHome) => {
const result = runBuiltCli(tempHome, ["config", "schema"], {
OPENCLAW_LOG_LEVEL: "debug",
});
expect(result.status).toBe(0);
const parsed = JSON.parse(result.stdout) as {
properties?: Record<string, unknown>;
};
expect(parsed.properties?.$schema).toEqual({ type: "string" });
expect(result.stdout).not.toContain("possibly sensitive key found");
expect(result.stderr).not.toContain("possibly sensitive key found");
},
{ prefix: "openclaw-config-schema-json-e2e-" },
);
});
it("keeps `config validate --json` stdout parseable at debug log level", async () => {
await withTempHome(
async (tempHome) => {
const configPath = path.join(tempHome, "openclaw.json");
await fs.writeFile(configPath, "{}", "utf8");
const result = runBuiltCli(tempHome, ["config", "validate", "--json"], {
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_LOG_LEVEL: "debug",
});
expect(result.status).toBe(0);
expect(JSON.parse(result.stdout)).toMatchObject({
valid: true,
path: configPath,
});
expect(result.stdout).not.toContain("possibly sensitive key found");
},
{ prefix: "openclaw-config-validate-json-e2e-" },
);
});
});