mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:50:43 +00:00
fix: cover Windows pnpm and Lobster install regressions
This commit is contained in:
@@ -83,6 +83,7 @@ Docs: https://docs.openclaw.ai
|
||||
canonical Anthropic models through `claude-cli` without passing CLI backend
|
||||
aliases to embedded harness selection. Fixes #71957. Thanks @WolvenRA.
|
||||
- CLI/update: guard Windows scheduled-task stops by state and timeout so auto-update restart cannot hang indefinitely on `schtasks /End` before stale-listener cleanup. Fixes #69970. Thanks @yangswld and @sherlock-huang.
|
||||
- Windows install/Lobster: execute `pnpm.exe` directly when `npm_execpath` points at the native pnpm binary, add an installed-package fallback for the Lobster embedded runtime, and include the Lobster runner regression test in Windows CI. Fixes #69456. Thanks @igormf.
|
||||
- Gateway/install: refresh loaded gateway service installs when the current service embeds stale gateway auth instead of returning already-installed, avoiding LaunchAgent token-mismatch loops after token rotation. Fixes #70752. Thanks @hyspacex.
|
||||
- Update: ignore bundled plugin `.openclaw-install-stage` directories during global install verification and packaged dist pruning so leftover runtime-dep staging files do not turn successful updates into `unexpected packaged dist file` failures. Fixes #71752. Thanks @waynegault.
|
||||
- Node runtime: keep node-host retry timers alive across Gateway restarts and exit on terminal credential pauses so supervised nodes do not become silent zombies. Fixes #69800. Thanks @meroli28.
|
||||
|
||||
@@ -2,7 +2,11 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmbeddedLobsterRunner, resolveLobsterCwd } from "./lobster-runner.js";
|
||||
import {
|
||||
createEmbeddedLobsterRunner,
|
||||
loadEmbeddedToolRuntimeFromPackage,
|
||||
resolveLobsterCwd,
|
||||
} from "./lobster-runner.js";
|
||||
|
||||
describe("resolveLobsterCwd", () => {
|
||||
it("defaults to the current working directory", () => {
|
||||
@@ -352,6 +356,60 @@ describe("createEmbeddedLobsterRunner", () => {
|
||||
expect(loadRuntime).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("falls back to the installed package core file when the core export is unavailable", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-lobster-package-"));
|
||||
const packageRoot = path.join(tempDir, "node_modules", "@clawdbot", "lobster");
|
||||
const packageEntryPath = path.join(packageRoot, "dist", "src", "sdk", "index.js");
|
||||
const packageCorePath = path.join(packageRoot, "dist", "src", "core", "index.js");
|
||||
|
||||
try {
|
||||
await fs.mkdir(path.dirname(packageEntryPath), { recursive: true });
|
||||
await fs.mkdir(path.dirname(packageCorePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "@clawdbot/lobster",
|
||||
type: "module",
|
||||
main: "./dist/src/sdk/index.js",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(packageEntryPath, "export {};\n", "utf8");
|
||||
await fs.writeFile(
|
||||
packageCorePath,
|
||||
[
|
||||
"export async function runToolRequest() {",
|
||||
" return { ok: true, status: 'ok', output: [{ source: 'fallback' }], requiresApproval: null };",
|
||||
"}",
|
||||
"export async function resumeToolRequest() {",
|
||||
" return { ok: true, status: 'cancelled', output: [], requiresApproval: null };",
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const runtime = await loadEmbeddedToolRuntimeFromPackage({
|
||||
importModule: async (specifier) => {
|
||||
if (specifier === "@clawdbot/lobster/core") {
|
||||
throw new Error("package export missing");
|
||||
}
|
||||
return (await import(`${specifier}?t=${Date.now()}`)) as object;
|
||||
},
|
||||
resolvePackageEntry: () => packageEntryPath,
|
||||
});
|
||||
|
||||
await expect(runtime.runToolRequest({ pipeline: "commands.list" })).resolves.toEqual({
|
||||
ok: true,
|
||||
status: "ok",
|
||||
output: [{ source: "fallback" }],
|
||||
requiresApproval: null,
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("requires a pipeline for run", async () => {
|
||||
const runner = createEmbeddedLobsterRunner({
|
||||
loadRuntime: vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import {
|
||||
resumeToolRequest as embeddedResumeToolRequest,
|
||||
runToolRequest as embeddedRunToolRequest,
|
||||
} from "@clawdbot/lobster/core";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
export type LobsterEnvelope =
|
||||
| {
|
||||
@@ -97,6 +96,45 @@ type EmbeddedToolRuntime = {
|
||||
|
||||
type LoadEmbeddedToolRuntime = () => Promise<EmbeddedToolRuntime>;
|
||||
|
||||
type LoadEmbeddedToolRuntimeFromPackageOptions = {
|
||||
importModule?: (specifier: string) => Promise<Partial<EmbeddedToolRuntime>>;
|
||||
resolvePackageEntry?: (specifier: string) => string;
|
||||
};
|
||||
|
||||
const lobsterRequire = createRequire(import.meta.url);
|
||||
|
||||
function toEmbeddedToolRuntime(
|
||||
moduleExports: Partial<EmbeddedToolRuntime>,
|
||||
source: string,
|
||||
): EmbeddedToolRuntime {
|
||||
const { runToolRequest, resumeToolRequest } = moduleExports;
|
||||
if (typeof runToolRequest === "function" && typeof resumeToolRequest === "function") {
|
||||
return { runToolRequest, resumeToolRequest };
|
||||
}
|
||||
throw new Error(`${source} does not export Lobster embedded runtime functions`);
|
||||
}
|
||||
|
||||
function findLobsterPackageRoot(resolvedEntryPath: string): string {
|
||||
let dir = path.dirname(resolvedEntryPath);
|
||||
while (true) {
|
||||
const packageJsonPath = path.join(dir, "package.json");
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { name?: string };
|
||||
if (parsed.name === "@clawdbot/lobster") {
|
||||
return dir;
|
||||
}
|
||||
} catch {
|
||||
// Keep walking until the installed package root is found.
|
||||
}
|
||||
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) {
|
||||
throw new Error(`Could not locate @clawdbot/lobster package root from ${resolvedEntryPath}`);
|
||||
}
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeForCwdSandbox(p: string): string {
|
||||
const normalized = path.normalize(p);
|
||||
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
||||
@@ -255,11 +293,39 @@ async function withTimeout<T>(
|
||||
});
|
||||
}
|
||||
|
||||
async function loadEmbeddedToolRuntimeFromPackage(): Promise<EmbeddedToolRuntime> {
|
||||
return {
|
||||
runToolRequest: embeddedRunToolRequest,
|
||||
resumeToolRequest: embeddedResumeToolRequest,
|
||||
};
|
||||
export async function loadEmbeddedToolRuntimeFromPackage(
|
||||
options: LoadEmbeddedToolRuntimeFromPackageOptions = {},
|
||||
): Promise<EmbeddedToolRuntime> {
|
||||
const importModule =
|
||||
options.importModule ??
|
||||
(async (specifier: string) => (await import(specifier)) as Partial<EmbeddedToolRuntime>);
|
||||
const resolvePackageEntry =
|
||||
options.resolvePackageEntry ?? ((specifier: string) => lobsterRequire.resolve(specifier));
|
||||
|
||||
let coreLoadError: unknown;
|
||||
try {
|
||||
const coreSpecifier = ["@clawdbot", "lobster", "core"].join("/");
|
||||
return toEmbeddedToolRuntime(await importModule(coreSpecifier), "@clawdbot/lobster/core");
|
||||
} catch (error) {
|
||||
coreLoadError = error;
|
||||
}
|
||||
|
||||
let fallbackLoadError: unknown;
|
||||
try {
|
||||
const packageEntryPath = resolvePackageEntry("@clawdbot/lobster");
|
||||
const packageRoot = findLobsterPackageRoot(packageEntryPath);
|
||||
const coreRuntimeUrl = pathToFileURL(path.join(packageRoot, "dist/src/core/index.js")).href;
|
||||
return toEmbeddedToolRuntime(await importModule(coreRuntimeUrl), coreRuntimeUrl);
|
||||
} catch (error) {
|
||||
fallbackLoadError = error;
|
||||
}
|
||||
|
||||
throw new Error("Failed to load the Lobster embedded runtime", {
|
||||
cause: new AggregateError(
|
||||
[coreLoadError, fallbackLoadError],
|
||||
"Both Lobster embedded runtime load paths failed",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function createEmbeddedLobsterRunner(options?: {
|
||||
|
||||
@@ -1593,7 +1593,7 @@
|
||||
"test:unit:fast:audit": "node scripts/test-unit-fast-audit.mjs",
|
||||
"test:voicecall:closedloop": "node scripts/test-voicecall-closedloop.mjs",
|
||||
"test:watch": "node scripts/test-projects.mjs --watch",
|
||||
"test:windows:ci": "node scripts/test-projects.mjs src/process/exec.windows.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts test/scripts/npm-runner.test.ts test/scripts/pnpm-runner.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"test:windows:ci": "node scripts/test-projects.mjs src/process/exec.windows.test.ts src/process/windows-command.test.ts src/infra/windows-install-roots.test.ts extensions/lobster/src/lobster-runner.test.ts test/scripts/npm-runner.test.ts test/scripts/pnpm-runner.test.ts test/scripts/ui.test.ts test/scripts/vitest-process-group.test.ts",
|
||||
"tool-display:check": "node --import tsx scripts/tool-display.ts --check",
|
||||
"tool-display:write": "node --import tsx scripts/tool-display.ts --write",
|
||||
"ts-topology": "node --import tsx scripts/ts-topology.ts",
|
||||
|
||||
@@ -3,8 +3,16 @@ import { closeSync, openSync, readSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
|
||||
|
||||
function getPortableBasename(value) {
|
||||
return value.split(/[/\\]/).at(-1) ?? value;
|
||||
}
|
||||
|
||||
function getPortableExtension(value) {
|
||||
return path.posix.extname(getPortableBasename(value)).toLowerCase();
|
||||
}
|
||||
|
||||
function isPnpmExecPath(value) {
|
||||
return /^pnpm(?:-cli)?(?:\.(?:[cm]?js|cmd|exe))?$/.test(path.basename(value).toLowerCase());
|
||||
return /^pnpm(?:-cli)?(?:\.(?:[cm]?js|cmd|exe))?$/.test(getPortableBasename(value).toLowerCase());
|
||||
}
|
||||
|
||||
function hasScriptShebang(value) {
|
||||
@@ -30,7 +38,7 @@ function isNodeRunnablePnpmExecPath(value) {
|
||||
if (!isPnpmExecPath(value)) {
|
||||
return false;
|
||||
}
|
||||
const extension = path.extname(value).toLowerCase();
|
||||
const extension = getPortableExtension(value);
|
||||
if (extension === ".js" || extension === ".cjs" || extension === ".mjs") {
|
||||
return true;
|
||||
}
|
||||
@@ -48,16 +56,31 @@ export function resolvePnpmRunner(params = {}) {
|
||||
const platform = params.platform ?? process.platform;
|
||||
const comSpec = params.comSpec ?? process.env.ComSpec ?? "cmd.exe";
|
||||
|
||||
if (
|
||||
typeof npmExecPath === "string" &&
|
||||
npmExecPath.length > 0 &&
|
||||
isNodeRunnablePnpmExecPath(npmExecPath)
|
||||
) {
|
||||
return {
|
||||
command: nodeExecPath,
|
||||
args: [...nodeArgs, npmExecPath, ...pnpmArgs],
|
||||
shell: false,
|
||||
};
|
||||
if (typeof npmExecPath === "string" && npmExecPath.length > 0 && isPnpmExecPath(npmExecPath)) {
|
||||
if (isNodeRunnablePnpmExecPath(npmExecPath)) {
|
||||
return {
|
||||
command: nodeExecPath,
|
||||
args: [...nodeArgs, npmExecPath, ...pnpmArgs],
|
||||
shell: false,
|
||||
};
|
||||
}
|
||||
|
||||
const npmExecExtension = getPortableExtension(npmExecPath);
|
||||
if (platform === "win32" && npmExecExtension === ".exe") {
|
||||
return {
|
||||
command: npmExecPath,
|
||||
args: pnpmArgs,
|
||||
shell: false,
|
||||
};
|
||||
}
|
||||
if (platform === "win32" && npmExecExtension === ".cmd") {
|
||||
return {
|
||||
command: comSpec,
|
||||
args: ["/d", "/s", "/c", buildCmdExeCommandLine(npmExecPath, pnpmArgs)],
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === "win32") {
|
||||
|
||||
@@ -92,6 +92,67 @@ describe("resolvePnpmRunner", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("executes pnpm.exe directly on Windows", () => {
|
||||
const npmExecPath =
|
||||
"C:\\Users\\test\\AppData\\Local\\pnpm\\.tools\\@pnpm+exe\\10.32.1\\node_modules\\@pnpm\\exe\\pnpm.exe";
|
||||
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
npmExecPath,
|
||||
nodeArgs: ["--no-maglev"],
|
||||
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
pnpmArgs: ["exec", "vitest", "run"],
|
||||
platform: "win32",
|
||||
}),
|
||||
).toEqual({
|
||||
command: npmExecPath,
|
||||
args: ["exec", "vitest", "run"],
|
||||
shell: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses pnpm.cjs through node for Windows-style paths", () => {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
npmExecPath:
|
||||
"C:\\Users\\test\\AppData\\Local\\node\\corepack\\v1\\pnpm\\10.32.1\\bin\\pnpm.cjs",
|
||||
nodeExecPath: "C:\\Program Files\\nodejs\\node.exe",
|
||||
pnpmArgs: ["exec", "vitest", "run"],
|
||||
platform: "win32",
|
||||
}),
|
||||
).toEqual({
|
||||
command: "C:\\Program Files\\nodejs\\node.exe",
|
||||
args: [
|
||||
"C:\\Users\\test\\AppData\\Local\\node\\corepack\\v1\\pnpm\\10.32.1\\bin\\pnpm.cjs",
|
||||
"exec",
|
||||
"vitest",
|
||||
"run",
|
||||
],
|
||||
shell: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("wraps an explicit pnpm.cmd path via cmd.exe on Windows", () => {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
npmExecPath: "C:\\Program Files\\pnpm\\pnpm.cmd",
|
||||
pnpmArgs: ["exec", "vitest", "run", "-t", "path with spaces"],
|
||||
platform: "win32",
|
||||
}),
|
||||
).toEqual({
|
||||
command: "C:\\Windows\\System32\\cmd.exe",
|
||||
args: [
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
'"C:\\Program Files\\pnpm\\pnpm.cmd" exec vitest run -t "path with spaces"',
|
||||
],
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to bare pnpm on non-Windows when npm_execpath is missing", () => {
|
||||
expect(
|
||||
resolvePnpmRunner({
|
||||
|
||||
Reference in New Issue
Block a user