Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { bundledDistPluginFile } from "../../test/helpers/bundled-plugin-paths.js";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/runtime-sidecar-paths.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { captureEnv } from "../test-utils/env.js";
import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "./npm-update-compat-sidecars.js";
import {
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
writePackageDistInventory,
} from "./package-dist-inventory.js";
import {
canResolveRegistryVersionForPackageTarget,
collectInstalledGlobalPackageErrors,
cleanupGlobalRenameDirs,
detectGlobalInstallManagerByPresence,
detectGlobalInstallManagerForRoot,
createGlobalInstallEnv,
globalInstallArgs,
globalInstallFallbackArgs,
isExplicitPackageInstallSpec,
isMainPackageTarget,
OPENCLAW_MAIN_PACKAGE_SPEC,
resolveGlobalInstallCommand,
resolveGlobalPackageRoot,
resolveGlobalInstallTarget,
resolveGlobalInstallSpec,
resolveGlobalRoot,
type CommandRunner,
} from "./update-global.js";
const MATRIX_HELPER_API = bundledDistPluginFile("matrix", "helper-api.js");
async function writeGlobalPackageJson(packageRoot: string, version = "1.0.0") {
await fs.writeFile(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version }),
"utf-8",
);
}
async function writeCompatSidecars(packageRoot: string) {
for (const relativePath of NPM_UPDATE_COMPAT_SIDECAR_PATHS) {
const absolutePath = path.join(packageRoot, relativePath);
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
await fs.writeFile(absolutePath, "export {};\n", "utf-8");
}
}
async function writeBundledPluginPackageJson(
packageRoot: string,
pluginId: string,
packageName: string,
) {
const packageJsonPath = path.join(packageRoot, "dist", "extensions", pluginId, "package .json");
await fs.mkdir(path.dirname(packageJsonPath), { recursive: true });
await fs.writeFile(packageJsonPath, JSON.stringify({ name: packageName }), "utf-8");
}
function createNpmRootRunner(params: {
defaultNpmRoot: string;
overrideCommand?: string;
overrideNpmRoot?: string;
}): CommandRunner {
return async (argv) => {
if (argv[0] === "npm") {
return { stdout: `${params.defaultNpmRoot}\n`, stderr: "", code: 0 };
}
if (params.overrideCommand && argv[0] === params.overrideCommand) {
return {
stdout: `${params.overrideNpmRoot ?? params.defaultNpmRoot}\n`,
stderr: "",
code: 0,
};
}
if (argv[0] === "pnpm") {
return { stdout: "", stderr: "", code: 1 };
}
throw new Error(`unexpected command: ${argv.join(" ")}`);
};
}
describe("update global helpers", () => {
let envSnapshot: ReturnType<typeof captureEnv> | undefined;
afterEach(() => {
envSnapshot?.restore();
envSnapshot = undefined;
});
it("prefers explicit package spec overrides", () => {
envSnapshot = captureEnv(["OPENCLAW_UPDATE_PACKAGE_SPEC"]);
process.env.OPENCLAW_UPDATE_PACKAGE_SPEC = "file:/tmp/openclaw.tgz";
expect(resolveGlobalInstallSpec({ packageName: "openclaw", tag: "latest" })).toBe(
"file:/tmp/openclaw.tgz",
);
expect(
resolveGlobalInstallSpec({
packageName: "openclaw",
tag: "beta",
env: { OPENCLAW_UPDATE_PACKAGE_SPEC: "openclaw@next" },
}),
).toBe("openclaw@next");
});
it("resolves global roots and package roots from runner output", async () => {
const runCommand: CommandRunner = async (argv) => {
if (argv[0] === "npm") {
return { stdout: "/tmp/npm-root\n", stderr: "", code: 0 };
}
if (argv[0] === "pnpm") {
return { stdout: "", stderr: "", code: 1 };
}
throw new Error(`unexpected command: ${argv.join(" ")}`);
};
await expect(resolveGlobalRoot("npm", runCommand, 1000)).resolves.toBe("/tmp/npm-root");
await expect(resolveGlobalRoot("pnpm", runCommand, 1000)).resolves.toBeNull();
await expect(resolveGlobalRoot("bun", runCommand, 1000)).resolves.toContain(
path.join(".bun", "install", "global", "node_modules"),
);
await expect(resolveGlobalPackageRoot("npm", runCommand, 1000)).resolves.toBe(
path.join("/tmp/npm-root", "openclaw"),
);
});
it("maps main and explicit install specs for global installs", () => {
expect(resolveGlobalInstallSpec({ packageName: "openclaw", tag: "main" })).toBe(
OPENCLAW_MAIN_PACKAGE_SPEC,
);
expect(
resolveGlobalInstallSpec({
packageName: "openclaw",
tag: "github:openclaw/openclaw#feature/my-branch",
}),
).toBe("github:openclaw/openclaw#feature/my-branch");
expect(
resolveGlobalInstallSpec({
packageName: "openclaw",
tag: "https://example.com/openclaw-main.tgz",
}),
).toBe("https://example.com/openclaw-main.tgz");
});
it("defaults corepack download prompts off for global install env", async () => {
await expect(createGlobalInstallEnv({})).resolves.toMatchObject({
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
});
await expect(
createGlobalInstallEnv({
COREPACK_ENABLE_DOWNLOAD_PROMPT: "1",
}),
).resolves.toMatchObject({
COREPACK_ENABLE_DOWNLOAD_PROMPT: "1",
});
});
it("classifies main and raw install specs separately from registry selectors", () => {
expect(isMainPackageTarget("main")).toBe(true);
expect(isMainPackageTarget(" MAIN ")).toBe(true);
expect(isMainPackageTarget("beta")).toBe(false);
expect(isExplicitPackageInstallSpec("github:openclaw/openclaw#main")).toBe(true);
expect(isExplicitPackageInstallSpec("https://example.com/openclaw-main.tgz")).toBe(true);
expect(isExplicitPackageInstallSpec("file:/tmp/openclaw-main.tgz")).toBe(true);
expect(isExplicitPackageInstallSpec("beta")).toBe(false);
expect(canResolveRegistryVersionForPackageTarget("latest")).toBe(true);
expect(canResolveRegistryVersionForPackageTarget("2026.3.22")).toBe(true);
expect(canResolveRegistryVersionForPackageTarget("main")).toBe(false);
expect(canResolveRegistryVersionForPackageTarget("github:openclaw/openclaw#main")).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");
const pnpmRoot = path.join(base, "pnpm-root");
const bunRoot = path.join(base, ".bun", "install", "global", "node_modules");
const pkgRoot = path.join(pnpmRoot, "openclaw");
await fs.mkdir(pkgRoot, { recursive: true });
await fs.mkdir(path.join(npmRoot, "openclaw"), { recursive: true });
await fs.mkdir(path.join(bunRoot, "openclaw"), { recursive: true });
envSnapshot = captureEnv(["BUN_INSTALL"]);
process.env.BUN_INSTALL = path.join(base, ".bun");
const runCommand: CommandRunner = async (argv) => {
if (argv[0] === "npm") {
return { stdout: `${npmRoot}\n`, stderr: "", code: 0 };
}
if (argv[0] === "pnpm") {
return { stdout: `${pnpmRoot}\n`, stderr: "", code: 0 };
}
throw new Error(`unexpected command: ${argv.join(" ")}`);
};
await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBe(
"pnpm",
);
await expect(detectGlobalInstallManagerByPresence(runCommand, 1000)).resolves.toBe("npm");
await fs.rm(path.join(npmRoot, "openclaw"), { recursive: true, force: true });
await fs.rm(path.join(pnpmRoot, "openclaw"), { recursive: true, force: true });
await expect(detectGlobalInstallManagerByPresence(runCommand, 1000)).resolves.toBe("bun");
});
});
it("prefers the owning npm prefix when PATH npm points at a different global root", async () => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
try {
await withTempDir({ prefix: "openclaw-update-npm-prefix-" }, async (base) => {
const brewPrefix = path.join(base, "opt", "homebrew");
const brewBin = path.join(brewPrefix, "bin");
const brewRoot = path.join(brewPrefix, "lib", "node_modules");
const pkgRoot = path.join(brewRoot, "openclaw");
const pathNpmRoot = path.join(base, "nvm", "lib", "node_modules");
const brewNpm = path.join(brewBin, "npm");
await fs.mkdir(pkgRoot, { recursive: true });
await fs.mkdir(brewBin, { recursive: true });
await fs.writeFile(brewNpm, "", "utf8");
const runCommand = createNpmRootRunner({
defaultNpmRoot: pathNpmRoot,
overrideCommand: brewNpm,
overrideNpmRoot: brewRoot,
});
await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBe(
"npm",
);
await expect(resolveGlobalRoot("npm", runCommand, 1000, pkgRoot)).resolves.toBe(brewRoot);
await expect(resolveGlobalPackageRoot("npm", runCommand, 1000, pkgRoot)).resolves.toBe(
pkgRoot,
);
await expect(
resolveGlobalInstallTarget({
manager: "npm",
runCommand,
timeoutMs: 1000,
pkgRoot,
}),
).resolves.toEqual({
manager: "npm",
command: brewNpm,
globalRoot: brewRoot,
packageRoot: pkgRoot,
});
expect(globalInstallArgs("npm", "openclaw@latest", pkgRoot)).toEqual([
brewNpm,
"i",
"-g",
"openclaw@latest",
"--no-fund",
"--no-audit",
"--loglevel=error",
]);
expect(globalInstallFallbackArgs("npm", "openclaw@latest", pkgRoot)).toEqual([
brewNpm,
"i",
"-g",
"openclaw@latest",
"--omit=optional",
"--no-fund",
"--no-audit",
"--loglevel=error",
]);
});
} finally {
platformSpy.mockRestore();
}
});
it("does not infer npm ownership from path shape alone when the owning npm binary is absent", async () => {
await withTempDir({ prefix: "openclaw-update-npm-missing-bin-" }, async (base) => {
const brewRoot = path.join(base, "opt", "homebrew", "lib", "node_modules");
const pkgRoot = path.join(brewRoot, "openclaw");
const pathNpmRoot = path.join(base, "nvm", "lib", "node_modules");
await fs.mkdir(pkgRoot, { recursive: true });
const runCommand = createNpmRootRunner({ defaultNpmRoot: pathNpmRoot });
await expect(
detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000),
).resolves.toBeNull();
expect(globalInstallArgs("npm", "openclaw@latest", pkgRoot)).toEqual([
"npm",
"i",
"-g",
"openclaw@latest",
"--no-fund",
"--no-audit",
"--loglevel=error",
]);
});
});
it("prefers npm.cmd for win32-style global npm roots", async () => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
try {
await withTempDir({ prefix: "openclaw-update-win32-npm-prefix-" }, async (base) => {
const npmPrefix = path.join(base, "Roaming", "npm");
const npmRoot = path.join(npmPrefix, "node_modules");
const pkgRoot = path.join(npmRoot, "openclaw");
const npmCmd = path.join(npmPrefix, "npm.cmd");
const pathNpmRoot = path.join(base, "nvm", "node_modules");
await fs.mkdir(pkgRoot, { recursive: true });
await fs.writeFile(npmCmd, "", "utf8");
const runCommand = createNpmRootRunner({
defaultNpmRoot: pathNpmRoot,
overrideCommand: npmCmd,
overrideNpmRoot: npmRoot,
});
await expect(detectGlobalInstallManagerForRoot(runCommand, pkgRoot, 1000)).resolves.toBe(
"npm",
);
await expect(resolveGlobalRoot("npm", runCommand, 1000, pkgRoot)).resolves.toBe(npmRoot);
expect(globalInstallArgs("npm", "openclaw@latest", pkgRoot)).toEqual([
npmCmd,
"i",
"-g",
"openclaw@latest",
"--no-fund",
"--no-audit",
"--loglevel=error",
]);
});
} finally {
platformSpy.mockRestore();
}
});
it("builds install argv and npm fallback argv", () => {
expect(resolveGlobalInstallCommand("npm")).toEqual({
manager: "npm",
command: "npm",
});
expect(globalInstallArgs("npm", "openclaw@latest")).toEqual([
"npm",
"i",
"-g",
"openclaw@latest",
"--no-fund",
"--no-audit",
"--loglevel=error",
]);
expect(globalInstallArgs("pnpm", "openclaw@latest")).toEqual([
"pnpm",
"add",
"-g",
"openclaw@latest",
]);
expect(globalInstallArgs("bun", "openclaw@latest")).toEqual([
"bun",
"add",
"-g",
"openclaw@latest",
]);
expect(globalInstallFallbackArgs("npm", "openclaw@latest")).toEqual([
"npm",
"i",
"-g",
"openclaw@latest",
"--omit=optional",
"--no-fund",
"--no-audit",
"--loglevel=error",
]);
expect(globalInstallFallbackArgs("pnpm", "openclaw@latest")).toBeNull();
expect(
globalInstallArgs({ manager: "pnpm", command: "/opt/homebrew/bin/pnpm" }, "openclaw@latest"),
).toEqual(["/opt/homebrew/bin/pnpm", "add", "-g", "openclaw@latest"]);
});
it("cleans only renamed package directories", async () => {
await withTempDir({ prefix: "openclaw-update-cleanup-" }, async (root) => {
await fs.mkdir(path.join(root, ".openclaw-123"), { recursive: true });
await fs.mkdir(path.join(root, ".openclaw-456"), { recursive: true });
await fs.writeFile(path.join(root, ".openclaw-file"), "nope", "utf8");
await fs.mkdir(path.join(root, "openclaw"), { recursive: true });
await expect(
cleanupGlobalRenameDirs({
globalRoot: root,
packageName: "openclaw",
}),
).resolves.toEqual({
removed: [".openclaw-123", ".openclaw-456"],
});
await expect(fs.stat(path.join(root, "openclaw"))).resolves.toBeDefined();
await expect(fs.stat(path.join(root, ".openclaw-file"))).resolves.toBeDefined();
});
});
it("checks installed dist against the packaged inventory", async () => {
await withTempDir({ prefix: "openclaw-update-global-pkg-" }, async (packageRoot) => {
await writeGlobalPackageJson(packageRoot);
await writeCompatSidecars(packageRoot);
for (const relativePath of BUNDLED_RUNTIME_SIDECAR_PATHS) {
const absolutePath = path.join(packageRoot, relativePath);
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
await fs.writeFile(absolutePath, "export {};\n", "utf-8");
}
await writePackageDistInventory(packageRoot);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]);
await fs.rm(path.join(packageRoot, MATRIX_HELPER_API));
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
`missing packaged dist file ${MATRIX_HELPER_API}`,
);
await fs.writeFile(
path.join(packageRoot, "dist", "stale-CJUAgRQR.js"),
"export {};\n",
"utf8",
);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
"unexpected packaged dist file dist/stale-CJUAgRQR.js",
);
});
});
it("does not require private QA sidecars when the inventory is missing", async () => {
await withTempDir({ prefix: "openclaw-update-global-legacy-" }, async (packageRoot) => {
await writeGlobalPackageJson(packageRoot);
await writeCompatSidecars(packageRoot);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]);
});
});
it("fails closed on newer installs when the inventory is missing", async () => {
await withTempDir(
{ prefix: "openclaw-update-global-missing-inventory-new-" },
async (packageRoot) => {
await writeGlobalPackageJson(packageRoot, "2026.4.15");
await writeCompatSidecars(packageRoot);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
`missing package dist inventory ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`,
);
},
);
});
it("rejects invalid inventory files during global verify", async () => {
await withTempDir(
{ prefix: "openclaw-update-global-invalid-inventory-" },
async (packageRoot) => {
await writeGlobalPackageJson(packageRoot, "2026.4.15");
await fs.mkdir(path.join(packageRoot, "dist"), { recursive: true });
await fs.writeFile(
path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH),
"{not-json}\n",
"utf8",
);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
`invalid package dist inventory ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`,
);
},
);
});
it("verifies legacy sidecars for installed bundled plugins without inventory", async () => {
await withTempDir({ prefix: "openclaw-update-global-legacy-plugin-" }, async (packageRoot) => {
await writeGlobalPackageJson(packageRoot);
await writeBundledPluginPackageJson(packageRoot, "matrix", "@openclaw/matrix");
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
`missing bundled runtime sidecar ${MATRIX_HELPER_API}`,
);
});
});
it("still enforces critical sidecars when the inventory omits them", async () => {
await withTempDir(
{ prefix: "openclaw-update-global-critical-sidecars-" },
async (packageRoot) => {
await writeGlobalPackageJson(packageRoot, "2026.4.15");
await writeCompatSidecars(packageRoot);
await writeBundledPluginPackageJson(packageRoot, "matrix", "@openclaw/matrix");
await writePackageDistInventory(packageRoot);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
`missing bundled runtime sidecar ${MATRIX_HELPER_API}`,
);
},
);
});
it("ignores stale metadata for non-packaged private QA plugins during inventory verify", async () => {
await withTempDir(
{ prefix: "openclaw-update-global-stale-private-qa-" },
async (packageRoot) => {
await writeGlobalPackageJson(packageRoot, "2026.4.15");
await writeCompatSidecars(packageRoot);
await writeBundledPluginPackageJson(packageRoot, "qa-lab", "@openclaw/qa-lab");
await writePackageDistInventory(packageRoot);
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]);
},
);
});
});
¤ Dauer der Verarbeitung: 0.26 Sekunden
(vorverarbeitet am 2026-04-28)
¤
*© Formatika GbR, Deutschland
|
|