fix(daemon): guard preferred node selection

This commit is contained in:
Sebastian
2026-02-17 10:01:37 -05:00
parent 3f66280c3c
commit 11fcbadec8
3 changed files with 34 additions and 1 deletions

View File

@@ -74,6 +74,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Update: preserve update.run restart delivery context so post-update status replies route back to the initiating channel/thread. (#18267) Thanks @yinghaosang.
- CLI/Update: run a standalone restart helper after updates, honoring service-name overrides and reporting restart initiation separately from confirmed restarts. (#18050)
- CLI/Daemon: warn when a gateway restart sees a stale service token so users can reinstall with `openclaw gateway install --force`, and skip drift warnings for non-gateway service restarts. (#18018)
- CLI/Daemon: prefer the active version-manager Node when installing daemons and include macOS version-manager bin directories in the service PATH so launchd services resolve user-managed runtimes.
- CLI/Status: fix `openclaw status --all` token summaries for bot-token-only channels so Mattermost/Zalo no longer show a bot+app warning. (#18527) Thanks @echo931.
- CLI/Configure: make the `/model picker` allowlist prompt searchable with tokenized matching in `openclaw configure` so users can filter huge model lists by typing terms like `gpt-5.2 openai/`. (#19010) Thanks @bjesuiter.
- CLI/Message: preserve `--components` JSON payloads in `openclaw message send` so Discord component payloads are no longer dropped. (#18222) Thanks @saurabhchopade.

View File

@@ -70,6 +70,31 @@ describe("resolvePreferredNodePath", () => {
expect(execFile).toHaveBeenCalledTimes(2);
});
it("ignores execPath when it is not node", async () => {
fsMocks.access.mockImplementation(async (target: string) => {
if (target === darwinNode) {
return;
}
throw new Error("missing");
});
const execFile = vi.fn().mockResolvedValue({ stdout: "22.12.0\n", stderr: "" });
const result = await resolvePreferredNodePath({
env: {},
runtime: "node",
platform: "darwin",
execFile,
execPath: "/Users/test/.bun/bin/bun",
});
expect(result).toBe(darwinNode);
expect(execFile).toHaveBeenCalledTimes(1);
expect(execFile).toHaveBeenCalledWith(darwinNode, ["-p", "process.versions.node"], {
encoding: "utf8",
});
});
it("uses system node when it meets the minimum version", async () => {
fsMocks.access.mockImplementation(async (target: string) => {
if (target === darwinNode) {

View File

@@ -19,6 +19,12 @@ function getPathModule(platform: NodeJS.Platform) {
return platform === "win32" ? path.win32 : path.posix;
}
function isNodeExecPath(execPath: string, platform: NodeJS.Platform): boolean {
const pathModule = getPathModule(platform);
const base = pathModule.basename(execPath).toLowerCase();
return base === "node" || base === "node.exe";
}
function normalizeForCompare(input: string, platform: NodeJS.Platform): string {
const pathModule = getPathModule(platform);
const normalized = pathModule.normalize(input).replaceAll("\\", "/");
@@ -160,8 +166,9 @@ export async function resolvePreferredNodePath(params: {
// Prefer the node that is currently running `openclaw gateway install`.
// This respects the user's active version manager (fnm/nvm/volta/etc.).
const platform = params.platform ?? process.platform;
const currentExecPath = params.execPath ?? process.execPath;
if (currentExecPath) {
if (currentExecPath && isNodeExecPath(currentExecPath, platform)) {
const execFileImpl = params.execFile ?? execFileAsync;
const version = await resolveNodeVersion(currentExecPath, execFileImpl);
if (isSupportedNodeVersion(version)) {