diff --git a/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts b/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts index 0b766e003f4..895cbece13a 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.stages-inbound-media-into-sandbox-workspace.test.ts @@ -26,22 +26,40 @@ afterEach(() => { childProcessMocks.spawn.mockClear(); }); +function setupSandboxWorkspace(home: string): { + cfg: ReturnType; + workspaceDir: string; + sandboxDir: string; +} { + const cfg = createSandboxMediaStageConfig(home); + const workspaceDir = join(home, "openclaw"); + const sandboxDir = join(home, "sandboxes", "session"); + vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({ + workspaceDir: sandboxDir, + containerWorkdir: "/work", + }); + return { cfg, workspaceDir, sandboxDir }; +} + +async function writeInboundMedia( + home: string, + fileName: string, + payload: string | Buffer, +): Promise { + const inboundDir = join(home, ".openclaw", "media", "inbound"); + await fs.mkdir(inboundDir, { recursive: true }); + const mediaPath = join(inboundDir, fileName); + await fs.writeFile(mediaPath, payload); + return mediaPath; +} + describe("stageSandboxMedia", () => { it("stages allowed media and blocks unsafe paths", async () => { await withSandboxMediaTempHome("openclaw-triggers-", async (home) => { - const cfg = createSandboxMediaStageConfig(home); - const workspaceDir = join(home, "openclaw"); - const sandboxDir = join(home, "sandboxes", "session"); - vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({ - workspaceDir: sandboxDir, - containerWorkdir: "/work", - }); + const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home); { - const inboundDir = join(home, ".openclaw", "media", "inbound"); - await fs.mkdir(inboundDir, { recursive: true }); - const mediaPath = join(inboundDir, "photo.jpg"); - await fs.writeFile(mediaPath, "test"); + const mediaPath = await writeInboundMedia(home, "photo.jpg", "test"); const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath); await stageSandboxMedia({ @@ -105,18 +123,9 @@ describe("stageSandboxMedia", () => { it("blocks destination symlink escapes when staging into sandbox workspace", async () => { await withSandboxMediaTempHome("openclaw-triggers-", async (home) => { - const cfg = createSandboxMediaStageConfig(home); - const workspaceDir = join(home, "openclaw"); - const sandboxDir = join(home, "sandboxes", "session"); - vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({ - workspaceDir: sandboxDir, - containerWorkdir: "/work", - }); + const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home); - const inboundDir = join(home, ".openclaw", "media", "inbound"); - await fs.mkdir(inboundDir, { recursive: true }); - const mediaPath = join(inboundDir, "payload.txt"); - await fs.writeFile(mediaPath, "PAYLOAD"); + const mediaPath = await writeInboundMedia(home, "payload.txt", "PAYLOAD"); const outsideDir = join(home, "outside"); const outsideInboundDir = join(outsideDir, "inbound"); @@ -145,18 +154,13 @@ describe("stageSandboxMedia", () => { it("skips oversized media staging and keeps original media paths", async () => { await withSandboxMediaTempHome("openclaw-triggers-", async (home) => { - const cfg = createSandboxMediaStageConfig(home); - const workspaceDir = join(home, "openclaw"); - const sandboxDir = join(home, "sandboxes", "session"); - vi.mocked(ensureSandboxWorkspaceForSession).mockResolvedValue({ - workspaceDir: sandboxDir, - containerWorkdir: "/work", - }); + const { cfg, workspaceDir, sandboxDir } = setupSandboxWorkspace(home); - const inboundDir = join(home, ".openclaw", "media", "inbound"); - await fs.mkdir(inboundDir, { recursive: true }); - const mediaPath = join(inboundDir, "oversized.bin"); - await fs.writeFile(mediaPath, Buffer.alloc(MEDIA_MAX_BYTES + 1, 0x41)); + const mediaPath = await writeInboundMedia( + home, + "oversized.bin", + Buffer.alloc(MEDIA_MAX_BYTES + 1, 0x41), + ); const { ctx, sessionCtx } = createSandboxMediaContexts(mediaPath); await stageSandboxMedia({ diff --git a/src/pairing/pairing-store.ts b/src/pairing/pairing-store.ts index a39b90b8f47..b7840d33181 100644 --- a/src/pairing/pairing-store.ts +++ b/src/pairing/pairing-store.ts @@ -30,6 +30,7 @@ type AllowFromReadCacheEntry = { size: number | null; entries: string[]; }; +type AllowFromStatLike = { mtimeMs: number; size: number } | null; const allowFromReadCache = new Map(); @@ -321,6 +322,31 @@ function resolveAllowFromReadCacheHit(params: { return cloneAllowFromCacheEntry(cached); } +function resolveAllowFromReadCacheOrMissing( + filePath: string, + stat: AllowFromStatLike, +): { entries: string[]; exists: boolean } | null { + const cached = resolveAllowFromReadCacheHit({ + filePath, + exists: Boolean(stat), + mtimeMs: stat?.mtimeMs ?? null, + size: stat?.size ?? null, + }); + if (cached) { + return { entries: cached.entries, exists: cached.exists }; + } + if (!stat) { + setAllowFromReadCache(filePath, { + exists: false, + mtimeMs: null, + size: null, + entries: [], + }); + return { entries: [], exists: false }; + } + return null; +} + async function readAllowFromStateForPathWithExists( channel: PairingChannel, filePath: string, @@ -335,24 +361,9 @@ async function readAllowFromStateForPathWithExists( } } - const cached = resolveAllowFromReadCacheHit({ - filePath, - exists: Boolean(stat), - mtimeMs: stat?.mtimeMs ?? null, - size: stat?.size ?? null, - }); - if (cached) { - return { entries: cached.entries, exists: cached.exists }; - } - - if (!stat) { - setAllowFromReadCache(filePath, { - exists: false, - mtimeMs: null, - size: null, - entries: [], - }); - return { entries: [], exists: false }; + const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat); + if (cachedOrMissing) { + return cachedOrMissing; } const { value, exists } = await readJsonFile(filePath, { @@ -387,24 +398,9 @@ function readAllowFromStateForPathSyncWithExists( } } - const cached = resolveAllowFromReadCacheHit({ - filePath, - exists: Boolean(stat), - mtimeMs: stat?.mtimeMs ?? null, - size: stat?.size ?? null, - }); - if (cached) { - return { entries: cached.entries, exists: cached.exists }; - } - - if (!stat) { - setAllowFromReadCache(filePath, { - exists: false, - mtimeMs: null, - size: null, - entries: [], - }); - return { entries: [], exists: false }; + const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat); + if (cachedOrMissing) { + return cachedOrMissing; } let raw = "";