fix(update): reject openclaw source package targets

This commit is contained in:
Vincent Koc
2026-05-22 07:20:49 +08:00
parent fad1c8a071
commit 15a0156a8c
19 changed files with 254 additions and 36 deletions

View File

@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
- Agents/subagents: surface blocked child-run completions as errors instead of successful subagent finishes. (#80886) Thanks @TurboTheTurtle.
- WhatsApp: update Baileys to `7.0.0-rc13` and drop the obsolete logger type patch.
- Install/update: reject OpenClaw GitHub source package targets early and point moving-main users at the dev/git install path instead of the broken npm source-install flow.
- Infra/json: retry transient `File changed during read` races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)
- Providers/Ollama: resolve configured Ollama Cloud `OLLAMA_API_KEY` markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)
- Discord: keep persistent component registry fallback warnings actionable by forwarding structured error and cause metadata through the runtime logger. Fixes #84185. (#84190) Thanks @100menotu001.

View File

@@ -23,7 +23,6 @@ openclaw update wizard
openclaw update --channel beta
openclaw update --channel dev
openclaw update --tag beta
openclaw update --tag main
openclaw update --dry-run
openclaw update --no-restart
openclaw update --yes
@@ -35,7 +34,7 @@ openclaw --update
- `--no-restart`: skip restarting the Gateway service after a successful update. Package-manager updates that do restart the Gateway verify the restarted service reports the expected updated version before the command succeeds.
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
- `--tag <dist-tag|version|spec>`: override the package target for this update only. For package installs, `main` maps to `github:openclaw/openclaw#main`.
- `--tag <dist-tag|version|spec>`: override the package target for this update only. Use `--channel dev`, not `--tag main`, for the moving GitHub `main` checkout.
- `--dry-run`: preview planned update actions (channel/tag/target/restart flow) without writing config, installing, syncing plugins, or restarting.
- `--json`: print machine-readable `UpdateRunResult` JSON, including
`postUpdate.plugins.warnings` when corrupt or unloadable managed plugins need

View File

@@ -60,8 +60,8 @@ openclaw update --tag 2026.4.1-beta.1
# Install from the beta dist-tag (one-off, does not persist)
openclaw update --tag beta
# Install from GitHub main branch (npm tarball)
openclaw update --tag main
# Switch to the moving GitHub main checkout
openclaw update --channel dev
# Install a specific npm package spec
openclaw update --tag openclaw@2026.4.1-beta.1
@@ -72,6 +72,9 @@ Notes:
- `--tag` applies to **package (npm) installs only**. Git installs ignore it.
- The tag is not persisted. Your next `openclaw update` uses your configured
channel as usual.
- OpenClaw does not support npm GitHub source installs for `openclaw/openclaw`.
Use `--channel dev` or `--install-method git --version main` for the moving
`main` checkout.
- Downgrade protection: if the target version is older than your current version,
OpenClaw prompts for confirmation (skip with `--yes`).
- `--channel beta` is different from `--tag beta`: the channel flow can fall back

View File

@@ -131,10 +131,10 @@ openclaw onboard --install-daemon
Or skip the link and use `pnpm openclaw ...` from inside the repo. See [Setup](/start/setup) for full development workflows.
### Install from GitHub main
### Install from the GitHub main checkout
```bash
npm install -g github:openclaw/openclaw#main
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --version main
```
### Containers and package managers

View File

