fix(ui): keep tmp-dir resolver browser-import safe

Defers the Node fs.constants lookup until tmp-dir resolution actually runs, adds browser-shim import regression coverage, and records the fix in the changelog.\n\nLocal verification:\n- pnpm test src/infra/tmp-openclaw-dir.browser-import.test.ts src/infra/tmp-openclaw-dir.test.ts src/logging/logger.browser-import.test.ts\n- pnpm test src/infra/run-node.test.ts -t "serializes runtime postbuild restaging|forwards wrapper SIGTERM"\n- pnpm build\n\nCo-authored-by: Valentinws <Valentinws@users.noreply.github.com>
This commit is contained in:
Valentinws
2026-04-25 08:02:10 +02:00
committed by GitHub
parent f5868ad1f8
commit 4a68fa3962
3 changed files with 70 additions and 1 deletions

View File

@@ -77,6 +77,7 @@ Docs: https://docs.openclaw.ai
- Telegram/agents: suppress the phantom "Agent couldn't generate a response" fallback after a reply was already delivered through the messaging tool on clean non-error terminal turns. (#70623) Thanks @chinar-amrutkar.
- Dashboard/security: avoid writing tokenized Control UI URLs or SSH hints to runtime logs, keeping gateway bearer fragments out of console-captured logs readable through `logs.tail`. (#70029) Thanks @Ziy1-Tan.
- Providers/OpenRouter: treat DeepSeek refs as cache-TTL eligible without injecting Anthropic cache-control markers, aligning context pruning with OpenRouter-managed prompt caching. (#51983) Thanks @QuinnH496.
- Control UI/browser: defer temp-dir access-mode constants until Node-only temp-dir resolution runs, preventing browser bundles from crashing when `node:fs` constants are stubbed. (#48930) Thanks @Valentinws.
- Discord/cron: deliver text-only isolated cron and heartbeat announce output from the canonical final assistant text once, avoiding duplicate Discord posts when streamed block payloads and the final answer contain the same content. Fixes #71406. Thanks @alexgross21.
- macOS Gateway: wait for launchd to reload the exited Gateway LaunchAgent before bootstrapping repair fallback, preventing config-triggered restarts from leaving the service not loaded. Fixes #45178. Thanks @vincentkoc.
- macOS Gateway: tolerate launchctl bootstrap's already-loaded exit during restart fallback and use non-killing kickstart after bootstrap, avoiding a second race that can unload the LaunchAgent. Fixes #41934. Thanks @zerone0x.

View File

@@ -0,0 +1,67 @@
import { Buffer } from "node:buffer";
import crypto from "node:crypto";
import { build, type Plugin } from "esbuild";
import { describe, expect, it } from "vitest";
describe("tmp-openclaw-dir browser-safe import", () => {
it("loads when a browser fs shim omits constants", async () => {
const resultKey = `__openclawTmpDirBrowserImport_${crypto.randomUUID().replaceAll("-", "_")}`;
const nodeShimPlugin: Plugin = {
name: "node-browser-shims",
setup(pluginBuild) {
pluginBuild.onResolve({ filter: /^node:(fs|os|path)$/ }, (args) => ({
path: args.path,
namespace: "node-browser-shim",
}));
pluginBuild.onLoad({ filter: /^node:fs$/, namespace: "node-browser-shim" }, () => ({
contents: "export default { constants: undefined };",
loader: "js",
}));
pluginBuild.onLoad({ filter: /^node:os$/, namespace: "node-browser-shim" }, () => ({
contents: 'export const tmpdir = () => "/tmp";',
loader: "js",
}));
pluginBuild.onLoad({ filter: /^node:path$/, namespace: "node-browser-shim" }, () => ({
contents: "export default { join: (...parts) => parts.join('/') };",
loader: "js",
}));
},
};
const bundled = await build({
bundle: true,
format: "esm",
platform: "browser",
plugins: [nodeShimPlugin],
stdin: {
contents: `
import { POSIX_OPENCLAW_TMP_DIR, resolvePreferredOpenClawTmpDir } from "./src/infra/tmp-openclaw-dir.ts";
globalThis.${resultKey} = {
posixTmpDir: POSIX_OPENCLAW_TMP_DIR,
resolverType: typeof resolvePreferredOpenClawTmpDir,
};
`,
loader: "ts",
resolveDir: process.cwd(),
sourcefile: "tmp-openclaw-dir-browser-entry.ts",
},
write: false,
});
const bundledSource = bundled.outputFiles[0]?.text;
expect(bundledSource).toBeTruthy();
await import(
`data:text/javascript;base64,${Buffer.from(bundledSource ?? "").toString("base64")}`
);
try {
expect((globalThis as Record<string, unknown>)[resultKey]).toEqual({
posixTmpDir: "/tmp/openclaw",
resolverType: "function",
});
} finally {
delete (globalThis as Record<string, unknown>)[resultKey];
}
});
});

View File

@@ -3,7 +3,6 @@ import { tmpdir as getOsTmpDir } from "node:os";
import path from "node:path";
export const POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw";
const TMP_DIR_ACCESS_MODE = fs.constants.W_OK | fs.constants.X_OK;
type ResolvePreferredOpenClawTmpDirOptions = {
accessSync?: (path: string, mode?: number) => void;
@@ -34,6 +33,8 @@ function isNodeErrorWithCode(err: unknown, code: string): err is MaybeNodeError
export function resolvePreferredOpenClawTmpDir(
options: ResolvePreferredOpenClawTmpDirOptions = {},
): string {
// Evaluated here (not at module load) so this file is safe to import in browser bundles.
const TMP_DIR_ACCESS_MODE = fs.constants.W_OK | fs.constants.X_OK;
const accessSync = options.accessSync ?? fs.accessSync;
const chmodSync = options.chmodSync ?? fs.chmodSync;
const lstatSync = options.lstatSync ?? fs.lstatSync;