diff --git a/src/auto-reply/reply/reply-media-paths.test.ts b/src/auto-reply/reply/reply-media-paths.test.ts index 8ada8839dc8..5f25e0f3d90 100644 --- a/src/auto-reply/reply/reply-media-paths.test.ts +++ b/src/auto-reply/reply/reply-media-paths.test.ts @@ -176,4 +176,24 @@ describe("createReplyMediaPathNormalizer", () => { mediaUrls: ["/Users/peter/.openclaw/media/outbound/persisted.png"], }); }); + + it("keeps TTS voice output from the OpenClaw temp directory", async () => { + // resolvePreferredOpenClawTmpDir() returns /tmp/openclaw on POSIX when it exists. + // We rely on the real function (no mock) since the test environment has /tmp/openclaw. + const normalize = createReplyMediaPathNormalizer({ + cfg: {}, + sessionKey: "session-key", + workspaceDir: "/tmp/agent-workspace", + }); + + const result = await normalize({ + mediaUrls: ["/tmp/openclaw/tts-abc123/voice-1234567890.opus"], + }); + + expect(result).toMatchObject({ + mediaUrl: "/tmp/openclaw/tts-abc123/voice-1234567890.opus", + mediaUrls: ["/tmp/openclaw/tts-abc123/voice-1234567890.opus"], + }); + expect(saveMediaSource).not.toHaveBeenCalled(); + }); }); diff --git a/src/auto-reply/reply/reply-media-paths.ts b/src/auto-reply/reply/reply-media-paths.ts index 1e213f3dd92..49f9753540d 100644 --- a/src/auto-reply/reply/reply-media-paths.ts +++ b/src/auto-reply/reply/reply-media-paths.ts @@ -60,10 +60,12 @@ function isAllowedAbsoluteReplyMediaPath(params: { * These are trusted paths written by OpenClaw's own tooling * (TTS, media processing, etc.) and should be deliverable as reply media. */ +let cachedTmpRoot: string | undefined; + function isOpenClawTmpPath(candidate: string): boolean { try { - const tmpRoot = resolvePreferredOpenClawTmpDir(); - return isPathInside(tmpRoot, candidate); + cachedTmpRoot ??= resolvePreferredOpenClawTmpDir(); + return isPathInside(cachedTmpRoot, candidate); } catch { return false; }