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.
431 lines
16 KiB
TypeScript
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-" },
|
|
);
|
|
});
|
|
});
|