mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:10:45 +00:00
fix: avoid full runtime dependency restaging
This commit is contained in:
@@ -445,6 +445,10 @@ describe("installBundledRuntimeDeps", () => {
|
||||
expect(JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"))).toEqual({
|
||||
name: "openclaw-runtime-deps-install",
|
||||
private: true,
|
||||
dependencies: {
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
grammy: "1.37.0",
|
||||
},
|
||||
});
|
||||
writeInstalledPackage(cwd, "@grammyjs/runner", "2.0.3");
|
||||
return {
|
||||
@@ -460,6 +464,7 @@ describe("installBundledRuntimeDeps", () => {
|
||||
installBundledRuntimeDeps({
|
||||
installRoot,
|
||||
missingSpecs: ["@grammyjs/runner@^2.0.3"],
|
||||
installSpecs: ["@grammyjs/runner@^2.0.3", "grammy@1.37.0"],
|
||||
env: {
|
||||
HOME: parentRoot,
|
||||
},
|
||||
@@ -467,13 +472,47 @@ describe("installBundledRuntimeDeps", () => {
|
||||
|
||||
expect(spawnSyncMock).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(Array),
|
||||
expect.not.arrayContaining(["grammy@1.37.0"]),
|
||||
expect.objectContaining({
|
||||
cwd: installRoot,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("repairs external install roots by installing only missing specs while retaining staged deps", async () => {
|
||||
const installRoot = makeTempDir();
|
||||
writeInstalledPackage(installRoot, "alpha-runtime", "1.0.0");
|
||||
spawnMock.mockImplementation((_command, args, options) => {
|
||||
const cwd = String(options?.cwd ?? "");
|
||||
expect(args.slice(-3)).toEqual(["install", "--ignore-scripts", "beta-runtime@2.0.0"]);
|
||||
expect(JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"))).toEqual({
|
||||
name: "openclaw-runtime-deps-install",
|
||||
private: true,
|
||||
dependencies: {
|
||||
"alpha-runtime": "1.0.0",
|
||||
"beta-runtime": "2.0.0",
|
||||
},
|
||||
});
|
||||
writeInstalledPackage(cwd, "beta-runtime", "2.0.0");
|
||||
const child = new EventEmitter() as ReturnType<typeof spawn>;
|
||||
Object.assign(child, {
|
||||
stdout: new EventEmitter(),
|
||||
stderr: new EventEmitter(),
|
||||
});
|
||||
queueMicrotask(() => child.emit("close", 0, null));
|
||||
return child;
|
||||
});
|
||||
|
||||
await repairBundledRuntimeDepsInstallRootAsync({
|
||||
installRoot,
|
||||
missingSpecs: ["beta-runtime@2.0.0"],
|
||||
installSpecs: ["alpha-runtime@1.0.0", "beta-runtime@2.0.0"],
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("warns but still installs bundled runtime deps when disk space looks low", () => {
|
||||
const installRoot = makeTempDir();
|
||||
const warn = vi.fn();
|
||||
@@ -541,6 +580,9 @@ describe("installBundledRuntimeDeps", () => {
|
||||
).toEqual({
|
||||
name: "openclaw-runtime-deps-install",
|
||||
private: true,
|
||||
dependencies: {
|
||||
tokenjuice: "0.6.1",
|
||||
},
|
||||
});
|
||||
expect(
|
||||
JSON.parse(
|
||||
@@ -562,6 +604,60 @@ describe("installBundledRuntimeDeps", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("installs the full retained set when plugin-root staging replaces node_modules", () => {
|
||||
const pluginRoot = makeTempDir();
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
dependencies: {
|
||||
"alpha-runtime": "1.0.0",
|
||||
"beta-runtime": "2.0.0",
|
||||
},
|
||||
}),
|
||||
);
|
||||
writeInstalledPackage(pluginRoot, "alpha-runtime", "1.0.0");
|
||||
spawnSyncMock.mockImplementation((_command, args, options) => {
|
||||
const cwd = String(options?.cwd ?? "");
|
||||
expect(cwd).toBe(path.join(pluginRoot, ".openclaw-install-stage"));
|
||||
expect((args ?? []).slice(-4)).toEqual([
|
||||
"install",
|
||||
"--ignore-scripts",
|
||||
"alpha-runtime@1.0.0",
|
||||
"beta-runtime@2.0.0",
|
||||
]);
|
||||
writeInstalledPackage(cwd, "alpha-runtime", "1.0.0");
|
||||
writeInstalledPackage(cwd, "beta-runtime", "2.0.0");
|
||||
return {
|
||||
pid: 123,
|
||||
output: [],
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
signal: null,
|
||||
status: 0,
|
||||
};
|
||||
});
|
||||
|
||||
expect(
|
||||
ensureBundledPluginRuntimeDeps({
|
||||
env: {},
|
||||
pluginId: "local-plugin",
|
||||
pluginRoot,
|
||||
}),
|
||||
).toEqual({
|
||||
installedSpecs: ["beta-runtime@2.0.0"],
|
||||
retainSpecs: ["alpha-runtime@1.0.0", "beta-runtime@2.0.0"],
|
||||
});
|
||||
expect(spawnSyncMock).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(pluginRoot, "node_modules", "alpha-runtime", "package.json"),
|
||||
"utf8",
|
||||
),
|
||||
),
|
||||
).toEqual({ name: "alpha-runtime", version: "1.0.0" });
|
||||
});
|
||||
|
||||
it("uses an OpenClaw-owned npm cache for runtime dependency installs", () => {
|
||||
const installRoot = makeTempDir();
|
||||
spawnSyncMock.mockImplementation((_command, _args, options) => {
|
||||
@@ -595,6 +691,9 @@ describe("installBundledRuntimeDeps", () => {
|
||||
expect(JSON.parse(fs.readFileSync(path.join(installRoot, "package.json"), "utf8"))).toEqual({
|
||||
name: "openclaw-runtime-deps-install",
|
||||
private: true,
|
||||
dependencies: {
|
||||
tokenjuice: "0.6.1",
|
||||
},
|
||||
});
|
||||
expect(spawnSyncMock).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
|
||||
@@ -1757,16 +1757,33 @@ function shouldCleanBundledRuntimeDepsInstallExecutionRoot(params: {
|
||||
return installExecutionRoot.startsWith(`${installRoot}${path.sep}`);
|
||||
}
|
||||
|
||||
function ensureNpmInstallExecutionManifest(installExecutionRoot: string): void {
|
||||
function createNpmInstallExecutionManifest(installSpecs: readonly string[]): JsonObject {
|
||||
const dependencies: Record<string, string> = {};
|
||||
for (const spec of installSpecs) {
|
||||
const dep = parseInstallableRuntimeDepSpec(spec);
|
||||
dependencies[dep.name] = dep.version;
|
||||
}
|
||||
const sortedDependencies = Object.fromEntries(
|
||||
Object.entries(dependencies).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
);
|
||||
return {
|
||||
name: "openclaw-runtime-deps-install",
|
||||
private: true,
|
||||
...(Object.keys(sortedDependencies).length > 0 ? { dependencies: sortedDependencies } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function ensureNpmInstallExecutionManifest(
|
||||
installExecutionRoot: string,
|
||||
installSpecs: readonly string[] = [],
|
||||
): void {
|
||||
const manifestPath = path.join(installExecutionRoot, "package.json");
|
||||
if (fs.existsSync(manifestPath)) {
|
||||
const manifest = createNpmInstallExecutionManifest(installSpecs);
|
||||
const nextContents = `${JSON.stringify(manifest, null, 2)}\n`;
|
||||
if (fs.existsSync(manifestPath) && fs.readFileSync(manifestPath, "utf8") === nextContents) {
|
||||
return;
|
||||
}
|
||||
fs.writeFileSync(
|
||||
manifestPath,
|
||||
`${JSON.stringify({ name: "openclaw-runtime-deps-install", private: true }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(manifestPath, nextContents, "utf8");
|
||||
}
|
||||
|
||||
function formatBundledRuntimeDepsInstallError(result: {
|
||||
@@ -1885,6 +1902,7 @@ export function installBundledRuntimeDeps(params: {
|
||||
installExecutionRoot?: string;
|
||||
linkNodeModulesFromExecutionRoot?: boolean;
|
||||
missingSpecs: string[];
|
||||
installSpecs?: string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
warn?: (message: string) => void;
|
||||
}): void {
|
||||
@@ -1911,7 +1929,15 @@ export function installBundledRuntimeDeps(params: {
|
||||
// doctor repair path installs directly in the external stage dir; without a
|
||||
// manifest, npm can honor a user's global prefix config and write under
|
||||
// $HOME/node_modules instead of our managed stage.
|
||||
ensureNpmInstallExecutionManifest(installExecutionRoot);
|
||||
//
|
||||
// The manifest also declares retained staged deps. npm may prune packages
|
||||
// that are present in node_modules but absent from package dependencies
|
||||
// while installing a new explicit spec, so keep retained deps in the
|
||||
// manifest and pass only actually missing specs as install args.
|
||||
ensureNpmInstallExecutionManifest(
|
||||
installExecutionRoot,
|
||||
params.installSpecs ?? params.missingSpecs,
|
||||
);
|
||||
const installEnv = createBundledRuntimeDepsInstallEnv(params.env, {
|
||||
cacheDir: path.join(installExecutionRoot, ".openclaw-npm-cache"),
|
||||
});
|
||||
@@ -1955,6 +1981,7 @@ export async function installBundledRuntimeDepsAsync(params: {
|
||||
installExecutionRoot?: string;
|
||||
linkNodeModulesFromExecutionRoot?: boolean;
|
||||
missingSpecs: string[];
|
||||
installSpecs?: string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
warn?: (message: string) => void;
|
||||
onProgress?: (message: string) => void;
|
||||
@@ -1978,7 +2005,10 @@ export async function installBundledRuntimeDepsAsync(params: {
|
||||
if (diskWarning) {
|
||||
params.warn?.(diskWarning);
|
||||
}
|
||||
ensureNpmInstallExecutionManifest(installExecutionRoot);
|
||||
ensureNpmInstallExecutionManifest(
|
||||
installExecutionRoot,
|
||||
params.installSpecs ?? params.missingSpecs,
|
||||
);
|
||||
const installEnv = createBundledRuntimeDepsInstallEnv(params.env, {
|
||||
cacheDir: path.join(installExecutionRoot, ".openclaw-npm-cache"),
|
||||
});
|
||||
@@ -2035,7 +2065,8 @@ export function repairBundledRuntimeDepsInstallRoot(params: {
|
||||
((installParams) =>
|
||||
installBundledRuntimeDeps({
|
||||
installRoot: installParams.installRoot,
|
||||
missingSpecs: installParams.installSpecs ?? installParams.missingSpecs,
|
||||
missingSpecs: installParams.missingSpecs,
|
||||
installSpecs: installParams.installSpecs,
|
||||
env: params.env,
|
||||
warn: params.warn,
|
||||
}));
|
||||
@@ -2129,7 +2160,8 @@ export async function repairBundledRuntimeDepsInstallRootAsync(params: {
|
||||
((installParams) =>
|
||||
installBundledRuntimeDepsAsync({
|
||||
installRoot: installParams.installRoot,
|
||||
missingSpecs: installParams.installSpecs ?? installParams.missingSpecs,
|
||||
missingSpecs: installParams.missingSpecs,
|
||||
installSpecs: installParams.installSpecs,
|
||||
env: params.env,
|
||||
warn: params.warn,
|
||||
onProgress: params.onProgress,
|
||||
@@ -2268,14 +2300,22 @@ export function ensureBundledPluginRuntimeDeps(params: {
|
||||
|
||||
const install =
|
||||
params.installDeps ??
|
||||
((installParams) =>
|
||||
installBundledRuntimeDeps({
|
||||
((installParams) => {
|
||||
const isolatedExecutionRoot =
|
||||
installParams.installExecutionRoot &&
|
||||
path.resolve(installParams.installExecutionRoot) !==
|
||||
path.resolve(installParams.installRoot);
|
||||
return installBundledRuntimeDeps({
|
||||
installRoot: installParams.installRoot,
|
||||
installExecutionRoot: installParams.installExecutionRoot,
|
||||
linkNodeModulesFromExecutionRoot: installParams.linkNodeModulesFromExecutionRoot,
|
||||
missingSpecs: installParams.installSpecs ?? installParams.missingSpecs,
|
||||
missingSpecs: isolatedExecutionRoot
|
||||
? (installParams.installSpecs ?? installParams.missingSpecs)
|
||||
: installParams.missingSpecs,
|
||||
installSpecs: installParams.installSpecs,
|
||||
env: params.env,
|
||||
}));
|
||||
});
|
||||
});
|
||||
const finishActivity = beginBundledRuntimeDepsInstall({
|
||||
installRoot,
|
||||
missingSpecs,
|
||||
|
||||
Reference in New Issue
Block a user