mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
fix(gateway): accept fnm default path on Linux
This commit is contained in:
@@ -85,6 +85,7 @@ Docs: https://docs.openclaw.ai
|
||||
when the local control service is missing. Fixes #66637.
|
||||
- Gateway/update: fail package updates when the restarted managed gateway reports the wrong version, avoiding false-success mixed-version restarts after macOS LaunchAgent updates. Fixes #71835. Thanks @abhinas90 and @jsompis.
|
||||
- Plugins/runtime deps: surface activated plugin load failures in health and fail package-update restart verification or doctor repair when bundled runtime deps still cannot load, avoiding false-success repairs. (#71883) Thanks @Solvely-Colin.
|
||||
- Gateway/Linux: include fnm `aliases/default/bin` in generated service PATHs and let doctor accept either modern fnm aliases or the legacy `current/bin` symlink, avoiding false PATH repair prompts. Fixes #68169. Thanks @richard-scott.
|
||||
- WhatsApp: remove ack reactions after a visible reply when `messages.removeAckAfterReply` is enabled, matching other reaction-capable channels. Fixes #26183. Thanks @MrUnforsaken.
|
||||
- Providers/Z.AI: map OpenClaw thinking controls to Z.AI's `thinking` payload and add opt-in preserved thinking replay via `params.preserveThinking`, so GLM 5.x can keep prior `reasoning_content` when requested. Fixes #58680. Thanks @xuanmingguo.
|
||||
- Channels/status: keep read-only channel lists on manifest and package metadata by default, loading setup runtime only for explicit fallback callers. Thanks @shakkernerd.
|
||||
|
||||
@@ -112,6 +112,44 @@ describe("auditGatewayServiceConfig", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts Linux fnm aliases/default without requiring the legacy current symlink", async () => {
|
||||
const env = { HOME: "/home/testuser", FNM_DIR: "/home/testuser/.local/share/fnm" };
|
||||
const pathParts = buildMinimalServicePath({ platform: "linux", env })
|
||||
.split(":")
|
||||
.filter((entry) => !entry.includes("/fnm/current/bin"));
|
||||
const audit = await auditGatewayServiceConfig({
|
||||
env,
|
||||
platform: "linux",
|
||||
command: {
|
||||
programArguments: ["/usr/bin/node", "gateway"],
|
||||
environment: { PATH: pathParts.join(":") },
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts Linux fnm current symlink without requiring aliases/default", async () => {
|
||||
const env = { HOME: "/home/testuser", FNM_DIR: "/home/testuser/.local/share/fnm" };
|
||||
const pathParts = buildMinimalServicePath({ platform: "linux", env })
|
||||
.split(":")
|
||||
.filter((entry) => !entry.includes("/fnm/aliases/default/bin"));
|
||||
const audit = await auditGatewayServiceConfig({
|
||||
env,
|
||||
platform: "linux",
|
||||
command: {
|
||||
programArguments: ["/usr/bin/node", "gateway"],
|
||||
environment: { PATH: pathParts.join(":") },
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
audit.issues.some((issue) => issue.code === SERVICE_AUDIT_CODES.gatewayPathMissingDirs),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("flags gateway token mismatch when service token is stale", async () => {
|
||||
const audit = await createGatewayAudit({
|
||||
expectedGatewayToken: "new-token",
|
||||
|
||||
@@ -260,6 +260,26 @@ function normalizePathEntry(entry: string, platform: NodeJS.Platform): string {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function getEquivalentMinimalPathEntries(
|
||||
entry: string,
|
||||
platform: NodeJS.Platform,
|
||||
normalizedExpected: Set<string>,
|
||||
): string[] {
|
||||
if (platform !== "linux") {
|
||||
return [];
|
||||
}
|
||||
const equivalent = entry.endsWith("/aliases/default/bin")
|
||||
? `${entry.slice(0, -"/aliases/default/bin".length)}/current/bin`
|
||||
: entry.endsWith("/current/bin")
|
||||
? `${entry.slice(0, -"/current/bin".length)}/aliases/default/bin`
|
||||
: undefined;
|
||||
if (!equivalent) {
|
||||
return [];
|
||||
}
|
||||
const normalizedEquivalent = normalizePathEntry(equivalent, platform);
|
||||
return normalizedExpected.has(normalizedEquivalent) ? [equivalent] : [];
|
||||
}
|
||||
|
||||
function auditGatewayServicePath(
|
||||
command: GatewayServiceCommand,
|
||||
issues: ServiceConfigIssue[],
|
||||
@@ -288,7 +308,12 @@ function auditGatewayServicePath(
|
||||
const normalizedExpected = new Set(expected.map((entry) => normalizePathEntry(entry, platform)));
|
||||
const missing = expected.filter((entry) => {
|
||||
const normalized = normalizePathEntry(entry, platform);
|
||||
return !normalizedParts.has(normalized);
|
||||
if (normalizedParts.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return !getEquivalentMinimalPathEntries(entry, platform, normalizedExpected).some(
|
||||
(equivalent) => normalizedParts.has(normalizePathEntry(equivalent, platform)),
|
||||
);
|
||||
});
|
||||
if (missing.length > 0) {
|
||||
issues.push({
|
||||
|
||||
@@ -24,6 +24,9 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
|
||||
expect(result).toContain("/home/testuser/.npm-global/bin");
|
||||
expect(result).toContain("/home/testuser/bin");
|
||||
expect(result).toContain("/home/testuser/.nvm/current/bin");
|
||||
expect(result).toContain("/home/testuser/.local/share/fnm/aliases/default/bin");
|
||||
expect(result).toContain("/home/testuser/.local/share/fnm/current/bin");
|
||||
expect(result).toContain("/home/testuser/.fnm/aliases/default/bin");
|
||||
expect(result).toContain("/home/testuser/.fnm/current/bin");
|
||||
expect(result).toContain("/home/testuser/.volta/bin");
|
||||
expect(result).toContain("/home/testuser/.asdf/shims");
|
||||
@@ -96,6 +99,7 @@ describe("getMinimalServicePathParts - Linux user directories", () => {
|
||||
expect(result).toContain("/opt/volta/bin");
|
||||
expect(result).toContain("/opt/asdf/shims");
|
||||
expect(result).toContain("/opt/nvm/current/bin");
|
||||
expect(result).toContain("/opt/fnm/aliases/default/bin");
|
||||
expect(result).toContain("/opt/fnm/current/bin");
|
||||
});
|
||||
|
||||
@@ -302,6 +306,7 @@ describe("buildMinimalServicePath", () => {
|
||||
expect(parts).toContain("/home/alice/.local/bin");
|
||||
expect(parts).toContain("/home/alice/.npm-global/bin");
|
||||
expect(parts).toContain("/home/alice/.nvm/current/bin");
|
||||
expect(parts).toContain("/home/alice/.local/share/fnm/aliases/default/bin");
|
||||
|
||||
// Verify system directories are also included
|
||||
expect(parts).toContain("/usr/local/bin");
|
||||
|
||||
@@ -197,6 +197,7 @@ export function resolveLinuxUserBinDirs(
|
||||
// Env-configured bin roots (override defaults when present).
|
||||
addCommonEnvConfiguredBinDirs(dirs, env);
|
||||
addNonEmptyDir(dirs, appendSubdir(env?.NVM_DIR, "current/bin"));
|
||||
addNonEmptyDir(dirs, appendSubdir(env?.FNM_DIR, "aliases/default/bin"));
|
||||
addNonEmptyDir(dirs, appendSubdir(env?.FNM_DIR, "current/bin"));
|
||||
|
||||
// Common user bin directories
|
||||
@@ -207,7 +208,10 @@ export function resolveLinuxUserBinDirs(
|
||||
|
||||
// Node version managers
|
||||
dirs.push(`${home}/.nvm/current/bin`); // nvm with current symlink
|
||||
dirs.push(`${home}/.fnm/current/bin`); // fnm
|
||||
dirs.push(`${home}/.local/share/fnm/aliases/default/bin`); // fnm default
|
||||
dirs.push(`${home}/.local/share/fnm/current/bin`); // fnm legacy current symlink
|
||||
dirs.push(`${home}/.fnm/aliases/default/bin`); // fnm if customized to ~/.fnm
|
||||
dirs.push(`${home}/.fnm/current/bin`); // fnm legacy current symlink
|
||||
dirs.push(`${home}/.local/share/pnpm`); // pnpm global bin
|
||||
|
||||
return dirs;
|
||||
|
||||
Reference in New Issue
Block a user