fix(install): avoid success after incomplete lifecycle changes (#125992)

* fix(install): make lifecycle mutations transactional

Standalone installers now apply npm-version-aware lifecycle approval. Updates verify and repair the installation before reporting success and preserve the prior install owner during method switches. Uninstall now exits nonzero when requested cleanup is only partially completed. Plugin update behavior is unchanged.

Closes #125925

* test(uninstall): assert aggregated live-owner failure

* fix(install): satisfy standalone shell checks

* fix(update): scan PATH for prior Git wrapper

* test(hooks): await Gmail watcher descendant exit

* fix(install): verify Windows npm candidate

* fix(ci): normalize package acceptance version

* fix(update): preserve staged local package links

* test(update): fold staged symlink coverage

* fix(update): retire every legacy Git wrapper

* test(docs): align consolidated ownership checks
This commit is contained in:
Peter Steinberger
2026-08-18 20:50:15 -07:00
committed by GitHub
parent a441431896
commit 7bc994aee8
32 changed files with 2119 additions and 333 deletions
+103 -2
View File
@@ -23,6 +23,7 @@ import {
writeNpmBeforePolicyFixture,
writeNpmFreshnessConflictFixture,
writeNpmInstallRetryFixture,
writeNpmLifecycleFixture,
} from "./install-npm-fixtures.js";
const SCRIPT_PATH = "scripts/install-cli.sh";
@@ -604,7 +605,10 @@ describe("install-cli.sh", () => {
const dependencyInstallIndex = script.indexOf(
'CI="${CI:-true}" run_pnpm -C "$repo_dir" install "$install_lockfile_flag"',
);
const wrapperIndex = script.indexOf('cat > "${PREFIX}/bin/openclaw"', compatibilityIndex);
const wrapperIndex = script.indexOf(
'publish_executable_wrapper "${PREFIX}/bin/openclaw"',
compatibilityIndex,
);
expect(checkoutIndex).toBeGreaterThan(-1);
expect(compatibilityIndex).toBeGreaterThan(checkoutIndex);
@@ -660,6 +664,7 @@ describe("install-cli.sh", () => {
"set -euo pipefail",
`cd ${JSON.stringify(process.cwd())}`,
`source ${JSON.stringify(SCRIPT_PATH)}`,
"npm_lifecycle_allow_arg() { :; }",
'install_node() { mkdir -p "$(node_dir)/lib/node_modules/openclaw/dist"; : > "$(node_dir)/lib/node_modules/openclaw/dist/entry.js"; }',
"ensure_git() { :; }",
'npm_bin() { printf "/usr/bin/true\\n"; }',
@@ -773,7 +778,7 @@ describe("install-cli.sh", () => {
symlinkSync("node-v24.15.0", join(prefix, "tools", "node"));
writeFileSync(
join(nodeDir, "bin", "npm"),
'#!/bin/bash\nif [[ "$1" == "config" ]]; then printf "null\\n"; fi\n',
'#!/bin/bash\nif [[ "$1" == "--version" ]]; then printf "11.15.0\\n"; elif [[ "$1" == "config" ]]; then printf "null\\n"; fi\n',
);
chmodSync(join(nodeDir, "bin", "npm"), 0o755);
for (const entry of [
@@ -1599,6 +1604,100 @@ describe("install-cli.sh", () => {
expect(script).toContain("env -u NPM_CONFIG_BEFORE -u npm_config_before");
});
it.each([
{ expected: "", version: "11.15.0" },
{ expected: "--allow-scripts=openclaw", version: "11.16.0" },
{ expected: "--allow-scripts=openclaw", version: "12.0.0" },
])("resolves canonical npm lifecycle policy for npm $version", ({ expected, version }) => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-lifecycle-"));
const npm = join(tmp, "npm");
writeNpmLifecycleFixture(npm);
try {
const result = runInstallCliShell(
[
`source ${JSON.stringify(SCRIPT_PATH)}`,
`result="$(npm_lifecycle_allow_arg ${JSON.stringify(npm)} openclaw@latest)"`,
`printf '%s' "$result"`,
].join("\n"),
{ NPM_FAKE_VERSION: version },
);
expect(result.status).toBe(0);
expect(result.stdout).toBe(expected);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("rejects an invalid npm version before mutation", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-lifecycle-invalid-"));
const npm = join(tmp, "npm");
const args = join(tmp, "args");
writeNpmLifecycleFixture(npm);
try {
const result = runInstallCliShell(
[
`source ${JSON.stringify(SCRIPT_PATH)}`,
`npm_lifecycle_allow_arg ${JSON.stringify(npm)} openclaw@latest`,
].join("\n"),
{ NPM_FAKE_ARGS: args, NPM_FAKE_VERSION: "invalid" },
);
expect(result.status).not.toBe(0);
expect(existsSync(args)).toBe(false);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it.each([
["openclaw@npm:@scope/candidate@1.0.0", "--allow-scripts=@scope/candidate"],
["file:/tmp/openclaw.tgz", "--allow-scripts=file:/tmp/openclaw.tgz"],
[
"https://example.invalid/openclaw.tgz",
"--allow-scripts=https://example.invalid/openclaw.tgz",
],
])("uses npm-resolved lifecycle identity for %s", (spec, expected) => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-identity-"));
const npm = join(tmp, "npm");
writeNpmLifecycleFixture(npm);
try {
const result = runInstallCliShell(
[
`source ${JSON.stringify(SCRIPT_PATH)}`,
`npm_lifecycle_allow_arg ${JSON.stringify(npm)} ${JSON.stringify(spec)}`,
].join("\n"),
{ NPM_FAKE_VERSION: "12.0.0" },
);
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe(expected);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("relativizes absolute npm path identities against the command cwd", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-identity-comma,"));
const npm = join(tmp, "npm");
const commandCwd = join(tmp, "safe");
const candidate = join(tmp, "candidate.tgz");
mkdirSync(commandCwd);
writeNpmLifecycleFixture(npm);
try {
const result = runInstallCliShell(
[
`source ${JSON.stringify(SCRIPT_PATH)}`,
`node_bin() { printf '%s\\n' ${JSON.stringify(process.execPath)}; }`,
`cd ${JSON.stringify(commandCwd)}`,
`npm_lifecycle_allow_arg ${JSON.stringify(npm)} ${JSON.stringify(candidate)} "$PWD"`,
].join("\n"),
{ NPM_FAKE_VERSION: "12.0.0" },
);
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("--allow-scripts=../candidate.tgz");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});
it("does not emit --before when raw user npmrc config contains min-release-age", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-npmrc-"));
const bin = join(tmp, "bin");
@@ -1638,6 +1737,7 @@ describe("install-cli.sh", () => {
"set -euo pipefail",
`cd ${JSON.stringify(process.cwd())}`,
`source ${JSON.stringify(SCRIPT_PATH)}`,
"npm_lifecycle_allow_arg() { :; }",
`npm_bin() { printf '%s\\n' ${JSON.stringify(fakeNpm)}; }`,
`node_dir() { printf '%s\\n' ${JSON.stringify(nodeDir)}; }`,
"emit_json() { :; }",
@@ -1722,6 +1822,7 @@ describe("install-cli.sh", () => {
"set -euo pipefail",
`cd ${JSON.stringify(process.cwd())}`,
`source ${JSON.stringify(SCRIPT_PATH)}`,
"npm_lifecycle_allow_arg() { :; }",
`npm_bin() { printf '%s\\n' ${JSON.stringify(fakeNpm)}; }`,
`node_dir() { printf '%s\\n' ${JSON.stringify(nodeDir)}; }`,
"emit_json() { :; }",