mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
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:
committed by
GitHub
parent
a441431896
commit
7bc994aee8
@@ -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() { :; }",
|
||||
|
||||
@@ -7,6 +7,7 @@ export function writeNpmInstallRetryFixture(path: string) {
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'if [[ "${1:-}" == "--version" ]]; then printf "11.15.0\\n"; exit 0; fi',
|
||||
'if [[ "${1:-}" == "config" ]]; then printf "null\\n"; exit 0; fi',
|
||||
'if [[ "${1:-}" == "view" ]]; then printf "2026.8.1\\n"; exit 0; fi',
|
||||
'if [[ "${1:-}" == "root" ]]; then printf "%s\\n" "${NPM_FAKE_ROOT:-}"; exit 0; fi',
|
||||
@@ -15,7 +16,7 @@ export function writeNpmInstallRetryFixture(path: string) {
|
||||
'if [[ "$is_install" -eq 0 ]]; then exit 0; fi',
|
||||
'spec="${!#}"',
|
||||
'printf "%s\\n" "$spec" >> "$NPM_FAKE_CALLS"',
|
||||
'attempt="$(wc -l < "$NPM_FAKE_CALLS")"',
|
||||
'attempt="$(awk \'END { print NR }\' "$NPM_FAKE_CALLS")"',
|
||||
'if [[ "$NPM_FAKE_OUTCOME" == "success" || "$NPM_FAKE_OUTCOME" == "transient" && "$attempt" -eq 2 ]]; then',
|
||||
' if [[ -n "${NPM_FAKE_PACKAGE_DIR:-}" ]]; then',
|
||||
' mkdir -p "$NPM_FAKE_PACKAGE_DIR/dist"',
|
||||
@@ -39,6 +40,7 @@ export function writeNpmFreshnessConflictFixture(path: string, argsLog: string)
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'if [[ "${1:-}" == "--version" ]]; then printf "11.15.0\\n"; exit 0; fi',
|
||||
`printf '%s\\n' "$*" >> ${JSON.stringify(argsLog)}`,
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "min-release-age" ]]; then',
|
||||
" printf 'null\\n'",
|
||||
@@ -74,6 +76,7 @@ export function writeNpmBeforePolicyFixture(path: string, argsLog: string) {
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'if [[ "${1:-}" == "--version" ]]; then printf "11.15.0\\n"; exit 0; fi',
|
||||
`printf '%s\\n' "$*" >> ${JSON.stringify(argsLog)}`,
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "min-release-age" ]]; then',
|
||||
" printf 'null\\n'",
|
||||
@@ -100,3 +103,27 @@ export function writeNpmBeforePolicyFixture(path: string, argsLog: string) {
|
||||
);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
export function writeNpmLifecycleFixture(path: string) {
|
||||
writeFileSync(
|
||||
path,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'if [[ "${1:-}" == "--version" ]]; then',
|
||||
' [[ "${NPM_FAKE_VERSION_STATUS:-0}" == "0" ]] || exit "$NPM_FAKE_VERSION_STATUS"',
|
||||
' printf "%s\\n" "$NPM_FAKE_VERSION"',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "${1:-}" == "root" ]]; then printf "%s\\n" "$NPM_FAKE_ROOT"; exit 0; fi',
|
||||
'if [[ "${1:-}" == "config" ]]; then printf "null\\n"; exit 0; fi',
|
||||
'printf "%s\\n" "$*" >> "$NPM_FAKE_ARGS"',
|
||||
'mkdir -p "$NPM_FAKE_PACKAGE_DIR/dist"',
|
||||
'printf "#!/usr/bin/env node\\n" > "$NPM_FAKE_PACKAGE_DIR/dist/entry.js"',
|
||||
'if [[ "${NPM_FAKE_KEEP_GUARD:-0}" == "1" ]]; then : > "$NPM_FAKE_PACKAGE_DIR/dist/openclaw-install-guard"; else rm -f "$NPM_FAKE_PACKAGE_DIR/dist/openclaw-install-guard"; fi',
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
@@ -171,7 +171,103 @@ describe("install.ps1 failure handling", () => {
|
||||
"$output = @(Run-Doctor *>&1 | ForEach-Object { $_.ToString() })",
|
||||
'$text = $output -join "`n"',
|
||||
"if ($text -match 'Migration complete') { throw 'doctor failure reported success' }",
|
||||
"if ($text -notmatch 'Migration failed') { throw \"missing warning: $text\" }",
|
||||
"if ($text -notmatch 'Migration failed') { throw \"missing error: $text\" }",
|
||||
"if ($output[-1] -ne $false) { throw 'doctor failure did not propagate' }",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "npm-lifecycle-policy",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"$script:NpmVersion = ''",
|
||||
"function Invoke-NpmCommand {",
|
||||
" param([string[]]$Arguments = @(), [string]$CommandPath, [string]$WorkingDirectory)",
|
||||
" if ($Arguments[0] -eq '--version') { Write-Output $script:NpmVersion; $global:LASTEXITCODE = 0; return }",
|
||||
" throw 'unexpected npm mutation'",
|
||||
"}",
|
||||
"$cases = @{ '11.15.0' = $null; '11.16.0' = '--allow-scripts=openclaw'; '12.0.0' = '--allow-scripts=openclaw' }",
|
||||
"foreach ($entry in $cases.GetEnumerator()) {",
|
||||
" $script:NpmVersion = $entry.Key",
|
||||
" $actual = Get-NpmLifecycleAllowArgument -NpmCommand 'npm.cmd' -InstallSpec 'openclaw@latest'",
|
||||
' if ($actual -ne $entry.Value) { throw "version=$($entry.Key) actual=$actual" }',
|
||||
"}",
|
||||
"$script:NpmVersion = '12.0.0'",
|
||||
"$alias = Get-NpmLifecycleAllowArgument -NpmCommand 'npm.cmd' -InstallSpec 'openclaw@npm:@scope/candidate@1.0.0'",
|
||||
"if ($alias -ne '--allow-scripts=@scope/candidate') { throw \"alias=$alias\" }",
|
||||
"$tarball = Get-NpmLifecycleAllowArgument -NpmCommand 'npm.cmd' -InstallSpec 'https://example.invalid/openclaw.tgz'",
|
||||
"if ($tarball -ne '--allow-scripts=https://example.invalid/openclaw.tgz') { throw \"tarball=$tarball\" }",
|
||||
'$commaRoot = Join-Path ([System.IO.Path]::GetTempPath()) "openclaw,identity"',
|
||||
'$safeCwd = Join-Path $commaRoot "safe"',
|
||||
'$candidate = Join-Path $commaRoot "candidate.tgz"',
|
||||
"$relative = Get-NpmLifecycleAllowArgument -NpmCommand 'npm.cmd' -InstallSpec $candidate -NpmCwd $safeCwd",
|
||||
"if ($relative -match ',' -or $relative -notmatch '^--allow-scripts=\.\.[\\/]candidate\.tgz$') { throw \"relative=$relative\" }",
|
||||
"$script:NpmVersion = 'invalid'",
|
||||
"$caught = $false",
|
||||
"try { Get-NpmLifecycleAllowArgument -NpmCommand 'npm.cmd' -InstallSpec 'openclaw@latest' } catch { $caught = $true }",
|
||||
"if (-not $caught) { throw 'invalid npm version was accepted' }",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "npm-candidate-validation",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
'$root = Join-Path ([System.IO.Path]::GetTempPath()) ("openclaw-missing-candidate-" + [guid]::NewGuid().ToString("N"))',
|
||||
"New-Item -ItemType Directory -Path $root | Out-Null",
|
||||
"function Check-ExistingOpenClaw { return $true }",
|
||||
"function Check-Node { return $true }",
|
||||
"function Ensure-Git { return $true }",
|
||||
"function Test-PreviousGitWrapper { return $false }",
|
||||
"function Get-NpmCommandPath { return 'npm.cmd' }",
|
||||
"function Get-WindowsCommandSafeDirectory { return $root }",
|
||||
"function Resolve-NpmOpenClawInstallSpec { return 'openclaw@latest' }",
|
||||
"function Test-NpmConfigRawKey { return $true }",
|
||||
"function Get-NpmDebugLogRootCandidates { return @() }",
|
||||
"function Invoke-NpmCommand {",
|
||||
" param([string[]]$Arguments = @(), [string]$CommandPath, [string]$WorkingDirectory)",
|
||||
" $global:LASTEXITCODE = 0",
|
||||
" if ($Arguments[0] -eq '--version') { return '12.0.0' }",
|
||||
" if ($Arguments[0] -eq 'root') { return $root }",
|
||||
" if ($Arguments[0] -eq 'config') { return $root }",
|
||||
" if ($Arguments[0] -eq 'install') { return }",
|
||||
" throw \"unexpected npm command: $($Arguments -join ' ')\"",
|
||||
"}",
|
||||
"function Ensure-OpenClawOnPath { throw 'old PATH command was accepted after missing candidate' }",
|
||||
"$InstallMethod = 'npm'",
|
||||
"$NoOnboard = $true",
|
||||
"$Tag = 'latest'",
|
||||
"try {",
|
||||
" $null = Main",
|
||||
' if ($script:InstallExitCode -ne 1) { throw "InstallExitCode=$script:InstallExitCode" }',
|
||||
"} finally {",
|
||||
" Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "method-switch-preservation",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"$script:OldOwnerRemoved = $false",
|
||||
"function Check-ExistingOpenClaw { return $true }",
|
||||
"function Check-Node { return $true }",
|
||||
"function Get-NpmCommandPath { return 'npm.cmd' }",
|
||||
"function Invoke-NpmCommand {",
|
||||
" param([string[]]$Arguments = @(), [string]$CommandPath, [string]$WorkingDirectory)",
|
||||
" if ($Arguments[0] -eq 'list') { $global:LASTEXITCODE = 0; return }",
|
||||
" if ($Arguments[0] -eq 'uninstall') { $script:OldOwnerRemoved = $true; $global:LASTEXITCODE = 0; return }",
|
||||
" throw 'unexpected npm command'",
|
||||
"}",
|
||||
"function Install-OpenClawFromGit { return $false }",
|
||||
"$InstallMethod = 'git'",
|
||||
"$NoOnboard = $true",
|
||||
"$null = Main",
|
||||
"if ($script:OldOwnerRemoved) { throw 'failed candidate retired the working npm owner' }",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
@@ -193,6 +289,39 @@ describe("install.ps1 failure handling", () => {
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "same-prefix-shim-transaction",
|
||||
source: [
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
'$root = Join-Path ([System.IO.Path]::GetTempPath()) ("openclaw-shim-transaction-" + [guid]::NewGuid().ToString("N"))',
|
||||
'$target = Join-Path $root "openclaw.cmd"',
|
||||
"try {",
|
||||
" New-Item -ItemType Directory -Force -Path $root | Out-Null",
|
||||
' $old = "@echo off`r`nnode `"C:\\old\\dist\\entry.js`" %*`r`n"',
|
||||
' $launcher = Join-Path $root "node_modules\\openclaw\\openclaw.mjs"',
|
||||
' $candidate = "@ECHO off`r`nGOTO start`r`n:find_dp0`r`nSET dp0=%~dp0`r`nEXIT /b`r`n:start`r`nSETLOCAL`r`nCALL :find_dp0`r`nnode `"%dp0%\\node_modules\\openclaw\\openclaw.mjs`" %*`r`n"',
|
||||
" [System.IO.File]::WriteAllText($target, $old)",
|
||||
" $backup = Start-NpmShimBackup -Path $target -ExpectedLauncher $launcher",
|
||||
" [System.IO.File]::WriteAllText($target, $candidate)",
|
||||
" Restore-NpmShimBackup -Backup $backup",
|
||||
" if ([System.IO.File]::ReadAllText($target) -ne $old) { throw 'failure did not restore old wrapper' }",
|
||||
" $backup = Start-NpmShimBackup -Path $target -ExpectedLauncher $launcher",
|
||||
" [System.IO.File]::WriteAllText($target, $candidate)",
|
||||
" Complete-NpmShimBackup -Backup $backup",
|
||||
" if ([System.IO.File]::ReadAllText($target) -ne $candidate) { throw 'success did not retain npm shim' }",
|
||||
" if (Test-Path -LiteralPath $backup.BackupPath) { throw 'committed backup remains' }",
|
||||
" [System.IO.File]::WriteAllText($target, $old)",
|
||||
" $backup = Start-NpmShimBackup -Path $target -ExpectedLauncher $launcher",
|
||||
' [System.IO.File]::WriteAllText($target, "@echo off`r`necho unrelated`r`n")',
|
||||
" $refused = $false",
|
||||
" try { Restore-NpmShimBackup -Backup $backup } catch { $refused = $true }",
|
||||
" if (-not $refused) { throw 'unrelated replacement was deleted' }",
|
||||
" if ([System.IO.File]::ReadAllText($target) -notmatch 'unrelated') { throw 'unrelated replacement changed' }",
|
||||
"} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }",
|
||||
"",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "canonical-temp-root",
|
||||
source: [
|
||||
@@ -803,6 +932,22 @@ describe("install.ps1 failure handling", () => {
|
||||
expectBatchedPowerShellCase("canonical-temp-root");
|
||||
});
|
||||
|
||||
runIfPowerShell("applies the canonical npm lifecycle version policy", () => {
|
||||
expectBatchedPowerShellCase("npm-lifecycle-policy");
|
||||
});
|
||||
|
||||
runIfPowerShell("rejects npm success without a usable candidate package", () => {
|
||||
expectBatchedPowerShellCase("npm-candidate-validation");
|
||||
});
|
||||
|
||||
runIfPowerShell("preserves the npm owner when a git replacement fails", () => {
|
||||
expectBatchedPowerShellCase("method-switch-preservation");
|
||||
});
|
||||
|
||||
runIfPowerShell("restores or commits the same-prefix npm shim transaction", () => {
|
||||
expectBatchedPowerShellCase("same-prefix-shim-transaction");
|
||||
});
|
||||
|
||||
runIfPowerShell("installs portable Git from multiple archive roots without collisions", () => {
|
||||
expectBatchedPowerShellCase("portable-git-layout");
|
||||
});
|
||||
@@ -826,12 +971,16 @@ describe("install.ps1 failure handling", () => {
|
||||
|
||||
it("runs npm install through the resolved command with quiet CI defaults", () => {
|
||||
const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw");
|
||||
expect(npmInstallBody).toContain("$npmOutput = Invoke-NpmCommand -Arguments");
|
||||
expect(npmInstallBody).toContain(
|
||||
"$npmOutput = Invoke-NpmCommand -CommandPath $npmCommand -WorkingDirectory $npmCwd -Arguments",
|
||||
);
|
||||
expect(npmInstallBody).toContain("$npmDebugLogRoots = @(Get-NpmDebugLogRootCandidates)");
|
||||
expect(npmInstallBody).toContain('$npmInstallArguments = @("install", "-g")');
|
||||
expect(npmInstallBody).toContain('Write-Host "[!] npm install failed; retrying once"');
|
||||
expect(
|
||||
npmInstallBody.match(/Invoke-NpmCommand -Arguments \$npmInstallArguments/g),
|
||||
npmInstallBody.match(
|
||||
/Invoke-NpmCommand -CommandPath \$npmCommand -WorkingDirectory \$npmCwd -Arguments \$npmInstallArguments/g,
|
||||
),
|
||||
).toHaveLength(2);
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_LOGLEVEL = "error"');
|
||||
expect(npmInstallBody).toContain('$env:NPM_CONFIG_UPDATE_NOTIFIER = "false"');
|
||||
@@ -886,6 +1035,7 @@ describe("install.ps1 failure handling", () => {
|
||||
const mainBody = extractFunctionBody(source, "Main");
|
||||
|
||||
expect(commandSafeBody).toContain("Get-WindowsCommandSafeDirectory");
|
||||
expect(commandSafeBody).toContain("$WorkingDirectory");
|
||||
expect(commandSafeBody).toContain("Push-Location -LiteralPath $safeDir");
|
||||
expect(commandSafeBody).toContain("& $CommandPath @Arguments");
|
||||
expect(commandSafeBody).toContain("Pop-Location");
|
||||
@@ -896,7 +1046,11 @@ describe("install.ps1 failure handling", () => {
|
||||
'Invoke-CorepackCommand -Arguments @("prepare", $pnpmSpec, "--activate")',
|
||||
);
|
||||
expect(ensurePnpmBody).toContain('Invoke-NpmCommand -Arguments @("install", "-g", $pnpmSpec)');
|
||||
expect(mainBody).toContain('Invoke-NpmCommand -Arguments @("uninstall", "-g", "openclaw")');
|
||||
expect(mainBody).toContain("Remove-PreviousNpmOwner");
|
||||
expect(mainBody).toContain("Remove-PreviousGitWrapper");
|
||||
expect(mainBody).toContain("Start-NpmShimBackup");
|
||||
expect(mainBody).toContain("Restore-NpmShimBackup");
|
||||
expect(mainBody).toContain("Complete-NpmShimBackup");
|
||||
expect(mainBody).toContain(
|
||||
'Invoke-NpmCommand -Arguments @("list", "-g", "--depth", "0", "--json")',
|
||||
);
|
||||
@@ -953,10 +1107,10 @@ describe("install.ps1 failure handling", () => {
|
||||
"} elseif ($minReleaseAgeStatus -ne 0 -or -not $minReleaseAge",
|
||||
);
|
||||
expect(npmInstallBody).toContain(
|
||||
'Invoke-NpmCommand -Arguments @("config", "get", "min-release-age", "--global")',
|
||||
'Invoke-NpmCommand -CommandPath $npmCommand -WorkingDirectory $npmCwd -Arguments @("config", "get", "min-release-age", "--global")',
|
||||
);
|
||||
expect(npmInstallBody).toContain(
|
||||
'Invoke-NpmCommand -Arguments @("config", "get", "before", "--global")',
|
||||
'Invoke-NpmCommand -CommandPath $npmCommand -WorkingDirectory $npmCwd -Arguments @("config", "get", "before", "--global")',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1174,9 +1328,9 @@ describe("install.ps1 failure handling", () => {
|
||||
expect(gitInstallBody).toContain('$entryPath = Join-Path $RepoDir "dist\\\\entry.js"');
|
||||
expect(gitInstallBody).toContain("Test-Path $entryPath");
|
||||
expect(gitInstallBody).toContain('Write-Host "[!] OpenClaw build did not produce $entryPath"');
|
||||
expect(gitInstallBody).toContain('node ""$entryPath"" %*');
|
||||
expect(gitInstallBody).toContain("node $entryPath --version");
|
||||
expect(gitInstallBody).toContain("Format-OpenClawGitWrapper -EntryPath $entryPath");
|
||||
expect(gitInstallBody).not.toContain("& $pnpmCommand -C $RepoDir install");
|
||||
expect(gitInstallBody).not.toContain('node ""$RepoDir\\\\dist\\\\entry.js"" %*');
|
||||
});
|
||||
|
||||
it("cleans legacy git submodules only from the selected git checkout", () => {
|
||||
@@ -1216,6 +1370,11 @@ describe("install.ps1 failure handling", () => {
|
||||
"function Check-Node { return $true }",
|
||||
"function Check-ExistingOpenClaw { return $false }",
|
||||
"function Get-NpmCommandPath { return 'npm.cmd' }",
|
||||
"function Invoke-NpmCommand {",
|
||||
" param([string[]]$Arguments = @(), [string]$CommandPath, [string]$WorkingDirectory)",
|
||||
" if ($Arguments[0] -eq 'config' -and $Arguments[2] -eq 'prefix') { Write-Output $env:USERPROFILE; $global:LASTEXITCODE = 0; return }",
|
||||
" throw 'unexpected npm command'",
|
||||
"}",
|
||||
"function Install-OpenClaw { return $true }",
|
||||
"function Ensure-OpenClawOnPath { return $true }",
|
||||
"function Add-ToUserPath { param([string]$Path) }",
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
writeNpmBeforePolicyFixture,
|
||||
writeNpmFreshnessConflictFixture,
|
||||
writeNpmInstallRetryFixture,
|
||||
writeNpmLifecycleFixture,
|
||||
} from "./install-npm-fixtures.js";
|
||||
|
||||
const SCRIPT_PATH = "scripts/install.sh";
|
||||
@@ -305,7 +306,8 @@ describe("install.sh", () => {
|
||||
node_dir="node-bin"
|
||||
cd "$tmp"
|
||||
mkdir -p "$repo/.git" "$repo/dist" "$node_dir"
|
||||
touch "$repo/dist/entry.js"
|
||||
repo="$(cd "$repo" && pwd -P)"
|
||||
printf 'process.stdout.write("fixture-version\\n");\n' > "$repo/dist/entry.js"
|
||||
cat > "$node_dir/node" <<'NODE'
|
||||
#!/usr/bin/env bash
|
||||
printf 'fake-node:%s\\n' "$*"
|
||||
@@ -348,7 +350,7 @@ NODE
|
||||
PATH="/usr/bin:/bin" "$wrapper" --version
|
||||
`);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.status, JSON.stringify(result)).toBe(0);
|
||||
expect(result.stdout).toContain("exec ");
|
||||
expect(result.stdout).toContain("/node-bin/node");
|
||||
expect(result.stdout).toContain("fake-node:");
|
||||
@@ -470,6 +472,8 @@ NODE
|
||||
replacement="$root/replacement"
|
||||
alias_path="$root/alias"
|
||||
mkdir -p "$target" "$replacement"
|
||||
target="$(cd "$target" && pwd -P)"
|
||||
replacement="$(cd "$replacement" && pwd -P)"
|
||||
ln -s "$target" "$alias_path"
|
||||
|
||||
check_git() { return 0; }
|
||||
@@ -483,7 +487,13 @@ NODE
|
||||
[[ "$1" == "$target" ]]
|
||||
printf '%s\\n' '--frozen-lockfile'
|
||||
}
|
||||
run_pnpm() { [[ "$1" == "-C" && "$2" == "$target" ]]; }
|
||||
run_pnpm() {
|
||||
[[ "$1" == "-C" && "$2" == "$target" ]]
|
||||
if [[ "\${3:-}" == "build" ]]; then
|
||||
mkdir -p "$target/dist"
|
||||
printf '%s\n' 'process.stdout.write("fixture-version\\n");' > "$target/dist/entry.js"
|
||||
fi
|
||||
}
|
||||
run_quiet_step() {
|
||||
shift
|
||||
"$@"
|
||||
@@ -910,9 +920,222 @@ NODE
|
||||
it("clears npm freshness filters for package installs", () => {
|
||||
expect(script).toContain("env -u NPM_CONFIG_BEFORE -u npm_config_before");
|
||||
expect(script).toContain('freshness_flag="--min-release-age=0"');
|
||||
expect(script).toContain('npm_config_has_raw_key npm "min-release-age"');
|
||||
expect(script).toContain('npm_config_has_raw_key "$npm_cmd" "min-release-age"');
|
||||
expect(script).toContain('freshness_flag="--before=$(date -u');
|
||||
expect(script).toContain('cmd+=(--no-fund --no-audit "$freshness_flag" install -g "$spec")');
|
||||
expect(script).toContain('cmd+=(--no-fund --no-audit "$freshness_flag" install -g)');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ expected: false, version: "11.15.0" },
|
||||
{ expected: true, version: "11.16.0" },
|
||||
{ expected: true, version: "12.0.0" },
|
||||
])("applies canonical npm lifecycle policy for npm $version", ({ expected, version }) => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-lifecycle-"));
|
||||
const npm = join(tmp, "npm");
|
||||
const args = join(tmp, "args");
|
||||
const npmRoot = join(tmp, "lib", "node_modules");
|
||||
const packageDir = join(npmRoot, "openclaw");
|
||||
writeNpmLifecycleFixture(npm);
|
||||
try {
|
||||
const result = runInstallShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`npm_command_path() { printf '%s\\n' ${JSON.stringify(npm)}; }`,
|
||||
`run_verified_npm_global_install openclaw@latest ${JSON.stringify(join(tmp, "log"))}`,
|
||||
].join("\n"),
|
||||
{
|
||||
NPM_FAKE_ARGS: args,
|
||||
NPM_FAKE_PACKAGE_DIR: packageDir,
|
||||
NPM_FAKE_ROOT: npmRoot,
|
||||
NPM_FAKE_VERSION: version,
|
||||
},
|
||||
);
|
||||
expect(result.status).toBe(0);
|
||||
expect(readFileSync(args, "utf8").includes("--allow-scripts=openclaw")).toBe(expected);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("fails before npm mutation on invalid versions and rejects a remaining guard", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-lifecycle-fail-"));
|
||||
const npm = join(tmp, "npm");
|
||||
const args = join(tmp, "args");
|
||||
const npmRoot = join(tmp, "lib", "node_modules");
|
||||
writeNpmLifecycleFixture(npm);
|
||||
try {
|
||||
const run = (version: string, keepGuard: string) =>
|
||||
runInstallShell(
|
||||
[
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`npm_command_path() { printf '%s\\n' ${JSON.stringify(npm)}; }`,
|
||||
`run_verified_npm_global_install openclaw@latest ${JSON.stringify(join(tmp, "log"))}`,
|
||||
].join("\n"),
|
||||
{
|
||||
NPM_FAKE_ARGS: args,
|
||||
NPM_FAKE_KEEP_GUARD: keepGuard,
|
||||
NPM_FAKE_PACKAGE_DIR: join(npmRoot, "openclaw"),
|
||||
NPM_FAKE_ROOT: npmRoot,
|
||||
NPM_FAKE_VERSION: version,
|
||||
},
|
||||
);
|
||||
expect(run("invalid", "0").status).not.toBe(0);
|
||||
expect(existsSync(args)).toBe(false);
|
||||
expect(run("12.0.0", "1").status).not.toBe(0);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("relativizes absolute npm path identities against the command cwd", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-lifecycle-comma,"));
|
||||
const npm = join(tmp, "npm");
|
||||
const commandCwd = join(tmp, "work");
|
||||
const candidate = join(tmp, "candidate.tgz");
|
||||
mkdirSync(commandCwd);
|
||||
writeNpmLifecycleFixture(npm);
|
||||
try {
|
||||
const result = runInstallShell(
|
||||
[
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`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.each(["success", "guard-failure"])(
|
||||
"keeps same-bin git-to-npm switching rollback-safe on $mode",
|
||||
(mode) => {
|
||||
const result = runInstallShell(`
|
||||
set -euo pipefail
|
||||
source "${SCRIPT_PATH}"
|
||||
root="$(mktemp -d)"
|
||||
repo="$root/repo"
|
||||
npm_root="$root/lib/node_modules"
|
||||
bin="$HOME/.local/bin"
|
||||
mkdir -p "$repo/dist" "$npm_root/openclaw/dist" "$bin"
|
||||
printf '%s\n' 'process.stdout.write("git-version\\n")' > "$repo/dist/entry.js"
|
||||
cat > "$bin/openclaw" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec ${process.execPath} $repo/dist/entry.js "\\$@"
|
||||
EOF
|
||||
chmod +x "$bin/openclaw"
|
||||
fake_npm="$root/npm"
|
||||
cat > "$fake_npm" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
case "\${1:-}" in
|
||||
--version) printf '12.0.0\n'; exit 0 ;;
|
||||
root) printf '%s\n' "$NPM_FAKE_ROOT"; exit 0 ;;
|
||||
prefix) printf '%s\n' "$NPM_FAKE_PREFIX"; exit 0 ;;
|
||||
config) printf 'null\n'; exit 0 ;;
|
||||
esac
|
||||
mkdir -p "$NPM_FAKE_ROOT/openclaw/dist"
|
||||
printf '%s\n' '#!/usr/bin/env node' 'process.stdout.write("npm-version\\n")' > "$NPM_FAKE_ROOT/openclaw/openclaw.mjs"
|
||||
chmod +x "$NPM_FAKE_ROOT/openclaw/openclaw.mjs"
|
||||
if [[ "$NPM_FAKE_MODE" == guard-failure ]]; then
|
||||
: > "$NPM_FAKE_ROOT/openclaw/dist/openclaw-install-guard"
|
||||
else
|
||||
rm -f "$NPM_FAKE_ROOT/openclaw/dist/openclaw-install-guard"
|
||||
fi
|
||||
EOF
|
||||
chmod +x "$fake_npm"
|
||||
npm() { "$fake_npm" "$@"; }
|
||||
npm_command_path() { printf '%s\n' "$fake_npm"; }
|
||||
npm_global_bin_dir() { printf '%s\n' "$bin"; }
|
||||
GIT_DIR="$repo"
|
||||
OPENCLAW_VERSION="$root/candidate.tgz"
|
||||
export NPM_FAKE_ROOT="$npm_root" NPM_FAKE_PREFIX="$HOME/.local" NPM_FAKE_MODE=${mode}
|
||||
prepare_git_wrapper_backup_for_npm "$GIT_DIR"
|
||||
set +e
|
||||
install_openclaw
|
||||
status=$?
|
||||
set -e
|
||||
cleanup_tmpfiles
|
||||
printf 'status=%s version=%s link=%s\n' "$status" "$("$bin/openclaw" --version)" "$([[ -L "$bin/openclaw" ]] && echo yes || echo no)"
|
||||
`);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
if (mode === "success") {
|
||||
expect(result.stdout, result.stderr).toContain("status=0 version=npm-version link=yes");
|
||||
} else {
|
||||
expect(result.stdout, result.stderr).toContain("status=1 version=git-version link=no");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("restores an active shim backup when installation is interrupted", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-shim-signal-"));
|
||||
const target = join(tmp, "openclaw");
|
||||
writeFileSync(target, "original-wrapper\n", { mode: 0o755 });
|
||||
try {
|
||||
const result = runInstallShell(
|
||||
[
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
'begin_openclaw_bin_backup "$BACKUP_TARGET" "$BACKUP_CANDIDATE" 1',
|
||||
'kill -TERM "$$"',
|
||||
].join("\n"),
|
||||
{ BACKUP_CANDIDATE: join(tmp, "openclaw.mjs"), BACKUP_TARGET: target },
|
||||
);
|
||||
expect(result.status).toBe(143);
|
||||
expect(readFileSync(target, "utf8")).toBe("original-wrapper\n");
|
||||
expect(readdirSync(tmp)).toEqual(["openclaw"]);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("removes only stale npm rename directories before ENOTEMPTY retry", () => {
|
||||
const result = runInstallShell(`
|
||||
set -euo pipefail
|
||||
source "${SCRIPT_PATH}"
|
||||
root="$(mktemp -d)/node_modules"
|
||||
mkdir -p "$root/openclaw" "$root/.openclaw-stale"
|
||||
printf 'live\n' > "$root/openclaw/marker"
|
||||
npm() { [[ "$1" == root ]] && printf '%s\n' "$root"; }
|
||||
run_npm_global_install() {
|
||||
attempts=$((attempts + 1))
|
||||
if (( attempts == 1 )); then printf 'ENOTEMPTY: directory not empty, rename openclaw\n' > "$2"; return 1; fi
|
||||
return 0
|
||||
}
|
||||
auto_install_build_tools_for_npm_failure() { return 1; }
|
||||
attempts=0
|
||||
install_openclaw_npm openclaw@latest
|
||||
[[ -f "$root/openclaw/marker" && ! -e "$root/.openclaw-stale" ]]
|
||||
`);
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
it("does not report npm owner retirement when uninstall fails", () => {
|
||||
const result = runInstallShell(`
|
||||
source "${SCRIPT_PATH}"
|
||||
root="$(mktemp -d)/node_modules"
|
||||
mkdir -p "$root/openclaw"
|
||||
printf '{"name":"openclaw"}\n' > "$root/openclaw/package.json"
|
||||
fake_npm="$root/npm"
|
||||
printf '#!/bin/sh\nif [ "$1" = root ]; then echo "$NPM_ROOT"; exit 0; fi\nexit 9\n' > "$fake_npm"
|
||||
chmod +x "$fake_npm"
|
||||
export NPM_ROOT="$root"
|
||||
npm_command_path() { printf '%s\n' "$fake_npm"; }
|
||||
npm_global_bin_dir() { printf '/different/bin\n'; }
|
||||
set +e
|
||||
retire_npm_owner_after_git_install
|
||||
status=$?
|
||||
set -e
|
||||
printf 'status=%s\n' "$status"
|
||||
`);
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("status=1");
|
||||
expect(result.stdout).not.toContain("Previous npm install retired");
|
||||
});
|
||||
|
||||
it("does not emit --before when raw user npmrc config contains min-release-age", () => {
|
||||
@@ -954,6 +1177,7 @@ NODE
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"npm_lifecycle_allow_arg() { :; }",
|
||||
`run_npm_global_install openclaw@latest ${JSON.stringify(join(tmp, "install.log"))}`,
|
||||
'printf "cmd=%s\\n" "$LAST_NPM_INSTALL_CMD"',
|
||||
].join("\n"),
|
||||
@@ -1022,6 +1246,7 @@ NODE
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"npm_lifecycle_allow_arg() { :; }",
|
||||
`run_npm_global_install openclaw@latest ${JSON.stringify(join(tmp, "install.log"))}`,
|
||||
'printf "cmd=%s\\n" "$LAST_NPM_INSTALL_CMD"',
|
||||
].join("\n"),
|
||||
@@ -1092,6 +1317,7 @@ NODE
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"npm_lifecycle_allow_arg() { :; }",
|
||||
`run_npm_global_install openclaw@latest ${JSON.stringify(join(tmp, "install.log"))}`,
|
||||
'printf "cmd=%s\\n" "$LAST_NPM_INSTALL_CMD"',
|
||||
].join("\n"),
|
||||
@@ -1156,6 +1382,7 @@ NODE
|
||||
source "${SCRIPT_PATH}"
|
||||
repo="$HOME/openclaw"
|
||||
mkdir -p "$repo"
|
||||
repo="$(cd "$repo" && pwd -P)"
|
||||
check_git() { return 0; }
|
||||
ensure_pnpm() { :; }
|
||||
ensure_pnpm_binary_for_scripts() { :; }
|
||||
@@ -1170,6 +1397,9 @@ NODE
|
||||
target="\${*: -1}"
|
||||
mkdir -p "$target/.git"
|
||||
printf 'complete\\n' > "$target/checkout.marker"
|
||||
elif [[ "$1" == "Building OpenClaw" ]]; then
|
||||
mkdir -p "$repo/dist"
|
||||
printf '%s\\n' 'process.stdout.write("fixture-version\\n");' > "$repo/dist/entry.js"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
@@ -1182,7 +1412,7 @@ NODE
|
||||
install_openclaw_from_git "$repo"
|
||||
`);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.status, JSON.stringify(result)).toBe(0);
|
||||
expect(result.stdout).toContain(
|
||||
"step:Cloning OpenClaw|git clone --filter=blob:none https://github.com/openclaw/openclaw.git",
|
||||
);
|
||||
@@ -1427,6 +1657,7 @@ NODE
|
||||
ensure_default_node_active_shell() { return 0; }
|
||||
check_git() { return 0; }
|
||||
fix_npm_permissions() { :; }
|
||||
prepare_git_wrapper_backup_for_npm() { :; }
|
||||
install_openclaw() {
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
printf '#!/bin/sh\\nexit 0\\n' > "$HOME/.local/bin/openclaw"
|
||||
@@ -1456,6 +1687,48 @@ NODE
|
||||
expect(result.stdout).toContain("dashboard-called");
|
||||
});
|
||||
|
||||
it("fails a configured upgrade without printing success when doctor fails", () => {
|
||||
const result = runInstallShell(`
|
||||
source "${SCRIPT_PATH}"
|
||||
INSTALL_METHOD=npm; NO_ONBOARD=1; NO_PROMPT=1; OS=linux
|
||||
bootstrap_gum_temp() { :; }; print_installer_banner() { :; }; print_gum_status() { :; }
|
||||
detect_os_or_die() { OS=linux; }; detect_openclaw_checkout() { return 1; }; show_install_plan() { :; }
|
||||
check_existing_openclaw() { return 0; }; load_nvm_for_node_detection() { :; }; check_node() { return 0; }
|
||||
activate_supported_node_on_path() { :; }; ensure_default_node_active_shell() { return 0; }
|
||||
check_git() { return 0; }; fix_npm_permissions() { :; }
|
||||
prepare_git_wrapper_backup_for_npm() { :; }
|
||||
install_openclaw() { mkdir -p "$HOME/.local/bin"; printf '#!/bin/sh\nif [ "$1" = doctor ]; then exit 9; fi\nexit 0\n' > "$HOME/.local/bin/openclaw"; chmod +x "$HOME/.local/bin/openclaw"; }
|
||||
resolve_installed_openclaw_bin() { printf '%s\n' "$HOME/.local/bin/openclaw"; }
|
||||
warn_duplicate_openclaw_global_installs() { :; }; npm_global_bin_dir() { :; }; warn_shell_path_missing_dir() { :; }
|
||||
has_openclaw_config() { return 0; }; refresh_gateway_service_if_loaded() { :; }
|
||||
run_doctor() { return 9; }; resolve_openclaw_version() { printf 'test-version\n'; }
|
||||
retire_git_wrapper_after_npm_install() { :; }; show_footer_links() { :; }
|
||||
main
|
||||
`);
|
||||
|
||||
expect(result.status).toBe(9);
|
||||
expect(result.stdout).not.toContain("installed successfully");
|
||||
expect(result.stdout).not.toContain("Upgrade complete");
|
||||
});
|
||||
|
||||
it("keeps the npm owner runnable when a npm-to-git candidate fails", () => {
|
||||
const result = runInstallShell(`
|
||||
source "${SCRIPT_PATH}"
|
||||
INSTALL_METHOD=git; GIT_DIR="$HOME/openclaw"; OS=linux
|
||||
mkdir -p "$HOME/npm-owner"; printf 'working\n' > "$HOME/npm-owner/status"
|
||||
bootstrap_gum_temp() { :; }; print_installer_banner() { :; }; print_gum_status() { :; }
|
||||
detect_os_or_die() { OS=linux; }; detect_openclaw_checkout() { return 1; }; show_install_plan() { :; }
|
||||
check_existing_openclaw() { return 0; }; load_nvm_for_node_detection() { :; }; check_node() { return 0; }
|
||||
activate_supported_node_on_path() { :; }; ensure_default_node_active_shell() { return 0; }
|
||||
npm() { if [[ "$1" == list ]]; then return 0; fi; if [[ "$1" == uninstall ]]; then printf 'old-owner-removed\n'; rm -f "$HOME/npm-owner/status"; fi; }
|
||||
install_openclaw_from_git() { return 7; }
|
||||
main
|
||||
`);
|
||||
|
||||
expect(result.status).toBe(7);
|
||||
expect(result.stdout).not.toContain("old-owner-removed");
|
||||
});
|
||||
|
||||
it("rejects OpenClaw GitHub source targets for npm installs", () => {
|
||||
const result = runInstallShell(`
|
||||
set -euo pipefail
|
||||
@@ -1553,7 +1826,7 @@ NODE
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
expect(result.status).toBe(7);
|
||||
expect(result.status).not.toBe(0);
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
@@ -3435,7 +3708,7 @@ describe("install.sh doctor cancellation and dashboard guard", () => {
|
||||
|
||||
it("guards every run_doctor caller against failure", () => {
|
||||
// A failed or cancelled doctor must not launch the dashboard.
|
||||
expect(script).toContain("if run_doctor; then");
|
||||
expect(script).toContain("run_doctor || return $?");
|
||||
// Ensure there is no bare "run_doctor" call followed by
|
||||
// "should_open_dashboard=true" without an if-guard
|
||||
const bareDoctor = /^\s+run_doctor\s*$/m;
|
||||
|
||||
@@ -646,6 +646,7 @@ function runPackageAcceptanceSummary(params: {
|
||||
advisory?: boolean;
|
||||
dockerArtifactResult?: string;
|
||||
dockerRegistryResult?: string;
|
||||
npm12InstallResult?: string;
|
||||
telegramAdvisory?: boolean;
|
||||
telegramEnabled: boolean;
|
||||
telegramResult: string;
|
||||
@@ -662,6 +663,7 @@ function runPackageAcceptanceSummary(params: {
|
||||
DOCKER_ARTIFACT_RESULT: params.dockerArtifactResult ?? "success",
|
||||
DOCKER_REGISTRY_RESULT: params.dockerRegistryResult ?? "skipped",
|
||||
PACKAGE_INTEGRITY_RESULT: "success",
|
||||
NPM_12_INSTALL_RESULT: params.npm12InstallResult ?? "success",
|
||||
PACKAGE_TELEGRAM_RESULT: params.telegramResult,
|
||||
PATH: process.env.PATH,
|
||||
RESOLVE_RESULT: "success",
|
||||
@@ -1908,6 +1910,16 @@ describe("package acceptance workflow", () => {
|
||||
expect(workflow).toContain('[[ "$actual_sha256" == "$EXPECTED_PACKAGE_SHA256" ]]');
|
||||
expect(workflow).toContain("needs: [resolve_package, package_integrity]");
|
||||
expect(workflow).toContain("package_integrity=${PACKAGE_INTEGRITY_RESULT}");
|
||||
const npm12Job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "npm_12_install_sh");
|
||||
expect(jobNeeds(npm12Job)).toEqual(["resolve_package", "package_integrity"]);
|
||||
expect(npm12Job.permissions).toEqual({ actions: "read", contents: "read" });
|
||||
const npm12Step = workflowStep(npm12Job, "Run install.sh with npm 12");
|
||||
expect(npm12Step.run).toContain("npm@12.0.2");
|
||||
expect(npm12Step.run).toContain("bash scripts/install.sh");
|
||||
expect(npm12Step.run).toContain("scripts/docker/install-sh-common/version-parse.sh");
|
||||
expect(npm12Step.run).toContain("extract_openclaw_semver");
|
||||
expect(npm12Step.run).toContain("openclaw-install-guard");
|
||||
expect(JSON.stringify(npm12Job)).not.toContain("secrets.");
|
||||
});
|
||||
|
||||
it("keeps ref packaging independent of workflow-checkout dependencies", () => {
|
||||
@@ -4720,6 +4732,16 @@ describe("package artifact reuse", () => {
|
||||
telegramResult: "skipped",
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedOutput: "::error::npm_12_install_sh ended with failure",
|
||||
expectedStatus: 1,
|
||||
name: "rejects a failed npm 12 installer acceptance lane",
|
||||
params: {
|
||||
npm12InstallResult: "failure",
|
||||
telegramEnabled: false,
|
||||
telegramResult: "skipped",
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedOutput:
|
||||
"::warning::package_telegram ended with skipped; package acceptance is advisory for this caller.",
|
||||
@@ -6576,6 +6598,9 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
||||
expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "summary"))).toContain(
|
||||
"docker_acceptance",
|
||||
);
|
||||
expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "summary"))).toContain(
|
||||
"npm_12_install_sh",
|
||||
);
|
||||
expect(jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"))).toContain(
|
||||
"package_acceptance_release_checks",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user