diff --git a/extensions/browser/src/infra/fs-safe.ts b/extensions/browser/src/infra/fs-safe.ts deleted file mode 100644 index 9b90290a2a1..00000000000 --- a/extensions/browser/src/infra/fs-safe.ts +++ /dev/null @@ -1 +0,0 @@ -export { root, FsSafeError } from "../sdk-security-runtime.js"; diff --git a/extensions/browser/src/infra/path-guards.ts b/extensions/browser/src/infra/path-guards.ts deleted file mode 100644 index 1d790b633f8..00000000000 --- a/extensions/browser/src/infra/path-guards.ts +++ /dev/null @@ -1 +0,0 @@ -export { isNotFoundPathError, isPathInside } from "../sdk-security-runtime.js"; diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index 1b28a4e972e..43f975f35f4 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -2294,8 +2294,6 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { return; case "session.long_running": case "session.stalled": - case "session.recovery.completed": - case "session.recovery.requested": return; case "session.stuck": recordSessionStuck(evt); diff --git a/extensions/voice-call/src/realtime-fast-context.test.ts b/extensions/voice-call/src/realtime-fast-context.test.ts index 56724b6a9bd..8b3b747893b 100644 --- a/extensions/voice-call/src/realtime-fast-context.test.ts +++ b/extensions/voice-call/src/realtime-fast-context.test.ts @@ -3,11 +3,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { VoiceCallRealtimeFastContextConfig } from "./config.js"; const mocks = vi.hoisted(() => ({ - getActiveMemorySearchManager: vi.fn(), + resolveRealtimeVoiceFastContextConsult: vi.fn(), })); -vi.mock("../../../src/plugins/memory-runtime.js", () => ({ - getActiveMemorySearchManager: mocks.getActiveMemorySearchManager, +vi.mock("openclaw/plugin-sdk/realtime-voice", () => ({ + resolveRealtimeVoiceFastContextConsult: mocks.resolveRealtimeVoiceFastContextConsult, })); import { resolveRealtimeFastContextConsult } from "./realtime-fast-context.js"; @@ -36,16 +36,16 @@ function createLogger() { describe("resolveRealtimeFastContextConsult", () => { beforeEach(() => { - mocks.getActiveMemorySearchManager.mockReset(); + mocks.resolveRealtimeVoiceFastContextConsult.mockReset(); }); afterEach(() => { vi.useRealTimers(); }); - it("falls back to the full consult when memory manager setup fails", async () => { + it("passes voice-call labels into the SDK fast context resolver", async () => { const logger = createLogger(); - mocks.getActiveMemorySearchManager.mockRejectedValue(new Error("memory misconfigured")); + mocks.resolveRealtimeVoiceFastContextConsult.mockResolvedValue({ handled: false }); await expect( resolveRealtimeFastContextConsult({ @@ -58,31 +58,17 @@ describe("resolveRealtimeFastContextConsult", () => { }), ).resolves.toEqual({ handled: false }); - expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("memory misconfigured")); - }); - - it("returns a bounded miss when memory manager setup exceeds the fast context timeout", async () => { - vi.useFakeTimers(); - const logger = createLogger(); - mocks.getActiveMemorySearchManager.mockReturnValue(new Promise(() => {})); - - const resultPromise = resolveRealtimeFastContextConsult({ + expect(mocks.resolveRealtimeVoiceFastContextConsult).toHaveBeenCalledWith({ cfg, agentId: "main", sessionKey: "voice:15550001234", - config: createFastContextConfig({ fallbackToConsult: false, timeoutMs: 25 }), + config: createFastContextConfig({ fallbackToConsult: true }), args: { question: "What do you remember?" }, logger, - }); - - await vi.advanceTimersByTimeAsync(25); - - await expect(resultPromise).resolves.toEqual({ - handled: true, - result: { - text: expect.stringContaining("No relevant OpenClaw memory or session context"), + labels: { + audienceLabel: "caller", + contextName: "OpenClaw memory or session context", }, }); - expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("timed out after 25ms")); }); }); diff --git a/src/agents/bash-tools.exec.script-preflight.test.ts b/src/agents/bash-tools.exec.script-preflight.test.ts index c21bae838a9..8969f75ea64 100644 --- a/src/agents/bash-tools.exec.script-preflight.test.ts +++ b/src/agents/bash-tools.exec.script-preflight.test.ts @@ -1,8 +1,8 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; +import { __setFsSafeTestHooksForTest } from "@openclaw/fs-safe/test-hooks"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { __setFsSafeTestHooksForTest } from "../infra/fs-safe-test-hooks.js"; import { withTempDir } from "../test-utils/temp-dir.js"; import { __testing, createExecTool } from "./bash-tools.exec.js"; diff --git a/src/infra/archive-staging.test.ts b/src/infra/archive-staging.test.ts deleted file mode 100644 index 758a6c7fd37..00000000000 --- a/src/infra/archive-staging.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import fs from "node:fs/promises"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { withTempDir } from "../test-helpers/temp-dir.js"; -import { - ArchiveSecurityError, - createArchiveSymlinkTraversalError, - mergeExtractedTreeIntoDestination, - prepareArchiveDestinationDir, - prepareArchiveOutputPath, - withStagedArchiveDestination, -} from "./archive-staging.js"; - -const directorySymlinkType = process.platform === "win32" ? "junction" : undefined; - -describe("archive-staging helpers", () => { - it("accepts real destination directories and returns their real path", async () => { - await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => { - const destDir = path.join(rootDir, "dest"); - await fs.mkdir(destDir, { recursive: true }); - - await expect(prepareArchiveDestinationDir(destDir)).resolves.toBe(await fs.realpath(destDir)); - }); - }); - - it.runIf(process.platform !== "win32")( - "rejects symlink and non-directory archive destinations", - async () => { - await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => { - const realDestDir = path.join(rootDir, "real-dest"); - const symlinkDestDir = path.join(rootDir, "dest-link"); - const fileDest = path.join(rootDir, "dest.txt"); - await fs.mkdir(realDestDir, { recursive: true }); - await fs.symlink(realDestDir, symlinkDestDir, directorySymlinkType); - await fs.writeFile(fileDest, "nope", "utf8"); - - await expect(prepareArchiveDestinationDir(symlinkDestDir)).rejects.toMatchObject({ - code: "destination-symlink", - } satisfies Partial); - await expect(prepareArchiveDestinationDir(fileDest)).rejects.toMatchObject({ - code: "destination-not-directory", - } satisfies Partial); - }); - }, - ); - - it("creates in-destination parent directories for file outputs", async () => { - await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => { - const destDir = path.join(rootDir, "dest"); - await fs.mkdir(destDir, { recursive: true }); - const destinationRealDir = await prepareArchiveDestinationDir(destDir); - const outPath = path.join(destDir, "nested", "payload.txt"); - - await expect( - prepareArchiveOutputPath({ - destinationDir: destDir, - destinationRealDir, - relPath: "nested/payload.txt", - outPath, - originalPath: "nested/payload.txt", - isDirectory: false, - }), - ).resolves.toBeUndefined(); - - await expect(fs.stat(path.dirname(outPath))).resolves.toMatchObject({ - isDirectory: expect.any(Function), - }); - }); - }); - - it.runIf(process.platform !== "win32")( - "rejects output paths that traverse a destination symlink", - async () => { - await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => { - const destDir = path.join(rootDir, "dest"); - const outsideDir = path.join(rootDir, "outside"); - const linkDir = path.join(destDir, "escape"); - await fs.mkdir(destDir, { recursive: true }); - await fs.mkdir(outsideDir, { recursive: true }); - await fs.symlink(outsideDir, linkDir, directorySymlinkType); - const destinationRealDir = await prepareArchiveDestinationDir(destDir); - - await expect( - prepareArchiveOutputPath({ - destinationDir: destDir, - destinationRealDir, - relPath: "escape/payload.txt", - outPath: path.join(linkDir, "payload.txt"), - originalPath: "escape/payload.txt", - isDirectory: false, - }), - ).rejects.toMatchObject({ - code: "destination-symlink-traversal", - } satisfies Partial); - }); - }, - ); - - it("cleans up staged archive directories after success and failure", async () => { - await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => { - const destDir = path.join(rootDir, "dest"); - await fs.mkdir(destDir, { recursive: true }); - const destinationRealDir = await prepareArchiveDestinationDir(destDir); - let successStage = ""; - - await withStagedArchiveDestination({ - destinationRealDir, - run: async (stagingDir) => { - successStage = stagingDir; - await fs.writeFile(path.join(stagingDir, "payload.txt"), "ok", "utf8"); - }, - }); - await expect(fs.stat(successStage)).rejects.toMatchObject({ code: "ENOENT" }); - - let failureStage = ""; - await expect( - withStagedArchiveDestination({ - destinationRealDir, - run: async (stagingDir) => { - failureStage = stagingDir; - throw new Error("boom"); - }, - }), - ).rejects.toThrow("boom"); - await expect(fs.stat(failureStage)).rejects.toMatchObject({ code: "ENOENT" }); - }); - }); - - it.runIf(process.platform !== "win32")( - "merges staged trees and rejects symlink entries from the source", - async () => { - await withTempDir({ prefix: "openclaw-archive-staging-" }, async (rootDir) => { - const sourceDir = path.join(rootDir, "source"); - const sourceNestedDir = path.join(sourceDir, "nested"); - const destDir = path.join(rootDir, "dest"); - const outsideDir = path.join(rootDir, "outside"); - await fs.mkdir(sourceNestedDir, { recursive: true }); - await fs.mkdir(destDir, { recursive: true }); - await fs.mkdir(outsideDir, { recursive: true }); - await fs.writeFile(path.join(sourceNestedDir, "payload.txt"), "hi", "utf8"); - - const destinationRealDir = await prepareArchiveDestinationDir(destDir); - await mergeExtractedTreeIntoDestination({ - sourceDir, - destinationDir: destDir, - destinationRealDir, - }); - await expect( - fs.readFile(path.join(destDir, "nested", "payload.txt"), "utf8"), - ).resolves.toBe("hi"); - - await fs.symlink(outsideDir, path.join(sourceDir, "escape"), directorySymlinkType); - await expect( - mergeExtractedTreeIntoDestination({ - sourceDir, - destinationDir: destDir, - destinationRealDir, - }), - ).rejects.toMatchObject({ - code: "destination-symlink-traversal", - } satisfies Partial); - }); - }, - ); - - it("builds a typed archive symlink traversal error", () => { - const error = createArchiveSymlinkTraversalError("nested/payload.txt"); - expect(error).toBeInstanceOf(ArchiveSecurityError); - expect(error.code).toBe("destination-symlink-traversal"); - expect(error.message).toContain("nested/payload.txt"); - }); -}); diff --git a/src/infra/archive-staging.ts b/src/infra/archive-staging.ts deleted file mode 100644 index f4896b95d99..00000000000 --- a/src/infra/archive-staging.ts +++ /dev/null @@ -1,10 +0,0 @@ -import "./fs-safe-defaults.js"; -export { - ArchiveSecurityError, - createArchiveSymlinkTraversalError, - mergeExtractedTreeIntoDestination, - prepareArchiveDestinationDir, - prepareArchiveOutputPath, - withStagedArchiveDestination, - type ArchiveSecurityErrorCode, -} from "@openclaw/fs-safe/archive"; diff --git a/src/infra/fs-safe-test-hooks.ts b/src/infra/fs-safe-test-hooks.ts deleted file mode 100644 index 8bb89861a43..00000000000 --- a/src/infra/fs-safe-test-hooks.ts +++ /dev/null @@ -1,2 +0,0 @@ -import "./fs-safe-defaults.js"; -export { __setFsSafeTestHooksForTest, type FsSafeTestHooks } from "@openclaw/fs-safe/test-hooks"; diff --git a/src/infra/fs-safe.test.ts b/src/infra/fs-safe.test.ts index fa7ed242570..419ebb1edcd 100644 --- a/src/infra/fs-safe.test.ts +++ b/src/infra/fs-safe.test.ts @@ -1,13 +1,13 @@ import type { FileHandle } from "node:fs/promises"; import fs from "node:fs/promises"; import path from "node:path"; +import { __setFsSafeTestHooksForTest } from "@openclaw/fs-safe/test-hooks"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createRebindableDirectoryAlias, withRealpathSymlinkRebindRace, } from "../test-utils/symlink-rebind-race.js"; import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js"; -import { __setFsSafeTestHooksForTest } from "./fs-safe-test-hooks.js"; import { resolveOpenedFileRealPathForHandle, FsSafeError, diff --git a/src/media-understanding/media-understanding-misc.test.ts b/src/media-understanding/media-understanding-misc.test.ts index c5e953f1098..ef01552c60e 100644 --- a/src/media-understanding/media-understanding-misc.test.ts +++ b/src/media-understanding/media-understanding-misc.test.ts @@ -2,6 +2,7 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import * as fsSafe from "../infra/fs-safe.js"; import { withTempDir } from "../test-helpers/temp-dir.js"; import { withFetchPreconnect } from "../test-utils/fetch-mock.js"; import { MediaAttachmentCache } from "./attachments.js"; @@ -222,13 +223,11 @@ describe("media understanding attachments SSRF", () => { const cache = new MediaAttachmentCache([{ index: 0, path: attachmentPath }], { localPathRoots: [allowedRoot], }); - const originalRealpath = fs.realpath.bind(fs); - - vi.spyOn(fs, "realpath").mockImplementation(async (candidatePath) => { - if (String(candidatePath) === attachmentPath) { + vi.spyOn(fsSafe, "openLocalFileSafely").mockImplementation(async (params) => { + if (params.filePath === attachmentPath) { throw new Error("EACCES"); } - return await originalRealpath(candidatePath); + throw new Error(`Unexpected attachment path: ${params.filePath}`); }); await expect( diff --git a/src/shared/process-scoped-map.test.ts b/src/shared/process-scoped-map.test.ts deleted file mode 100644 index d4a0ff17a59..00000000000 --- a/src/shared/process-scoped-map.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import { resolveProcessScopedMap } from "./process-scoped-map.js"; - -const MAP_KEY = Symbol("process-scoped-map:test"); -const OTHER_MAP_KEY = Symbol("process-scoped-map:other"); - -afterEach(() => { - delete (process as unknown as Record)[MAP_KEY]; - delete (process as unknown as Record)[OTHER_MAP_KEY]; -}); - -describe("shared/process-scoped-map", () => { - it("reuses the same map for the same symbol", () => { - const first = resolveProcessScopedMap(MAP_KEY); - first.set("a", 1); - - const second = resolveProcessScopedMap(MAP_KEY); - - expect(second).toBe(first); - expect(second.get("a")).toBe(1); - }); - - it("keeps distinct maps for distinct symbols", () => { - const first = resolveProcessScopedMap(MAP_KEY); - const second = resolveProcessScopedMap(OTHER_MAP_KEY); - - expect(second).not.toBe(first); - }); - - it("reuses a prepopulated process map without replacing it", () => { - const existing = new Map([["a", 1]]); - (process as unknown as Record)[MAP_KEY] = existing; - - const resolved = resolveProcessScopedMap(MAP_KEY); - - expect(resolved).toBe(existing); - expect(resolved.get("a")).toBe(1); - }); -}); diff --git a/src/shared/process-scoped-map.ts b/src/shared/process-scoped-map.ts deleted file mode 100644 index 8235ba66a10..00000000000 --- a/src/shared/process-scoped-map.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function resolveProcessScopedMap(key: symbol): Map { - const proc = process as NodeJS.Process & { - [symbolKey: symbol]: Map | undefined; - }; - const existing = proc[key]; - if (existing) { - return existing; - } - const created = new Map(); - proc[key] = created; - return created; -}