mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 00:52:10 -06:00
fix(install): keep stable channel when npm install retries (#124778)
* fix(installer): keep npm channel target immutable Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae * fix(install): fail after repeated CLI package install errors Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae * fix(installer): verify npm package publication Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae * fix(install): satisfy shellcheck for entry validation * fix(installer): link the packaged OpenClaw launcher Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae * test(installer): keep retry fixture version-valid Amp-Thread-ID: https://ampcode.com/threads/T-01a00ae0-190d-718b-8a76-b75f3e8d1fae --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
committed by
GitHub
parent
1027837d96
commit
2b2c11e748
@@ -19,6 +19,7 @@ import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
import {
|
||||
writeNpmBeforePolicyFixture,
|
||||
writeNpmFreshnessConflictFixture,
|
||||
writeNpmInstallRetryFixture,
|
||||
} from "./install-npm-fixtures.js";
|
||||
|
||||
const SCRIPT_PATH = "scripts/install-cli.sh";
|
||||
@@ -41,6 +42,12 @@ function linkRequiredShellTools(bin: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function writeInstalledOpenClawEntry(nodeDir: string) {
|
||||
const entry = join(nodeDir, "lib", "node_modules", "openclaw", "dist", "entry.js");
|
||||
mkdirSync(join(entry, ".."), { recursive: true });
|
||||
writeFileSync(entry, "");
|
||||
}
|
||||
|
||||
describe("install-cli.sh", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
@@ -494,7 +501,7 @@ describe("install-cli.sh", () => {
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(process.cwd())}`,
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"install_node() { :; }",
|
||||
'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"; }',
|
||||
`refresh_gateway_service_if_loaded() { touch ${JSON.stringify(refreshLog)}; }`,
|
||||
@@ -1442,6 +1449,7 @@ describe("install-cli.sh", () => {
|
||||
const nodeDir = join(tmp, "node");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
mkdirSync(nodeDir, { recursive: true });
|
||||
writeInstalledOpenClawEntry(nodeDir);
|
||||
writeFileSync(npmrc, "min-release-age=7\n");
|
||||
const fakeNpm = join(bin, "npm");
|
||||
writeFileSync(
|
||||
@@ -1517,6 +1525,7 @@ describe("install-cli.sh", () => {
|
||||
mkdirSync(bin, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
mkdirSync(nodeDir, { recursive: true });
|
||||
writeInstalledOpenClawEntry(nodeDir);
|
||||
if (source === "global") {
|
||||
mkdirSync(join(prefix, "etc"), { recursive: true });
|
||||
}
|
||||
@@ -1598,6 +1607,137 @@ describe("install-cli.sh", () => {
|
||||
expect(result.stdout).toContain("--install-method git --version main");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ requested: "latest", outcome: "success", error: "", calls: 1, status: 0 },
|
||||
{
|
||||
requested: "beta",
|
||||
outcome: "transient",
|
||||
error: "ECONNRESET socket hang up",
|
||||
calls: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
requested: "next",
|
||||
outcome: "transient",
|
||||
error: "ECONNRESET socket hang up",
|
||||
calls: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
requested: "2026.8.1",
|
||||
outcome: "transient",
|
||||
error: "ECONNRESET socket hang up",
|
||||
calls: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
requested: "latest",
|
||||
outcome: "persistent",
|
||||
error: "EACCES permission denied",
|
||||
calls: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
requested: "beta",
|
||||
outcome: "persistent",
|
||||
error: "ENOSPC no space left",
|
||||
calls: 2,
|
||||
status: 1,
|
||||
},
|
||||
])(
|
||||
"keeps openclaw@$requested immutable across $outcome npm installs",
|
||||
({ requested, outcome, error, calls: expectedCalls, status }) => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-npm-retry-"));
|
||||
const fakeNpm = join(tmp, "npm");
|
||||
const calls = join(tmp, "calls");
|
||||
const nodeDir = join(tmp, "node");
|
||||
const prefix = join(tmp, "prefix");
|
||||
writeNpmInstallRetryFixture(fakeNpm);
|
||||
|
||||
try {
|
||||
const result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`npm_bin() { printf '%s\\n' ${JSON.stringify(fakeNpm)}; }`,
|
||||
`node_dir() { printf '%s\\n' ${JSON.stringify(nodeDir)}; }`,
|
||||
"npm_config_has_raw_key() { return 1; }",
|
||||
`PREFIX=${JSON.stringify(prefix)}`,
|
||||
`OPENCLAW_VERSION=${requested}`,
|
||||
"JSON=1",
|
||||
"set +e",
|
||||
"install_openclaw",
|
||||
"status=$?",
|
||||
'exit "$status"',
|
||||
].join("\n"),
|
||||
{
|
||||
NPM_FAKE_CALLS: calls,
|
||||
NPM_FAKE_ERROR: error,
|
||||
NPM_FAKE_OUTCOME: outcome,
|
||||
NPM_FAKE_PACKAGE_DIR: join(nodeDir, "lib", "node_modules", "openclaw"),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(status);
|
||||
expect(readFileSync(calls, "utf8").trim().split("\n")).toEqual(
|
||||
Array.from({ length: expectedCalls }, () => `openclaw@${requested}`),
|
||||
);
|
||||
if (status !== 0) {
|
||||
expect(result.stderr).toContain(`${error} (attempt 2)`);
|
||||
expect(result.stdout).not.toContain('"status":"ok"');
|
||||
expect(existsSync(join(prefix, "bin", "openclaw"))).toBe(false);
|
||||
}
|
||||
if (requested !== "next") {
|
||||
expect(`${result.stdout}\n${result.stderr}`).not.toContain("openclaw@next");
|
||||
}
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("fails after retrying the exact npm spec when npm exits zero without installing OpenClaw", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-empty-success-"));
|
||||
const fakeNpm = join(tmp, "npm");
|
||||
const calls = join(tmp, "calls");
|
||||
const nodeDir = join(tmp, "node");
|
||||
const prefix = join(tmp, "prefix");
|
||||
writeNpmInstallRetryFixture(fakeNpm);
|
||||
|
||||
try {
|
||||
const result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`npm_bin() { printf '%s\\n' ${JSON.stringify(fakeNpm)}; }`,
|
||||
`node_dir() { printf '%s\\n' ${JSON.stringify(nodeDir)}; }`,
|
||||
"npm_config_has_raw_key() { return 1; }",
|
||||
`PREFIX=${JSON.stringify(prefix)}`,
|
||||
"OPENCLAW_VERSION=latest",
|
||||
"JSON=1",
|
||||
"install_openclaw",
|
||||
].join("\n"),
|
||||
{
|
||||
NPM_FAKE_CALLS: calls,
|
||||
NPM_FAKE_ERROR: "",
|
||||
NPM_FAKE_OUTCOME: "success",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(readFileSync(calls, "utf8").trim().split("\n")).toEqual([
|
||||
"openclaw@latest",
|
||||
"openclaw@latest",
|
||||
]);
|
||||
expect(result.stdout).toContain("npm install did not produce a usable OpenClaw package");
|
||||
expect(result.stdout).not.toContain('"status":"ok"');
|
||||
expect(result.stdout).not.toContain("openclaw@next");
|
||||
expect(existsSync(join(prefix, "bin", "openclaw"))).toBe(false);
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not emit before args when npmrc min-release-age computes a before cutoff", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-freshness-"));
|
||||
const prefix = join(tmp, "prefix");
|
||||
@@ -1606,6 +1746,7 @@ describe("install-cli.sh", () => {
|
||||
const argsLog = join(tmp, "npm-args.log");
|
||||
mkdirSync(nodeBin, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
writeInstalledOpenClawEntry(join(prefix, "tools", "node-v24.15.0"));
|
||||
writeFileSync(join(home, ".npmrc"), "min-release-age=7\n");
|
||||
writeNpmFreshnessConflictFixture(join(nodeBin, "npm"), argsLog);
|
||||
|
||||
@@ -1643,6 +1784,7 @@ describe("install-cli.sh", () => {
|
||||
mkdirSync(nodeBin, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
mkdirSync(project, { recursive: true });
|
||||
writeInstalledOpenClawEntry(join(prefix, "tools", "node-v24.15.0"));
|
||||
writeFileSync(join(home, ".npmrc"), "before=2026-01-01T00:00:00.000Z\n");
|
||||
writeFileSync(join(project, ".npmrc"), "min-release-age=7\n");
|
||||
writeNpmBeforePolicyFixture(join(nodeBin, "npm"), argsLog);
|
||||
|
||||
@@ -1,6 +1,38 @@
|
||||
// NPM CLI fixture writers used by installer shell-script tests.
|
||||
import { chmodSync, writeFileSync } from "node:fs";
|
||||
|
||||
export function writeNpmInstallRetryFixture(path: string) {
|
||||
writeFileSync(
|
||||
path,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'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',
|
||||
"is_install=0",
|
||||
'for arg in "$@"; do [[ "$arg" == "install" ]] && is_install=1; done',
|
||||
'if [[ "$is_install" -eq 0 ]]; then exit 0; fi',
|
||||
'spec="${!#}"',
|
||||
'printf "%s\\n" "$spec" >> "$NPM_FAKE_CALLS"',
|
||||
'attempt="$(wc -l < "$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"',
|
||||
' printf "#!/bin/sh\\nprintf \'2026.8.1\\\\n\'\\n" > "$NPM_FAKE_PACKAGE_DIR/openclaw.mjs"',
|
||||
' printf "#!/usr/bin/env node\\n" > "$NPM_FAKE_PACKAGE_DIR/dist/entry.js"',
|
||||
' chmod +x "$NPM_FAKE_PACKAGE_DIR/openclaw.mjs" "$NPM_FAKE_PACKAGE_DIR/dist/entry.js"',
|
||||
" fi",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'printf "%s (attempt %s)\\n" "$NPM_FAKE_ERROR" "$attempt" >&2',
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
export function writeNpmFreshnessConflictFixture(path: string, argsLog: string) {
|
||||
writeFileSync(
|
||||
path,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
writeNpmBeforePolicyFixture,
|
||||
writeNpmFreshnessConflictFixture,
|
||||
writeNpmInstallRetryFixture,
|
||||
} from "./install-npm-fixtures.js";
|
||||
|
||||
const SCRIPT_PATH = "scripts/install.sh";
|
||||
@@ -1343,6 +1344,222 @@ NODE
|
||||
expect(result.stdout).toContain("--install-method git --version main");
|
||||
});
|
||||
|
||||
it("links the executable package launcher when dist/entry.js is not executable", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-bin-link-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const packageDir = join(tmp, "lib", "node_modules", "openclaw");
|
||||
mkdirSync(join(packageDir, "dist"), { recursive: true });
|
||||
mkdirSync(bin, { recursive: true });
|
||||
writeFileSync(join(packageDir, "dist", "entry.js"), "export {};\n");
|
||||
writeFileSync(
|
||||
join(packageDir, "openclaw.mjs"),
|
||||
'#!/usr/bin/env node\nprocess.stdout.write("OpenClaw fixture\\n");\n',
|
||||
);
|
||||
chmodSync(join(packageDir, "openclaw.mjs"), 0o755);
|
||||
|
||||
try {
|
||||
const result = runInstallShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`npm() { [[ "$1" == "root" ]] && printf '%s\\n' ${JSON.stringify(join(tmp, "lib", "node_modules"))}; }`,
|
||||
`npm_global_bin_dir() { printf '%s\\n' ${JSON.stringify(bin)}; }`,
|
||||
"ensure_openclaw_bin_link",
|
||||
`${JSON.stringify(join(bin, "openclaw"))} --version`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
expect(result.status, result.stderr || result.stdout).toBe(0);
|
||||
expect(result.stdout).toContain("OpenClaw fixture");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects an installed package whose executable launcher is missing", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-missing-bin-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const packageDir = join(tmp, "lib", "node_modules", "openclaw");
|
||||
mkdirSync(join(packageDir, "dist"), { recursive: true });
|
||||
mkdirSync(bin, { recursive: true });
|
||||
writeFileSync(join(packageDir, "dist", "entry.js"), "#!/usr/bin/env node\n");
|
||||
chmodSync(join(packageDir, "dist", "entry.js"), 0o755);
|
||||
|
||||
try {
|
||||
const result = runInstallShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`npm() { [[ "$1" == "root" ]] && printf '%s\\n' ${JSON.stringify(join(tmp, "lib", "node_modules"))}; }`,
|
||||
`npm_global_bin_dir() { printf '%s\\n' ${JSON.stringify(bin)}; }`,
|
||||
"ensure_openclaw_bin_link",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(existsSync(join(bin, "openclaw"))).toBe(false);
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects an installed package whose launcher fails version validation", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-invalid-bin-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const packageDir = join(tmp, "lib", "node_modules", "openclaw");
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
mkdirSync(bin, { recursive: true });
|
||||
writeFileSync(join(packageDir, "openclaw.mjs"), "#!/bin/sh\nexit 7\n");
|
||||
chmodSync(join(packageDir, "openclaw.mjs"), 0o755);
|
||||
|
||||
try {
|
||||
const result = runInstallShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`npm() { [[ "$1" == "root" ]] && printf '%s\\n' ${JSON.stringify(join(tmp, "lib", "node_modules"))}; }`,
|
||||
`npm_global_bin_dir() { printf '%s\\n' ${JSON.stringify(bin)}; }`,
|
||||
"ensure_openclaw_bin_link",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
expect(result.status).toBe(7);
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ requested: "latest", outcome: "success", error: "", calls: 1, status: 0 },
|
||||
{
|
||||
requested: "beta",
|
||||
outcome: "transient",
|
||||
error: "ECONNRESET socket hang up",
|
||||
calls: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
requested: "next",
|
||||
outcome: "transient",
|
||||
error: "ECONNRESET socket hang up",
|
||||
calls: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
requested: "2026.8.1",
|
||||
outcome: "transient",
|
||||
error: "ECONNRESET socket hang up",
|
||||
calls: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
requested: "latest",
|
||||
outcome: "persistent",
|
||||
error: "EACCES permission denied",
|
||||
calls: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
requested: "beta",
|
||||
outcome: "persistent",
|
||||
error: "ENOSPC no space left",
|
||||
calls: 2,
|
||||
status: 1,
|
||||
},
|
||||
])(
|
||||
"keeps openclaw@$requested immutable across $outcome npm installs",
|
||||
({ requested, outcome, error, calls: expectedCalls, status: expectedStatus }) => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npm-retry-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const calls = join(tmp, "calls");
|
||||
const npmRoot = join(tmp, "lib", "node_modules");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
writeNpmInstallRetryFixture(join(bin, "npm"));
|
||||
|
||||
try {
|
||||
const result = runInstallShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`PATH=${JSON.stringify(`${bin}:/usr/bin:/bin`)}`,
|
||||
`OPENCLAW_VERSION=${requested}`,
|
||||
"USE_BETA=0",
|
||||
"NPM_LOGLEVEL=error",
|
||||
"NPM_SILENT_FLAG=",
|
||||
`npm_global_bin_dir() { printf '%s\\n' ${JSON.stringify(bin)}; }`,
|
||||
"set +e",
|
||||
"install_openclaw",
|
||||
"status=$?",
|
||||
"exit $status",
|
||||
].join("\n"),
|
||||
{
|
||||
NPM_FAKE_CALLS: calls,
|
||||
NPM_FAKE_ERROR: error,
|
||||
NPM_FAKE_OUTCOME: outcome,
|
||||
NPM_FAKE_PACKAGE_DIR: join(npmRoot, "openclaw"),
|
||||
NPM_FAKE_ROOT: npmRoot,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(expectedStatus);
|
||||
expect(readFileSync(calls, "utf8").trim().split("\n")).toEqual(
|
||||
Array.from({ length: expectedCalls }, () => `openclaw@${requested}`),
|
||||
);
|
||||
if (expectedStatus !== 0) {
|
||||
expect(`${result.stdout}\n${result.stderr}`).toContain(`${error} (attempt 2)`);
|
||||
}
|
||||
if (requested !== "next") {
|
||||
expect(`${result.stdout}\n${result.stderr}`).not.toContain("openclaw@next");
|
||||
}
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("fails after retrying the exact npm spec when npm exits zero without installing OpenClaw", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npm-empty-success-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const calls = join(tmp, "calls");
|
||||
const npmRoot = join(tmp, "lib", "node_modules");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
writeNpmInstallRetryFixture(join(bin, "npm"));
|
||||
|
||||
try {
|
||||
const result = runInstallShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`PATH=${JSON.stringify(`${bin}:/usr/bin:/bin`)}`,
|
||||
"OPENCLAW_VERSION=latest",
|
||||
"USE_BETA=0",
|
||||
"NPM_LOGLEVEL=error",
|
||||
"NPM_SILENT_FLAG=",
|
||||
`npm_global_bin_dir() { printf '%s\\n' ${JSON.stringify(bin)}; }`,
|
||||
"install_openclaw",
|
||||
].join("\n"),
|
||||
{
|
||||
NPM_FAKE_CALLS: calls,
|
||||
NPM_FAKE_ERROR: "",
|
||||
NPM_FAKE_OUTCOME: "success",
|
||||
NPM_FAKE_ROOT: npmRoot,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(readFileSync(calls, "utf8").trim().split("\n")).toEqual([
|
||||
"openclaw@latest",
|
||||
"openclaw@latest",
|
||||
]);
|
||||
expect(`${result.stdout}\n${result.stderr}`).toContain(
|
||||
"npm install did not produce a usable OpenClaw package",
|
||||
);
|
||||
expect(`${result.stdout}\n${result.stderr}`).not.toContain("openclaw@next");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not emit before args when npmrc min-release-age computes a before cutoff", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npm-freshness-"));
|
||||
const bin = join(tmp, "bin");
|
||||
|
||||
Reference in New Issue
Block a user