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 ", }, { 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, { NODE_OPTIONS: `--import=${preload}`, 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: "", }); 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"], { NODE_OPTIONS: `--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 ", }, }); expect(result.stdout).not.toContain("\u001B"); expect(result.stdout).not.toContain("\u0007"); expect(result.stderr).toContain("Usage: openclaw plugins search "); 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" } : {}), ...("tty" in testCase ? { NODE_OPTIONS: `--import=data:text/javascript;base64,${ttyPreload}` } : {}), }, { inheritEnvironment: false }, ); 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-" }, ); }); });