openclaw/test/cli-json-stdout.skills.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

206 lines
7.7 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 { assert, describe, expect, it } from "vitest";
import { runBuiltCli } from "./cli-json-stdout.test-support.js";
describe("cli json stdout contract", () => {
it.each([
{
name: "search with a leaf JSON flag",
args: ["skills", "search", "fixture", "--json"],
message: "ClawHub /api/v1/search failed (400): offline fixture",
},
{
name: "search with a parent JSON flag",
args: ["skills", "--json", "search", "fixture"],
message: "ClawHub /api/v1/search failed (400): offline fixture",
},
{
name: "list with a leaf JSON flag",
args: ["skills", "list", "--agent", "", "--json"],
message: "--agent must not be blank",
},
{
name: "list with a parent JSON flag",
args: ["skills", "--json", "list", "--agent", ""],
message: "--agent must not be blank",
},
{
name: "info with a leaf JSON flag",
args: ["skills", "info", "fixture", "--agent", "", "--json"],
message: "--agent must not be blank",
},
{
name: "info with a parent JSON flag",
args: ["skills", "--json", "info", "fixture", "--agent", ""],
message: "--agent must not be blank",
},
{
name: "check with a leaf JSON flag",
args: ["skills", "check", "--agent", "", "--json"],
message: "--agent must not be blank",
},
{
name: "check with a parent JSON flag",
args: ["skills", "--json", "check", "--agent", ""],
message: "--agent must not be blank",
},
{
name: "the default report after its agent flag",
args: ["skills", "--agent", "", "--json"],
message: "--agent must not be blank",
},
{
name: "the default report before its agent flag",
args: ["skills", "--json", "--agent", ""],
message: "--agent must not be blank",
},
{
name: "list with a configured remote Gateway missing its URL",
args: ["skills", "list", "--json"],
message: "gateway remote mode misconfigured: gateway.remote.url missing",
remoteMissing: true,
},
...[
{ name: "the default report", args: ["skills", "--json"] },
{ name: "list", args: ["skills", "list", "--json"] },
{ name: "info", args: ["skills", "info", "fixture", "--json"] },
{ name: "check", args: ["skills", "check", "--json"] },
{ name: "curator status", args: ["skills", "curator", "status", "--json"] },
{ name: "curator pin", args: ["skills", "curator", "pin", "fixture", "--json"] },
{ name: "curator unpin", args: ["skills", "curator", "unpin", "fixture", "--json"] },
{ name: "curator restore", args: ["skills", "curator", "restore", "fixture", "--json"] },
{
name: "workshop apply",
args: ["skills", "workshop", "apply", "fixture-proposal", "--json"],
},
].map(({ name, args }) => ({
name: `${name} after an explicit environment Gateway fails`,
args,
message: "AUTOQA_SELECTED_GATEWAY_FAILURE",
explicitGateway: true,
})),
{
name: "retired curator mutation",
args: ["skills", "curator", "pin", "missing-skill", "--json"],
message:
"Skill lifecycle curation is retired. The weekly collection review manages the skill collection; pin, unpin, and restore no longer exist.",
},
{
name: "retired curator mutation with parent JSON",
args: ["skills", "curator", "--json", "pin", "missing-skill"],
message:
"Skill lifecycle curation is retired. The weekly collection review manages the skill collection; pin, unpin, and restore no longer exist.",
},
{
name: "workshop workspace validation with parent JSON",
args: ["skills", "--json", "workshop", "list", "--agent", ""],
message: "--agent must not be blank",
},
{
name: "workshop mutation",
args: ["skills", "workshop", "reject", "missing-proposal", "--json"],
message: "Skill proposal not found: missing-proposal",
},
{
name: "workshop inspection",
args: ["skills", "workshop", "inspect", "missing-proposal", "--json"],
message: "Skill proposal not found: missing-proposal",
},
])("returns one canonical JSON document when skills $name fails", async (testCase) => {
await withTempHome(
async (tempHome) => {
const configPath = path.join(tempHome, "missing-openclaw.json");
if ("remoteMissing" in testCase) {
await fs.writeFile(configPath, JSON.stringify({ gateway: { mode: "remote" } }));
}
const preload = `data:text/javascript,${encodeURIComponent(
[
'globalThis.fetch = async () => new Response("offline fixture", { status: 400 });',
...("explicitGateway" in testCase
? [
'import net from "node:net";',
'net.Socket.prototype.connect = function () { throw new Error("AUTOQA_SELECTED_GATEWAY_FAILURE"); };',
]
: []),
].join("\n"),
)}`;
const result = runBuiltCli(
tempHome,
testCase.args,
{
OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"),
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_GATEWAY_PORT: "1",
...("explicitGateway" in testCase
? {
OPENCLAW_GATEWAY_URL: "ws://127.0.0.1:9",
OPENCLAW_GATEWAY_TOKEN: "fixture-token",
}
: {}),
},
{ execArgv: [`--import=${preload}`] },
);
const message =
"remoteMissing" in testCase
? [
testCase.message,
`Config: ${configPath}`,
"Fix: set gateway.remote.url, or set gateway.mode=local.",
].join("\n")
: testCase.message;
expect(result.status, result.stderr).toBe(1);
expect(JSON.parse(result.stdout)).toEqual({
ok: false,
error: {
type: "cli_error",
message,
},
});
expect(result.stderr).toContain(message);
expect(result.stderr.length).toBeLessThan(2_048);
},
{ prefix: "openclaw-skills-json-failure-e2e-" },
);
});
it.each([
{ name: "off", debug: "0", includesCause: false },
{ name: "on", debug: "1", includesCause: true },
])("keeps skills search nested causes behind debug mode ($name)", async (testCase) => {
await withTempHome(
async (tempHome) => {
// Match the selected runtime's SyntaxError for the same malformed response.
let syntaxError: unknown;
try {
JSON.parse("not-json");
} catch (error) {
syntaxError = error;
}
assert(syntaxError instanceof SyntaxError);
const preload = `data:text/javascript,${encodeURIComponent(
'globalThis.fetch = async () => new Response("not-json", { status: 200 });',
)}`;
const result = runBuiltCli(
tempHome,
["skills", "search", "fixture"],
{
OPENCLAW_DEBUG: testCase.debug,
OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"),
OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"),
},
{ execArgv: [`--import=${preload}`] },
);
expect(result.status, result.stderr).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("ClawHub /api/v1/search returned malformed JSON");
expect(result.stderr.includes(syntaxError.message)).toBe(testCase.includesCause);
},
{ prefix: "openclaw-skills-human-failure-e2e-" },
);
});
});