import { spawnSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { fixtureCapabilityConsentArgs } from "../../scripts/e2e/lib/package-compat.mjs"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); const consent = "--accept-capabilities"; const BASH_BIN = process.platform === "win32" ? "bash" : "/bin/bash"; function writeCandidate( root: string, options: { commands?: string[]; helpStatus?: number; hang?: boolean; exitCode?: number; preserveUninstallIntent?: boolean; } = {}, name = "candidate.cjs", ) { const entry = path.join(root, name); writeFileSync( entry, ` const fs = require('node:fs'); const options = ${JSON.stringify(options)}; const args = process.argv.slice(2); fs.appendFileSync(process.env.ARGV_LOG, JSON.stringify(args) + '\\n'); const supported = (options.commands ?? ['install', 'enable', 'update']).includes(args[1]); if (args.includes('--help')) { console.log('OpenClaw 2026.8.1\\nOptions:'); console.log(supported ? ' --accept-capabilities Accept capabilities' : ' --force Confirm source'); if (options.hang) setInterval(() => {}, 1000); else process.exit(options.helpStatus ?? 0); } else { if (options.preserveUninstallIntent) { const marker = process.env.ARGV_LOG + '.disabled'; if (args[0] === 'plugins' && args[2] === 'demo-plugin-npm') { if (args[1] === 'uninstall') fs.writeFileSync(marker, 'disabled'); if (args[1] === 'enable') fs.rmSync(marker, { force: true }); } if (args[0] === 'demo-npm' && fs.existsSync(marker)) { console.error('OpenClaw does not know the command "demo-npm".'); process.exit(43); } } const accepted = args.includes('--accept-capabilities'); if (accepted && !supported) { console.error('unknown option --accept-capabilities'); process.exit(2); } if (args[2]?.includes('invalid-metadata') || args[2] === 'missing-version') process.exit(41); const needsConsent = args[0] === 'plugins' && (args[1] === 'install' || (args[1] === 'enable' && args[2] === 'claude-bundle-e2e') || (args[1] === 'update' && args[2] === 'marketplace-shortcut' && !args.includes('--dry-run'))); if (supported && needsConsent && !accepted) { console.error('requires capability consent'); process.exit(42); } console.log('{}'); process.exit(options.exitCode ?? 0); } `, ); return entry; } function runShell(root: string, entry: string, script: string) { const result = spawnSync(BASH_BIN, ["-c", `set -euo pipefail\n${script}`], { encoding: "utf8", timeout: 30_000, env: { ...process.env, HOME: root, OPENCLAW_ENTRY: entry, OPENCLAW_PLUGINS_TMP_DIR: root, KITCHEN_SINK_TMP_DIR: root, ARGV_LOG: path.join(root, "argv.jsonl"), OPENCLAW_PLUGINS_E2E_CLAWHUB: "0", OPENCLAW_PLUGINS_CLI_TIMEOUT: "5s", OPENCLAW_TEST_STATE_SCRIPT_B64: Buffer.from(":").toString("base64"), }, }); const log = path.join(root, "argv.jsonl"); const calls: string[][] = existsSync(log) ? readFileSync(log, "utf8") .trim() .split("\n") .map((line) => JSON.parse(line)) : []; return { ...result, calls }; } const fixtureCommand = ` source scripts/lib/openclaw-e2e-instance.sh openclaw_e2e_fixture_plugin_command openclaw_e2e_maybe_timeout 5s node "$OPENCLAW_ENTRY" -- `.trim(); // Replace artifact construction and inventory assertions, not the CLI runner or scenario order. const sweepFixtureLoader = ` source() { if [[ "$1" == scripts/e2e/lib/plugins/fixtures.sh ]]; then pack_fixture_plugin() { :; } pack_fixture_plugin_with_cli_registry_dependency() { :; } pack_fake_is_number_package() { :; } pack_fixture_plugin_with_invalid_extension_entry() { :; } start_npm_fixture_registry() { :; } openclaw_plugins_cleanup_fixture_servers() { :; } else builtin source "$@" fi } node() { case "$1" in scripts/e2e/lib/fixture.mjs) mkdir -p "$3" ;; scripts/e2e/lib/plugins/assertions.mjs) return 0 ;; *) command node "$@" ;; esac } git() { printf 'fixture-commit\\n'; } source scripts/e2e/lib/plugins/sweep.sh `; const kitchenFixtureLoader = ` export KITCHEN_SINK_SWEEP_SOURCE_ONLY=1 source scripts/e2e/lib/kitchen-sink-plugin/sweep.sh node() { case "$1" in scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs) return 0 ;; *) command node "$@" ;; esac } KITCHEN_SINK_LABEL=fixture KITCHEN_SINK_ID=fixture-id KITCHEN_SINK_SPEC=fixture-spec KITCHEN_SINK_PREINSTALL_SPEC=preinstall-spec KITCHEN_SINK_SOURCE=npm run_success_scenario KITCHEN_SINK_SPEC=missing-version run_failure_scenario `; describe("package fixture consent compatibility", () => { it.each([true, false])( "runs the plugin sweep with selective consent (supported=%s)", (supported) => { const root = tempDirs.make("openclaw-consent-sweep-"); const entry = writeCandidate(root, { commands: supported ? undefined : [], preserveUninstallIntent: true, }); const result = runShell(root, entry, sweepFixtureLoader); expect(result.status, result.stderr).toBe(0); const mutations = result.calls.filter( (args) => args[0] === "plugins" && !args.includes("--help"), ); const installs = mutations.filter((args) => args[1] === "install"); expect(installs).toHaveLength(12); for (const args of mutations) { const positive = (args[1] === "install" && !args.some((arg) => arg.includes("invalid-metadata"))) || args[1] === "enable" || (args[1] === "update" && args[2] === "marketplace-shortcut" && !args.includes("--dry-run")); expect(args.includes(consent), args.join(" ")).toBe(supported && positive); } expect(mutations.filter((args) => args[1] === "update")).toHaveLength(5); expect(mutations.filter((args) => args[1] === "enable").map((args) => args[2])).toEqual([ "demo-plugin-npm", "claude-bundle-e2e", ]); }, ); it.each([true, false])( "runs kitchen-sink success and registry failure (supported=%s)", (supported) => { const root = tempDirs.make("openclaw-consent-kitchen-"); const result = runShell( root, writeCandidate(root, { commands: supported ? undefined : [] }), kitchenFixtureLoader, ); expect(result.status, result.stderr).toBe(0); const installs = result.calls.filter( (args) => args[1] === "install" && !args.includes("--help"), ); expect(installs.map((args) => args[2])).toEqual([ "preinstall-spec", "fixture-spec", "missing-version", ]); expect(installs.map((args) => args.includes(consent))).toEqual([supported, supported, false]); expect(result.calls.find((args) => args[1] === "enable")).not.toContain(consent); }, ); it("probes each command and preserves literal argv", () => { const root = tempDirs.make("openclaw-consent-argv-"); const entry = writeCandidate(root, { commands: ["install", "update"] }); const result = runShell( root, entry, `${fixtureCommand} plugins install 'file:a b/$(literal)' --force '' ${fixtureCommand} plugins enable fixture ${fixtureCommand} plugins update fixture`, ); expect(result.status, result.stderr).toBe(0); expect(result.calls).toEqual([ ["plugins", "install", "--help"], ["plugins", "install", "file:a b/$(literal)", "--force", "", consent], ["plugins", "enable", "--help"], ["plugins", "enable", "fixture"], ["plugins", "update", "--help"], ["plugins", "update", "fixture", consent], ]); }); it.each([true, false])( "records candidate capability consent support (supported=%s)", (supported) => { const root = tempDirs.make("openclaw-consent-support-"); const result = runShell( root, writeCandidate(root, { commands: supported ? undefined : [] }), `${fixtureCommand} plugins install fixture printf 'support=%s\\n' "$OPENCLAW_E2E_LAST_FIXTURE_PLUGIN_CAPABILITY_CONSENT_SUPPORTED"`, ); expect(result.status, result.stderr).toBe(0); expect(result.stdout).toContain(`support=${supported ? "1" : "0"}`); }, ); it.each([true, false])( "consents to ClawHub install but not unchanged update (supported=%s)", (supported) => { const root = tempDirs.make("openclaw-consent-clawhub-"); const result = runShell( root, writeCandidate(root, { commands: supported ? undefined : [] }), ` export OPENCLAW_PLUGINS_SWEEP_SOURCE_ONLY=1 export OPENCLAW_PLUGINS_E2E_CLAWHUB=1 export OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB=1 export OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC=clawhub:@example/consent-fixture export OPENCLAW_PLUGINS_E2E_CLAWHUB_ID=consent-fixture source scripts/e2e/lib/plugins/sweep.sh node() { case "$1" in scripts/e2e/lib/plugins/assertions.mjs) return 0 ;; *) command node "$@" ;; esac } run_plugins_clawhub_scenario `, ); expect(result.status, result.stderr).toBe(0); expect( result.calls .find((args) => args[1] === "install" && !args.includes("--help")) ?.includes(consent), ).toBe(supported); expect(result.calls.find((args) => args[1] === "update")).not.toContain(consent); }, ); it("requires an explicit package identity for live ClawHub E2E", () => { const root = tempDirs.make("openclaw-clawhub-live-requirements-"); const result = runShell( root, writeCandidate(root), ` export OPENCLAW_PLUGINS_SWEEP_SOURCE_ONLY=1 export OPENCLAW_PLUGINS_E2E_CLAWHUB=1 export OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB=1 unset OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC OPENCLAW_PLUGINS_E2E_CLAWHUB_ID source scripts/e2e/lib/plugins/sweep.sh run_plugins_clawhub_scenario `, ); expect(result.status).toBe(2); expect(result.stderr).toContain("the Kitchen Sink listing has been retired"); }); it.each([ { environment: ` export OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB=0 unset OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC OPENCLAW_PLUGINS_E2E_CLAWHUB_ID `, expectedId: "openclaw-kitchen-sink-fixture", expectedSpec: "clawhub:@openclaw/plugin-e2e-fixture", name: "fixture default", }, { environment: ` export OPENCLAW_PLUGINS_E2E_LIVE_CLAWHUB=1 export OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC=clawhub:@example/custom-plugin export OPENCLAW_PLUGINS_E2E_CLAWHUB_ID=custom-plugin `, expectedId: "custom-plugin", expectedSpec: "clawhub:@example/custom-plugin", name: "explicit override", }, ])("selects the ClawHub identity for $name", ({ environment, expectedId, expectedSpec }) => { const root = tempDirs.make("openclaw-clawhub-identity-"); const result = runShell( root, writeCandidate(root), ` export OPENCLAW_PLUGINS_SWEEP_SOURCE_ONLY=1 export OPENCLAW_PLUGINS_E2E_CLAWHUB=1 ${environment} source scripts/e2e/lib/plugins/sweep.sh node() { case "$1" in scripts/e2e/lib/clawhub-fixture-server.cjs) printf '12345\\n' > "$3" while true; do sleep 1; done ;; scripts/e2e/lib/plugins/assertions.mjs) return 0 ;; *) command node "$@" ;; esac } run_plugins_clawhub_scenario `, ); expect(result.status, result.stderr).toBe(0); const install = result.calls.find((args) => args[1] === "install" && !args.includes("--help")); expect(install?.[2]).toBe(expectedSpec); expect(result.calls.filter((args) => args[1] === "inspect").map((args) => args[2])).toEqual([ expectedId, expectedId, ]); expect(result.calls.find((args) => args[1] === "update")?.[2]).toBe(expectedId); expect(result.calls.find((args) => args[1] === "uninstall")?.[2]).toBe(expectedSpec); }); it.each([ [" --accept-capabilities Accept\n", [consent]], [" \u001b[32m--accept-capabilities\u001b[0m Accept\n", [consent]], [" --accept-capabilities-extra Other\n", []], ["See --accept-capabilities in newer releases\n", []], [" --force Confirm\n", []], ])("reads only an advertised option from help %j", (help, expected) => { expect(fixtureCapabilityConsentArgs(help)).toEqual(expected); }); it.each([ ["plugins", "error"], ["plugins", "timeout"], ["kitchen-sink", "error"], ["kitchen-sink", "timeout"], ])("%s logger fails before mutation on help %s", (suite, failure) => { const root = tempDirs.make("openclaw-consent-failure-"); const entry = writeCandidate(root, failure === "error" ? { helpStatus: 47 } : { hang: true }); const result = runShell( root, entry, suite === "plugins" ? ` export OPENCLAW_PLUGINS_SWEEP_SOURCE_ONLY=1 export OPENCLAW_PLUGINS_CLI_TIMEOUT=1s source scripts/e2e/lib/plugins/sweep.sh run_plugins_fixture_logged failure plugins install fixture --force ` : ` export KITCHEN_SINK_SWEEP_SOURCE_ONLY=1 export KITCHEN_SINK_CLI_TIMEOUT=1s source scripts/e2e/lib/kitchen-sink-plugin/sweep.sh run_kitchen_sink_fixture_logged failure plugins install fixture --force `, ); expect(result.status, result.stderr).toBe(failure === "error" ? 47 : 124); expect(result.stdout + result.stderr).toContain("help probe failed"); expect(result.calls).toEqual([["plugins", "install", "--help"]]); }); it("returns the mutation exit status", () => { const root = tempDirs.make("openclaw-consent-exit-"); const result = runShell( root, writeCandidate(root, { exitCode: 49 }), `${fixtureCommand} plugins install fixture`, ); expect(result.status).toBe(49); expect(result.calls.at(-1)).toEqual(["plugins", "install", "fixture", consent]); }); it("reprobes a replacement at the same executable path and version", () => { const root = tempDirs.make("openclaw-consent-replacement-"); const entry = writeCandidate(root, { commands: [] }); writeCandidate(root, {}, "replacement.cjs"); const result = runShell( root, entry, `${fixtureCommand} plugins install fixture cp "$HOME/replacement.cjs" "$OPENCLAW_ENTRY" ${fixtureCommand} plugins install fixture`, ); expect(result.status, result.stderr).toBe(0); expect(result.calls).toEqual([ ["plugins", "install", "--help"], ["plugins", "install", "fixture"], ["plugins", "install", "--help"], ["plugins", "install", "fixture", consent], ]); }); });