diff --git a/src/infra/control-ui-assets.fs.runtime.ts b/src/infra/control-ui-assets.fs.runtime.ts new file mode 100644 index 00000000000..ab3a459a12a --- /dev/null +++ b/src/infra/control-ui-assets.fs.runtime.ts @@ -0,0 +1,6 @@ +import fs from "node:fs"; + +export const existsSync = fs.existsSync.bind(fs); +export const readFileSync = fs.readFileSync.bind(fs); +export const statSync = fs.statSync.bind(fs); +export const realpathSync = fs.realpathSync.bind(fs); diff --git a/src/infra/control-ui-assets.test.ts b/src/infra/control-ui-assets.test.ts index 79b94eb8f23..0d34aa98616 100644 --- a/src/infra/control-ui-assets.test.ts +++ b/src/infra/control-ui-assets.test.ts @@ -19,8 +19,8 @@ function setDir(p: string) { state.entries.set(abs(p), { kind: "dir" }); } -vi.mock("node:fs", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("./control-ui-assets.fs.runtime.js", async () => { + const actual = await import("node:fs"); const pathMod = await import("node:path"); const absInMock = (p: string) => pathMod.resolve(p); const fixturesRoot = `${absInMock("fixtures")}${pathMod.sep}`; @@ -31,7 +31,6 @@ vi.mock("node:fs", async (importOriginal) => { const readFixtureEntry = (p: string) => state.entries.get(absInMock(p)); const wrapped = { - ...actual, existsSync: (p: string) => isFixturePath(p) ? state.entries.has(absInMock(p)) : actual.existsSync(p), readFileSync: (p: string, encoding?: BufferEncoding) => { @@ -62,8 +61,7 @@ vi.mock("node:fs", async (importOriginal) => { ? (state.realpaths.get(absInMock(p)) ?? absInMock(p)) : actual.realpathSync(p), }; - - return { ...wrapped, default: wrapped }; + return wrapped; }); vi.mock("./openclaw-root.js", () => ({ diff --git a/src/infra/control-ui-assets.ts b/src/infra/control-ui-assets.ts index c9f05ff1c34..7e96b687769 100644 --- a/src/infra/control-ui-assets.ts +++ b/src/infra/control-ui-assets.ts @@ -1,8 +1,8 @@ -import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { runCommandWithTimeout } from "../process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import * as controlUiFsRuntime from "./control-ui-assets.fs.runtime.js"; import { resolveOpenClawPackageRoot, resolveOpenClawPackageRootSync } from "./openclaw-root.js"; const CONTROL_UI_DIST_PATH_SEGMENTS = ["dist", "control-ui", "index.html"] as const; @@ -31,7 +31,7 @@ export async function resolveControlUiDistIndexHealth( }); return { indexPath, - exists: Boolean(indexPath && fs.existsSync(indexPath)), + exists: Boolean(indexPath && controlUiFsRuntime.existsSync(indexPath)), }; } @@ -46,7 +46,7 @@ export function resolveControlUiRepoRoot( const srcIndex = parts.lastIndexOf("src"); if (srcIndex !== -1) { const root = parts.slice(0, srcIndex).join(path.sep); - if (fs.existsSync(path.join(root, "ui", "vite.config.ts"))) { + if (controlUiFsRuntime.existsSync(path.join(root, "ui", "vite.config.ts"))) { return root; } } @@ -54,8 +54,8 @@ export function resolveControlUiRepoRoot( let dir = path.dirname(normalized); for (let i = 0; i < 8; i++) { if ( - fs.existsSync(path.join(dir, "package.json")) && - fs.existsSync(path.join(dir, "ui", "vite.config.ts")) + controlUiFsRuntime.existsSync(path.join(dir, "package.json")) && + controlUiFsRuntime.existsSync(path.join(dir, "ui", "vite.config.ts")) ) { return dir; } @@ -81,7 +81,7 @@ export async function resolveControlUiDistIndexPath( const normalized = path.resolve(argv1); const entrypointCandidates = [normalized]; try { - const realpathEntrypoint = fs.realpathSync(normalized); + const realpathEntrypoint = controlUiFsRuntime.realpathSync(normalized); if (realpathEntrypoint !== normalized) { entrypointCandidates.push(realpathEntrypoint); } @@ -113,12 +113,12 @@ export async function resolveControlUiDistIndexPath( for (let i = 0; i < 8; i++) { const pkgJsonPath = path.join(dir, "package.json"); const indexPath = path.join(dir, "dist", "control-ui", "index.html"); - if (fs.existsSync(pkgJsonPath)) { + if (controlUiFsRuntime.existsSync(pkgJsonPath)) { try { - const raw = fs.readFileSync(pkgJsonPath, "utf-8"); + const raw = controlUiFsRuntime.readFileSync(pkgJsonPath, "utf-8"); const parsed = JSON.parse(raw) as { name?: unknown }; if (parsed.name === "openclaw") { - return fs.existsSync(indexPath) ? indexPath : null; + return controlUiFsRuntime.existsSync(indexPath) ? indexPath : null; } // Stop at the first package boundary to avoid resolving through unrelated ancestors. break; @@ -149,12 +149,12 @@ function pathsMatchByRealpathOrResolve(left: string, right: string): boolean { let realLeft: string; let realRight: string; try { - realLeft = fs.realpathSync(left); + realLeft = controlUiFsRuntime.realpathSync(left); } catch { realLeft = path.resolve(left); } try { - realRight = fs.realpathSync(right); + realRight = controlUiFsRuntime.realpathSync(right); } catch { realRight = path.resolve(right); } @@ -171,13 +171,13 @@ function addCandidate(candidates: Set, value: string | null) { export function resolveControlUiRootOverrideSync(rootOverride: string): string | null { const resolved = path.resolve(rootOverride); try { - const stats = fs.statSync(resolved); + const stats = controlUiFsRuntime.statSync(resolved); if (stats.isFile()) { return path.basename(resolved) === "index.html" ? path.dirname(resolved) : null; } if (stats.isDirectory()) { const indexPath = path.join(resolved, "index.html"); - return fs.existsSync(indexPath) ? resolved : null; + return controlUiFsRuntime.existsSync(indexPath) ? resolved : null; } } catch { return null; @@ -196,7 +196,7 @@ export function resolveControlUiRootSync(opts: ControlUiRootResolveOptions = {}) return null; } try { - return path.dirname(fs.realpathSync(path.resolve(argv1))); + return path.dirname(controlUiFsRuntime.realpathSync(path.resolve(argv1))); } catch { return null; } @@ -204,7 +204,7 @@ export function resolveControlUiRootSync(opts: ControlUiRootResolveOptions = {}) const execDir = (() => { try { const execPath = opts.execPath ?? process.execPath; - return path.dirname(fs.realpathSync(execPath)); + return path.dirname(controlUiFsRuntime.realpathSync(execPath)); } catch { return null; } @@ -243,7 +243,7 @@ export function resolveControlUiRootSync(opts: ControlUiRootResolveOptions = {}) for (const dir of candidates) { const indexPath = path.join(dir, "index.html"); - if (fs.existsSync(indexPath)) { + if (controlUiFsRuntime.existsSync(indexPath)) { return dir; } } @@ -312,12 +312,12 @@ export async function ensureControlUiAssetsBuilt( } const indexPath = resolveControlUiDistIndexPathForRoot(repoRoot); - if (fs.existsSync(indexPath)) { + if (controlUiFsRuntime.existsSync(indexPath)) { return { ok: true, built: false }; } const uiScript = path.join(repoRoot, "scripts", "ui.js"); - if (!fs.existsSync(uiScript)) { + if (!controlUiFsRuntime.existsSync(uiScript)) { return { ok: false, built: false, @@ -339,7 +339,7 @@ export async function ensureControlUiAssetsBuilt( }; } - if (!fs.existsSync(indexPath)) { + if (!controlUiFsRuntime.existsSync(indexPath)) { return { ok: false, built: true,