mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
Consolidate script lifecycle and helper ownership while preserving CLI, generated-output, cleanup, and guard contracts. Reject inherited object keys in model-matrix option admission with the existing unknown-argument diagnostic. Validation: 90 full sibling suites; original-fails parser regression; byte-identical native outputs; changed checks; SDK surface and API comparison; both zero-cycle checks; independent isolated review.
414 lines
14 KiB
TypeScript
414 lines
14 KiB
TypeScript
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],
|
|
]);
|
|
});
|
|
});
|