@@ -119,9 +119,9 @@ The script exits with code `2` for invalid method selection or invalid `--instal
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git
```
</Tab>
<Tab title="GitHub main via npm">
<Tab title="GitHub main checkout">
```bash
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --version main
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --version main
```
</Tab>
<Tab title="Dry run">
@@ -157,7 +157,7 @@ The script exits with code `2` for invalid method selection or invalid `--instal
| Variable | Description |
| ------------------------------------------------------- | --------------------------------------------- |
| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method |
| `OPENCLAW_VERSION=latest\|next\|main\|<semver>\|<spec>` | npm version, dist-tag, or package spec |
| `OPENCLAW_VERSION=latest\|next\|<semver>\|<spec>` | npm version, dist-tag, or package spec |
| `OPENCLAW_BETA=0\|1` | Use beta if available |
| `OPENCLAW_GIT_DIR=<path>` | Checkout directory |
| `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates |
@@ -315,9 +315,9 @@ by default, plus git-checkout installs under the same prefix flow.
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git
```
</Tab>
<Tab title="GitHub main via npm">
<Tab title="GitHub main checkout">
```powershell
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -Tag main
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git -Tag main
```
</Tab>
<Tab title="Custom git directory">

View File

@@ -21,7 +21,6 @@ To switch channels or target a specific version:
```bash
openclaw update --channel beta
openclaw update --channel dev
openclaw update --tag main
openclaw update --dry-run # preview without applying
```
@@ -35,6 +34,10 @@ installer has its own `--verbose` flag, but that flag is not part of
the beta tag is missing or older than the latest stable release. Use `--tag beta`
if you want the raw npm beta dist-tag for a one-off package update.
Use `--channel dev` for the moving GitHub `main` checkout. Package updates do
not support npm GitHub source installs for `openclaw/openclaw`; target a
published dist-tag, exact version, or built tarball instead.
For managed plugins, beta-channel fallback is a warning: the core update can
still succeed while a plugin uses its recorded default/latest release because no
plugin beta is available.

View File

@@ -364,6 +364,27 @@ run_pnpm() {
"${PNPM_CMD[@]}" "$@"
}
to_lowercase_ascii() {
printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]'
}
is_openclaw_source_package_install_spec() {
local value="${1:-}"
local normalized_value=""
normalized_value="$(to_lowercase_ascii "$value")"
normalized_value="${normalized_value#openclaw@}"
[[ "$normalized_value" == "main" ]] && return 0
[[ "$normalized_value" =~ ^github:openclaw/openclaw($|[#/]) ]] && return 0
normalized_value="${normalized_value#git+}"
[[ "$normalized_value" =~ ^https?://github\.com/openclaw/openclaw(\.git)?($|[?#]) ]] && return 0
[[ "$normalized_value" =~ ^ssh://git@github\.com[:/]openclaw/openclaw(\.git)?($|[?#]) ]] && return 0
[[ "$normalized_value" =~ ^git://github\.com/openclaw/openclaw(\.git)?($|[?#]) ]] && return 0
[[ "$normalized_value" =~ ^git@github\.com:openclaw/openclaw(\.git)?($|[?#]) ]] && return 0
return 1
}
resolve_git_openclaw_ref() {
local requested="${OPENCLAW_VERSION:-latest}"
local resolved_version=""
@@ -624,6 +645,9 @@ fix_npm_prefix_if_needed() {
install_openclaw() {
local requested="${OPENCLAW_VERSION:-latest}"
if is_openclaw_source_package_install_spec "$requested"; then
fail "npm installs do not support OpenClaw GitHub source targets like '${requested}'. Use --install-method git --version main, latest, beta, an exact version, or a built .tgz package."
fi
local freshness_flag="--min-release-age=0"
local min_release_age=""
min_release_age="$(env -u NPM_CONFIG_BEFORE -u npm_config_before "$(npm_bin)" config get min-release-age 2>/dev/null || true)"

View File

@@ -511,10 +511,45 @@ function Resolve-NpmOpenClawInstallSpec {
return "$PackageName@$trimmedTag"
}
function Test-OpenClawSourcePackageInstallSpec {
param([string]$RequestedTag)
if ([string]::IsNullOrWhiteSpace($RequestedTag)) {
return $false
}
$normalizedTag = $RequestedTag.Trim().ToLowerInvariant()
if ($normalizedTag.StartsWith("openclaw@")) {
$normalizedTag = $normalizedTag.Substring("openclaw@".Length)
}
if ($normalizedTag -eq "main") {
return $true
}
if ($normalizedTag -match '^github:openclaw/openclaw($|[#/])') {
return $true
}
if ($normalizedTag.StartsWith("git+")) {
$normalizedTag = $normalizedTag.Substring("git+".Length)
}
return (
$normalizedTag -match '^https?://github\.com/openclaw/openclaw(\.git)?($|[?#])' -or
$normalizedTag -match '^ssh://git@github\.com[:/]openclaw/openclaw(\.git)?($|[?#])' -or
$normalizedTag -match '^git://github\.com/openclaw/openclaw(\.git)?($|[?#])' -or
$normalizedTag -match '^git@github\.com:openclaw/openclaw(\.git)?($|[?#])'
)
}
function Install-OpenClaw {
if ([string]::IsNullOrWhiteSpace($Tag)) {
$Tag = "latest"
}
if (Test-OpenClawSourcePackageInstallSpec -RequestedTag $Tag) {
Write-Host "Error: npm installs do not support OpenClaw GitHub source targets like '$Tag'." -ForegroundColor Red
Write-Host "Use -InstallMethod git -Tag main for the moving main checkout, or use latest, beta, an exact version, or a built .tgz package." -ForegroundColor Yellow
return $false
}
if (-not (Ensure-Git)) {
return $false
}

View File

@@ -1047,7 +1047,7 @@ Options:
--install-method, --method npm|git Install via npm (default) or from a git checkout
--npm Shortcut for --install-method npm
--git, --github Shortcut for --install-method git
--version <version|dist-tag|spec> npm install target (default: latest; use "main" for GitHub main)
--version <version|dist-tag|spec> npm install target (default: latest)
--beta Use beta if available, else latest
--git-dir, --dir <path> Checkout directory (default: ~/openclaw)
--no-git-update Skip git pull for existing checkout
@@ -1060,7 +1060,7 @@ Options:
Environment variables:
OPENCLAW_INSTALL_METHOD=git|npm
OPENCLAW_VERSION=latest|next|main|<semver>|<spec>
OPENCLAW_VERSION=latest|next|<semver>|<spec>
OPENCLAW_BETA=0|1
OPENCLAW_GIT_DIR=...
OPENCLAW_GIT_UPDATE=0|1
@@ -1076,7 +1076,7 @@ Examples:
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard --verify
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --version main
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --version main
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git --no-onboard
EOF
}
@@ -2444,6 +2444,23 @@ is_explicit_package_install_spec() {
[[ "$value" == *"://"* || "$value" == *"#"* || "$value" =~ ^(file|github|git\+ssh|git\+https|git\+http|git\+file|npm): ]]
}
is_openclaw_source_package_install_spec() {
local value="${1:-}"
local normalized_value=""
normalized_value="$(to_lowercase_ascii "$value")"
normalized_value="${normalized_value#openclaw@}"
[[ "$normalized_value" == "main" ]] && return 0
[[ "$normalized_value" =~ ^github:openclaw/openclaw($|[#/]) ]] && return 0
normalized_value="${normalized_value#git+}"
[[ "$normalized_value" =~ ^https?://github\.com/openclaw/openclaw(\.git)?($|[?#]) ]] && return 0
[[ "$normalized_value" =~ ^ssh://git@github\.com[:/]openclaw/openclaw(\.git)?($|[?#]) ]] && return 0
[[ "$normalized_value" =~ ^git://github\.com/openclaw/openclaw(\.git)?($|[?#]) ]] && return 0
[[ "$normalized_value" =~ ^git@github\.com:openclaw/openclaw(\.git)?($|[?#]) ]] && return 0
return 1
}
can_resolve_registry_package_version() {
local value="${1:-}"
local normalized_value=""
@@ -2499,6 +2516,12 @@ install_openclaw() {
OPENCLAW_VERSION="latest"
fi
if is_openclaw_source_package_install_spec "${OPENCLAW_VERSION}"; then
ui_error "npm installs do not support OpenClaw GitHub source targets like '${OPENCLAW_VERSION}'."
ui_info "Use --install-method git --version main for the moving main checkout, or use latest, beta, an exact version, or a built .tgz package."
return 1
fi
local resolved_version=""
if can_resolve_registry_package_version "${OPENCLAW_VERSION}"; then
resolved_version="$(npm view "${package_name}@${OPENCLAW_VERSION}" version 2>/dev/null || true)"

View File

@@ -2074,22 +2074,6 @@ describe("update-cli", () => {
},
expectedSpec: "openclaw@next",
},
{
name: "main shorthand",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ yes: true, tag: "main" });
},
expectedSpec: "github:openclaw/openclaw#main",
},
{
name: "explicit git package spec",
run: async () => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ yes: true, tag: "github:openclaw/openclaw#main" });
},
expectedSpec: "github:openclaw/openclaw#main",
},
{
name: "OPENCLAW_UPDATE_PACKAGE_SPEC override",
run: async () => {
@@ -2116,6 +2100,22 @@ describe("update-cli", () => {
},
);
it.each(["main", "github:openclaw/openclaw#main", "openclaw@github:openclaw/openclaw#main"])(
"rejects OpenClaw GitHub source package updates: %s",
async (tag) => {
mockPackageInstallStatus(createCaseDir("openclaw-update"));
await updateCommand({ yes: true, tag });
expect(packageInstallCommandCall()).toBeUndefined();
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
const errors = vi.mocked(defaultRuntime.error).mock.calls.map((call) => String(call[0]));
expect(errors.join("\n")).toContain("Unsupported package update target");
expect(errors.join("\n")).toContain("openclaw/openclaw");
expect(errors.join("\n")).toContain("openclaw update --channel dev");
},
);
it("fails package updates when the installed correction version does not match the requested target", async () => {
const tempDir = createCaseDir("openclaw-update");
const nodeModules = path.join(tempDir, "node_modules");

View File

@@ -58,7 +58,6 @@ export function registerUpdateCli(program: Command) {
["openclaw update --channel beta", "Switch to beta channel (git + npm)"],
["openclaw update --channel dev", "Switch to dev channel (git + npm)"],
["openclaw update --tag beta", "One-off update to a dist-tag or version"],
["openclaw update --tag main", "One-off package install from GitHub main"],
["openclaw update --dry-run", "Preview actions without changing anything"],
["openclaw update --no-restart", "Update without restarting the service"],
["openclaw update --json", "Output result as JSON"],
@@ -78,6 +77,7 @@ ${theme.heading("Switch channels:")}
- Use --channel stable|beta|dev to persist the update channel in config
- Run openclaw update status to see the active channel and source
- Use --tag <dist-tag|version|spec> for a one-off package update without persisting
- Use --channel dev, not --tag main, for the moving GitHub main checkout
${theme.heading("Non-interactive:")}
- Use --yes to accept downgrade prompts

View File

@@ -69,6 +69,7 @@ import {
createGlobalInstallEnv,
cleanupGlobalRenameDirs,
globalInstallArgs,
isOpenClawSourcePackageInstallSpec,
resolveGlobalInstallTarget,
resolveGlobalInstallSpec,
resolvePnpmGlobalDirFromGlobalRoot,
@@ -976,6 +977,14 @@ async function resolvePackageRuntimePreflightError(params: {
].join("\n");
}
function formatUnsupportedOpenClawSourcePackageTargetMessage(target: string): string {
return [
`Unsupported package update target: ${target}.`,
"OpenClaw package updates use published npm artifacts or built tarballs; npm GitHub source installs for openclaw/openclaw do not reliably produce an installable package.",
"Use `openclaw update --channel dev` for the moving main checkout, or target `latest`, `beta`, an exact version, or a built `.tgz` package spec.",
].join("\n");
}
async function resolvePackageRuntimeForPreflight(params: {
nodeRunner?: string;
timeoutMs?: number;
@@ -3080,6 +3089,11 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
tag,
env: process.env,
});
if (isOpenClawSourcePackageInstallSpec(packageInstallSpec)) {
defaultRuntime.error(formatUnsupportedOpenClawSourcePackageTargetMessage(packageInstallSpec));
defaultRuntime.exit(1);
return;
}
}
if (opts.dryRun) {

View File

@@ -26,6 +26,7 @@ import {
globalInstallFallbackArgs,
isExplicitPackageInstallSpec,
isMainPackageTarget,
isOpenClawSourcePackageInstallSpec,
OPENCLAW_MAIN_PACKAGE_SPEC,
resolveGlobalInstallCommand,
resolveGlobalPackageRoot,
@@ -249,6 +250,30 @@ describe("update global helpers", () => {
expect(canResolveRegistryVersionForPackageTarget("/tmp/openclaw-main.tgz")).toBe(false);
});
it("classifies OpenClaw GitHub source package specs as unsupported package targets", () => {
expect(isOpenClawSourcePackageInstallSpec("main")).toBe(true);
expect(isOpenClawSourcePackageInstallSpec("github:openclaw/openclaw#main")).toBe(true);
expect(isOpenClawSourcePackageInstallSpec("openclaw@github:openclaw/openclaw#main")).toBe(
true,
);
expect(isOpenClawSourcePackageInstallSpec("OpenClaw@github:openclaw/openclaw#main")).toBe(
true,
);
expect(
isOpenClawSourcePackageInstallSpec("git+https://github.com/openclaw/openclaw.git#main"),
).toBe(true);
expect(isOpenClawSourcePackageInstallSpec("https://example.com/openclaw-main.tgz")).toBe(
false,
);
expect(
isOpenClawSourcePackageInstallSpec(
"https://github.com/openclaw/openclaw/releases/download/v2026.5.20/openclaw.tgz",
),
).toBe(false);
expect(isOpenClawSourcePackageInstallSpec("github:other/openclaw#main")).toBe(false);
expect(isOpenClawSourcePackageInstallSpec("beta")).toBe(false);
});
it("detects install managers from resolved roots and on-disk presence", async () => {
await withTempDir({ prefix: "openclaw-update-global-" }, async (base) => {
const npmRoot = path.join(base, "npm-root");

View File

@@ -89,7 +89,30 @@ export function isExplicitPackageInstallSpec(value: string): boolean {
function stripPrimaryPackageAlias(spec: string): string {
const normalized = normalizePackageTarget(spec);
const prefix = `${PRIMARY_PACKAGE_NAME}@`;
return normalized.startsWith(prefix) ? normalized.slice(prefix.length).trim() : normalized;
return normalized.toLowerCase().startsWith(prefix)
? normalized.slice(prefix.length).trim()
: normalized;
}
export function isOpenClawSourcePackageInstallSpec(value: string): boolean {
if (isMainPackageTarget(value)) {
return true;
}
const target = stripPrimaryPackageAlias(value);
const normalizedTarget = normalizeLowercaseStringOrEmpty(target);
if (!normalizedTarget) {
return false;
}
if (/^github:openclaw\/openclaw(?:$|[#/])/u.test(normalizedTarget)) {
return true;
}
const gitUrl = normalizedTarget.replace(/^git\+/u, "");
return (
/^https?:\/\/github\.com\/openclaw\/openclaw(?:\.git)?(?:$|[?#])/u.test(gitUrl) ||
/^ssh:\/\/git@github\.com[:/]openclaw\/openclaw(?:\.git)?(?:$|[?#])/u.test(gitUrl) ||
/^git:\/\/github\.com\/openclaw\/openclaw(?:\.git)?(?:$|[?#])/u.test(gitUrl) ||
/^git@github\.com:openclaw\/openclaw(?:\.git)?(?:$|[?#])/u.test(gitUrl)
);
}
function isPnpmOpenClawSourceInstallSpec(spec: string): boolean {

View File

@@ -1742,15 +1742,17 @@ describe("runGatewayUpdate", () => {
expect(calls).toContain(expectedInstallCommand);
});
it("updates global npm installs from the GitHub main package spec", async () => {
it("rejects global npm updates from the GitHub main package spec", async () => {
const { calls, result } = await runNpmGlobalUpdateCase({
expectedInstallCommand: npmGlobalInstallCommand("github:openclaw/openclaw#main"),
tag: "main",
});
expect(result.status).toBe("ok");
expect(result.status).toBe("error");
expect(result.mode).toBe("npm");
expect(calls).toContain(npmGlobalInstallCommand("github:openclaw/openclaw#main"));
expect(result.reason).toBe("unsupported-package-target");
expect(result.steps[0]?.name).toBe("package target validation");
expect(calls).not.toContain(npmGlobalInstallCommand("github:openclaw/openclaw#main"));
});
it("runs doctor after global npm updates before reporting success", async () => {

View File

@@ -25,6 +25,7 @@ import {
cleanupGlobalRenameDirs,
createGlobalInstallEnv,
detectGlobalInstallManagerForRoot,
isOpenClawSourcePackageInstallSpec,
resolveGlobalInstallTarget,
resolveGlobalInstallSpec,
type GlobalInstallManager,
@@ -1453,6 +1454,30 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
tag,
env: globalInstallEnv,
});
if (isOpenClawSourcePackageInstallSpec(spec)) {
const durationMs = Date.now() - startedAt;
return {
status: "error",
mode: globalManager,
root: pkgRoot,
reason: "unsupported-package-target",
before: { version: beforeVersion },
after: { version: beforeVersion },
steps: [
{
name: "package target validation",
command: `validate package target ${spec}`,
cwd: pkgRoot,
durationMs,
exitCode: 1,
stdoutTail: null,
stderrTail:
"OpenClaw package updates use published npm artifacts or built tarballs; use the dev channel for GitHub main.",
},
],
durationMs,
};
}
const packageUpdate = await runGlobalPackageUpdateSteps({
installTarget,
installSpec: spec,

View File

@@ -96,4 +96,17 @@ describe("install-cli.sh", () => {
expect(script).toContain('freshness_flag="--before=$(date -u');
expect(script).toContain("env -u NPM_CONFIG_BEFORE -u npm_config_before");
});
it("rejects OpenClaw GitHub source targets for npm installs", () => {
const result = runInstallCliShell(`
set -euo pipefail
source "${SCRIPT_PATH}"
OPENCLAW_VERSION=main
install_openclaw
`);
expect(result.status).toBe(1);
expect(result.stdout).toContain("npm installs do not support OpenClaw GitHub source targets");
expect(result.stdout).toContain("--install-method git --version main");
});
});

View File

@@ -104,6 +104,16 @@ describe("install.ps1 failure handling", () => {
);
});
it("rejects OpenClaw GitHub source targets for npm installs", () => {
const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw");
const sourceTargetBody = extractFunctionBody(source, "Test-OpenClawSourcePackageInstallSpec");
expect(sourceTargetBody).toContain('$normalizedTag -eq "main"');
expect(sourceTargetBody).toContain("^github:openclaw/openclaw");
expect(npmInstallBody).toContain("Test-OpenClawSourcePackageInstallSpec -RequestedTag $Tag");
expect(npmInstallBody).toContain("npm installs do not support OpenClaw GitHub source targets");
expect(npmInstallBody).toContain("-InstallMethod git -Tag main");
});
it("cleans legacy git submodules only from the selected git checkout", () => {
const gitInstallBody = extractFunctionBody(source, "Install-OpenClawFromGit");
const mainBody = extractFunctionBody(source, "Main");

View File

@@ -41,6 +41,24 @@ describe("install.sh", () => {
expect(script).toContain('cmd+=(--no-fund --no-audit "$freshness_flag" install -g "$spec")');
});
it("rejects OpenClaw GitHub source targets for npm installs", () => {
const result = runInstallShell(`
set -euo pipefail
source "${SCRIPT_PATH}"
set +e
OPENCLAW_VERSION=main
USE_BETA=0
install_openclaw
status=$?
printf 'status=%s\\n' "$status"
`);
expect(result.status).toBe(0);
expect(result.stdout).toContain("status=1");
expect(result.stdout).toContain("npm installs do not support OpenClaw GitHub source targets");
expect(result.stdout).toContain("--install-method git --version main");
});
it("exports noninteractive apt env during Linux startup", () => {
expect(script).toMatch(
/detect_os_or_die\s+if \[\[ "\$OS" == "linux" \]\]; then\s+export DEBIAN_FRONTEND="\$\{DEBIAN_FRONTEND:-noninteractive\}"\s+export NEEDRESTART_MODE="\$\{NEEDRESTART_MODE:-a\}"\s+fi/m,