mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 05:54:09 +08:00
257 lines
10 KiB
TypeScript
257 lines
10 KiB
TypeScript
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: "an ownerless explicit fleet", ownerSelected: false },
|
|
{ name: "an explicit system owner", ownerSelected: true },
|
|
])("preserves plugin diagnostic classifications for $name", async ({ ownerSelected }) => {
|
|
await withTempHome(
|
|
async (tempHome) => {
|
|
const configPath = path.join(tempHome, "openclaw.json");
|
|
const config = {
|
|
agents: {
|
|
ownership: "explicit",
|
|
entries: { alpha: {}, beta: {} },
|
|
...(ownerSelected ? { defaults: { systemAgent: { agentId: "alpha" } } } : {}),
|
|
},
|
|
plugins: { enabled: false },
|
|
};
|
|
const configText = JSON.stringify(config);
|
|
await fs.writeFile(configPath, configText);
|
|
const env = {
|
|
OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"),
|
|
OPENCLAW_CONFIG_PATH: configPath,
|
|
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
|
|
NO_COLOR: "1",
|
|
};
|
|
const list = runBuiltCli(tempHome, ["plugins", "list", "--json"], env, {
|
|
inheritEnvironment: false,
|
|
});
|
|
expect(list.error).toBeUndefined();
|
|
expect(list.signal).toBeNull();
|
|
expect(list.status, list.stderr).toBe(0);
|
|
const inventory = JSON.parse(list.stdout);
|
|
expect(inventory.workspaceScope).toBe(ownerSelected ? "selected" : "omitted");
|
|
const workspaceDiagnostic = inventory.diagnostics.find(
|
|
(entry: { code?: string }) => entry.code === "workspace-scope-omitted",
|
|
);
|
|
if (ownerSelected) {
|
|
expect(workspaceDiagnostic).toBeUndefined();
|
|
} else {
|
|
expect(workspaceDiagnostic).toMatchObject({
|
|
level: "warn",
|
|
code: "workspace-scope-omitted",
|
|
message: expect.stringContaining("Workspace plugin discovery was skipped"),
|
|
});
|
|
}
|
|
|
|
const doctorJson = runBuiltCli(tempHome, ["plugins", "doctor", "--json"], env, {
|
|
inheritEnvironment: false,
|
|
});
|
|
const doctorText = runBuiltCli(tempHome, ["plugins", "doctor"], env, {
|
|
inheritEnvironment: false,
|
|
});
|
|
for (const result of [doctorJson, doctorText]) {
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.signal).toBeNull();
|
|
expect(result.status, result.stderr).toBe(ownerSelected ? 0 : 1);
|
|
}
|
|
expect(doctorJson.stdout).not.toContain("\u001B");
|
|
expect(doctorJson.stdout).not.toContain("\u0007");
|
|
const diagnostics = JSON.parse(doctorJson.stdout);
|
|
expect(diagnostics.ok).toBe(ownerSelected);
|
|
expect(await fs.readFile(configPath, "utf8")).toBe(configText);
|
|
if (ownerSelected) {
|
|
expect(diagnostics.diagnostics).not.toContainEqual(
|
|
expect.objectContaining({ code: "workspace-scope-omitted" }),
|
|
);
|
|
expect(doctorText.stdout).not.toContain("Workspace plugin discovery was skipped");
|
|
expect(doctorText.stdout).toContain("configuration checks passed");
|
|
} else {
|
|
expect(doctorText.stdout).toContain(workspaceDiagnostic.message);
|
|
expect(doctorText.stdout).not.toContain("configuration checks passed");
|
|
expect(diagnostics.diagnostics).toContainEqual(workspaceDiagnostic);
|
|
}
|
|
},
|
|
{ prefix: "openclaw-plugin-doctor-classification-e2e-" },
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "the search query is missing",
|
|
args: ["plugins", "search", "--json"],
|
|
message: "Usage: openclaw plugins search <query>",
|
|
},
|
|
{
|
|
name: "ClawHub transport fails",
|
|
args: ["plugins", "search", "fixture", "--json"],
|
|
message: "offline fixture",
|
|
},
|
|
])("returns one canonical JSON document when plugins $name", async (testCase) => {
|
|
await withTempHome(
|
|
async (tempHome) => {
|
|
const preload = `data:text/javascript,${encodeURIComponent(
|
|
'globalThis.fetch = async () => { throw new Error("offline fixture"); };',
|
|
)}`;
|
|
const result = runBuiltCli(
|
|
tempHome,
|
|
testCase.args,
|
|
{
|
|
OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"),
|
|
OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"),
|
|
CLAWHUB_CONFIG_PATH: path.join(tempHome, "missing-clawhub.json"),
|
|
CLAWHUB_TOKEN: "",
|
|
CLAWHUB_AUTH_TOKEN: "",
|
|
},
|
|
{ execArgv: [`--import=${preload}`] },
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stdout, result.stderr).not.toBe("");
|
|
expect(result.stdout).not.toContain("\u001B");
|
|
expect(result.stdout).not.toContain("\u0007");
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
ok: false,
|
|
error: {
|
|
type: "cli_error",
|
|
message: testCase.message,
|
|
},
|
|
});
|
|
expect(result.stderr).toContain(testCase.message);
|
|
},
|
|
{ prefix: "openclaw-plugins-json-failure-e2e-" },
|
|
);
|
|
});
|
|
|
|
it("keeps plugins search JSON failures clean through dual-TTY finalization", async () => {
|
|
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,
|
|
["plugins", "search", "--json"],
|
|
{},
|
|
{
|
|
execArgv: [`--import=${preload}`],
|
|
},
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
ok: false,
|
|
error: {
|
|
type: "cli_error",
|
|
message: "Usage: openclaw plugins search <query>",
|
|
},
|
|
});
|
|
expect(result.stdout).not.toContain("\u001B");
|
|
expect(result.stdout).not.toContain("\u0007");
|
|
expect(result.stderr).toContain("Usage: openclaw plugins search <query>");
|
|
expect(result.stderr).toContain("\u001B[?25h");
|
|
},
|
|
{ prefix: "openclaw-plugins-json-tty-failure-e2e-" },
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{ name: "a missing local marketplace", source: "local" },
|
|
{
|
|
name: "a missing local marketplace through forced Commander",
|
|
source: "local",
|
|
commander: true,
|
|
},
|
|
{ name: "a missing local marketplace with dual TTYs", source: "local", tty: true },
|
|
{ name: "an unavailable Git marketplace", source: "git" },
|
|
{ name: "an unavailable Git marketplace with dual TTYs", source: "git", tty: true },
|
|
{ name: "a missing local marketplace in human mode", source: "local", human: true },
|
|
{ name: "an unavailable Git marketplace in human mode", source: "git", human: true },
|
|
])("keeps $name failures on the canonical marketplace output boundary", async (testCase) => {
|
|
await withTempHome(
|
|
async (tempHome) => {
|
|
const stateDir = path.join(tempHome, "isolated-state");
|
|
const configPath = path.join(tempHome, "missing-openclaw.json");
|
|
const source =
|
|
testCase.source === "git"
|
|
? "ssh://marketplace.invalid/openclaw/unavailable.git"
|
|
: path.join(tempHome, "missing-marketplace");
|
|
const ttyPreload = Buffer.from(
|
|
'Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true }); Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });',
|
|
).toString("base64");
|
|
|
|
await expect(fs.stat(stateDir)).rejects.toMatchObject({ code: "ENOENT" });
|
|
|
|
const result = runBuiltCli(
|
|
tempHome,
|
|
["plugins", "marketplace", "list", source, ...("human" in testCase ? [] : ["--json"])],
|
|
{
|
|
GIT_SSH_COMMAND: `${JSON.stringify(process.execPath)} -e "process.exit(1)"`,
|
|
GIT_TERMINAL_PROMPT: "0",
|
|
OPENCLAW_CONFIG_PATH: configPath,
|
|
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
|
|
OPENCLAW_STATE_DIR: stateDir,
|
|
...("commander" in testCase ? { OPENCLAW_DISABLE_ROUTE_FIRST: "1" } : {}),
|
|
},
|
|
{
|
|
inheritEnvironment: false,
|
|
execArgv:
|
|
"tty" in testCase ? [`--import=data:text/javascript;base64,${ttyPreload}`] : [],
|
|
},
|
|
);
|
|
const expectedMessage =
|
|
testCase.source === "git"
|
|
? `failed to clone marketplace source ${source}:`
|
|
: `unsupported marketplace source: ${source}`;
|
|
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stderr).toContain(expectedMessage);
|
|
|
|
if ("human" in testCase) {
|
|
if (testCase.source === "git") {
|
|
expect(result.stdout).toContain(`Cloning marketplace source ${source}...`);
|
|
}
|
|
} else {
|
|
expect(result.stdout, result.stderr).not.toContain("\u001B");
|
|
expect(result.stdout, result.stderr).not.toContain("\u0007");
|
|
expect(result.stdout).not.toContain("Cloning marketplace source");
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
ok: false,
|
|
error: {
|
|
type: "cli_error",
|
|
message:
|
|
testCase.source === "git"
|
|
? expect.stringContaining(expectedMessage)
|
|
: expectedMessage,
|
|
},
|
|
});
|
|
if ("tty" in testCase) {
|
|
expect(result.stderr).toContain("\u001B[?25h");
|
|
}
|
|
}
|
|
|
|
if (testCase.source === "git") {
|
|
expect(result.stderr).toContain("fatal: Could not read from remote repository.");
|
|
const clonePath = /Cloning into '([^']+)'\.\.\./u.exec(result.stderr)?.[1];
|
|
expect(clonePath).toBeDefined();
|
|
await expect(fs.stat(path.dirname(clonePath as string))).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
}
|
|
|
|
await expect(fs.stat(stateDir)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(fs.stat(configPath)).rejects.toMatchObject({ code: "ENOENT" });
|
|
await expect(fs.stat(path.join(tempHome, ".claude"))).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
},
|
|
{ prefix: "openclaw-marketplace-json-failure-e2e-" },
|
|
);
|
|
});
|
|
});
|