import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveApiKeyForProvider } from "../agents/model-auth.js"; import type { MsgContext } from "../auto-reply/templating.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { fetchRemoteMedia } from "../media/fetch.js"; import { runExec } from "../process/exec.js"; import { withEnvAsync } from "../test-utils/env.js"; import { clearMediaUnderstandingBinaryCacheForTests } from "./runner.js"; import { createSafeAudioFixtureBuffer } from "./runner.test-utils.js"; vi.mock("../agents/model-auth.js", () => ({ resolveApiKeyForProvider: vi.fn(async () => ({ apiKey: "test-key", // pragma: allowlist secret source: "test", mode: "api-key", })), requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => { if (auth?.apiKey) { return auth.apiKey; } throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`); }, })); vi.mock("../media/fetch.js", () => ({ fetchRemoteMedia: vi.fn(), })); vi.mock("../process/exec.js", () => ({ runExec: vi.fn(), })); let applyMediaUnderstanding: typeof import("./apply.js").applyMediaUnderstanding; const mockedRunExec = vi.mocked(runExec); const TEMP_MEDIA_PREFIX = "openclaw-media-"; let suiteTempMediaRootDir = ""; let tempMediaDirCounter = 0; let sharedTempMediaCacheDir = ""; const tempMediaFileCache = new Map(); async function createTempMediaDir() { if (!suiteTempMediaRootDir) { throw new Error("suite temp media root not initialized"); } const dir = path.join(suiteTempMediaRootDir, `case-${String(tempMediaDirCounter)}`); tempMediaDirCounter += 1; await fs.mkdir(dir, { recursive: true }); return dir; } async function getSharedTempMediaCacheDir() { if (!sharedTempMediaCacheDir) { sharedTempMediaCacheDir = await createTempMediaDir(); } return sharedTempMediaCacheDir; } function createGroqAudioConfig(): OpenClawConfig { return { tools: { media: { audio: { enabled: true, maxBytes: 1024 * 1024, models: [{ provider: "groq" }], }, }, }, }; } function createGroqProviders(transcribedText = "transcribed text") { return { groq: { id: "groq", transcribeAudio: async () => ({ text: transcribedText }), }, }; } function expectTranscriptApplied(params: { ctx: MsgContext; transcript: string; body: string; commandBody: string; }) { expect(params.ctx.Transcript).toBe(params.transcript); expect(params.ctx.Body).toBe(params.body); expect(params.ctx.CommandBody).toBe(params.commandBody); expect(params.ctx.RawBody).toBe(params.commandBody); expect(params.ctx.BodyForCommands).toBe(params.commandBody); } function createMediaDisabledConfig(): OpenClawConfig { return { tools: { media: { audio: { enabled: false }, image: { enabled: false }, video: { enabled: false }, }, }, }; } function createMediaDisabledConfigWithAllowedMimes(allowedMimes: string[]): OpenClawConfig { return { ...createMediaDisabledConfig(), gateway: { http: { endpoints: { responses: { files: { allowedMimes }, }, }, }, }, }; } async function createTempMediaFile(params: { fileName: string; content: Buffer | string }) { const normalizedContent = typeof params.content === "string" ? Buffer.from(params.content) : params.content; const contentHash = crypto.createHash("sha1").update(normalizedContent).digest("hex"); const cacheKey = `${params.fileName}:${contentHash}`; const cachedPath = tempMediaFileCache.get(cacheKey); if (cachedPath) { return cachedPath; } const cacheRootDir = await getSharedTempMediaCacheDir(); const cacheDir = path.join(cacheRootDir, contentHash); await fs.mkdir(cacheDir, { recursive: true }); const mediaPath = path.join(cacheDir, params.fileName); await fs.writeFile(mediaPath, params.content); tempMediaFileCache.set(cacheKey, mediaPath); return mediaPath; } async function createMockExecutable(dir: string, name: string) { const executablePath = path.join(dir, name); await fs.writeFile(executablePath, "echo mocked\n", { mode: 0o755 }); return executablePath; } async function withMediaAutoDetectEnv( env: Record, run: () => Promise, ): Promise { return await withEnvAsync( { SHERPA_ONNX_MODEL_DIR: undefined, WHISPER_CPP_MODEL: undefined, OPENAI_API_KEY: undefined, GROQ_API_KEY: undefined, DEEPGRAM_API_KEY: undefined, GEMINI_API_KEY: undefined, OPENCLAW_AGENT_DIR: undefined, PI_CODING_AGENT_DIR: undefined, ...env, }, run, ); } async function createAudioCtx(params?: { body?: string; fileName?: string; mediaType?: string; content?: Buffer | string; }): Promise { const mediaPath = await createTempMediaFile({ fileName: params?.fileName ?? "note.ogg", content: params?.content ?? createSafeAudioFixtureBuffer(2048), }); return { Body: params?.body ?? "", MediaPath: mediaPath, MediaType: params?.mediaType ?? "audio/ogg", } satisfies MsgContext; } async function setupAudioAutoDetectCase(stdout: string): Promise<{ ctx: MsgContext; cfg: OpenClawConfig; }> { const ctx = await createAudioCtx({ fileName: "sample.wav", mediaType: "audio/wav", content: createSafeAudioFixtureBuffer(2048), }); const cfg: OpenClawConfig = { tools: { media: { audio: {} } } }; mockedRunExec.mockResolvedValueOnce({ stdout, stderr: "", }); return { ctx, cfg }; } async function applyWithDisabledMedia(params: { body: string; mediaPath: string; mediaType?: string; cfg?: OpenClawConfig; }) { const ctx: MsgContext = { Body: params.body, MediaPath: params.mediaPath, ...(params.mediaType ? { MediaType: params.mediaType } : {}), }; const result = await applyMediaUnderstanding({ ctx, cfg: params.cfg ?? createMediaDisabledConfig(), }); return { ctx, result }; } function expectFileNotApplied(params: { ctx: MsgContext; result: { appliedFile: boolean }; body: string; }) { expect(params.result.appliedFile).toBe(false); expect(params.ctx.Body).toBe(params.body); expect(params.ctx.Body).not.toContain(" { const mockedResolveApiKey = vi.mocked(resolveApiKeyForProvider); const mockedFetchRemoteMedia = vi.mocked(fetchRemoteMedia); beforeAll(async () => { const baseDir = resolvePreferredOpenClawTmpDir(); await fs.mkdir(baseDir, { recursive: true }); suiteTempMediaRootDir = await fs.mkdtemp(path.join(baseDir, TEMP_MEDIA_PREFIX)); ({ applyMediaUnderstanding } = await import("./apply.js")); }); beforeEach(() => { mockedResolveApiKey.mockReset(); mockedResolveApiKey.mockResolvedValue({ apiKey: "test-key", // pragma: allowlist secret source: "test", mode: "api-key", }); mockedFetchRemoteMedia.mockClear(); mockedRunExec.mockReset(); mockedFetchRemoteMedia.mockResolvedValue({ buffer: createSafeAudioFixtureBuffer(2048), contentType: "audio/ogg", fileName: "note.ogg", }); clearMediaUnderstandingBinaryCacheForTests(); }); afterAll(async () => { if (!suiteTempMediaRootDir) { return; } await fs.rm(suiteTempMediaRootDir, { recursive: true, force: true }); suiteTempMediaRootDir = ""; sharedTempMediaCacheDir = ""; tempMediaFileCache.clear(); }); it("sets Transcript and replaces Body when audio transcription succeeds", async () => { const ctx = await createAudioCtx(); const result = await applyMediaUnderstanding({ ctx, cfg: createGroqAudioConfig(), providers: createGroqProviders(), }); expect(result.appliedAudio).toBe(true); expectTranscriptApplied({ ctx, transcript: "transcribed text", body: "[Audio]\nTranscript:\ntranscribed text", commandBody: "transcribed text", }); expect((ctx as unknown as { BodyForAgent?: string }).BodyForAgent).toBe(ctx.Body); }); it("skips file blocks for text-like audio when transcription succeeds", async () => { const ctx = await createAudioCtx({ fileName: "data.mp3", mediaType: "audio/mpeg", content: `"a","b"\n"1","2"\n${"x".repeat(2048)}`, }); const result = await applyMediaUnderstanding({ ctx, cfg: createGroqAudioConfig(), providers: createGroqProviders(), }); expect(result.appliedAudio).toBe(true); expect(result.appliedFile).toBe(false); expect(ctx.Body).toBe("[Audio]\nTranscript:\ntranscribed text"); expect(ctx.Body).not.toContain(" { const ctx = await createAudioCtx({ body: " /capture status", }); ctx.CommandAuthorized = false; const result = await applyMediaUnderstanding({ ctx, cfg: createGroqAudioConfig(), providers: createGroqProviders(), }); expect(result.appliedAudio).toBe(true); expectTranscriptApplied({ ctx, transcript: "transcribed text", body: "[Audio]\nUser text:\n/capture status\nTranscript:\ntranscribed text", commandBody: "/capture status", }); expect(ctx.CommandAuthorized).toBe(false); }); it("handles URL-only attachments for audio transcription", async () => { const ctx: MsgContext = { Body: "", MediaUrl: "https://example.com/note.ogg", MediaType: "audio/ogg", ChatType: "direct", }; const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, maxBytes: 1024 * 1024, scope: { default: "deny", rules: [{ action: "allow", match: { chatType: "direct" } }], }, models: [{ provider: "groq" }], }, }, }, }; const result = await applyMediaUnderstanding({ ctx, cfg, providers: { groq: { id: "groq", transcribeAudio: async () => ({ text: "remote transcript" }), }, }, }); expect(result.appliedAudio).toBe(true); expect(ctx.Transcript).toBe("remote transcript"); expect(ctx.Body).toBe("[Audio]\nTranscript:\nremote transcript"); }); it("transcribes WhatsApp audio with parameterized MIME despite casing/whitespace", async () => { const ctx = await createAudioCtx({ fileName: "voice-note", mediaType: " Audio/Ogg; codecs=opus ", }); ctx.Surface = "whatsapp"; const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, maxBytes: 1024 * 1024, scope: { default: "deny", rules: [{ action: "allow", match: { channel: "whatsapp" } }], }, models: [{ provider: "groq" }], }, }, }, }; const result = await applyMediaUnderstanding({ ctx, cfg, providers: createGroqProviders("whatsapp transcript"), }); expect(result.appliedAudio).toBe(true); expect(ctx.Transcript).toBe("whatsapp transcript"); expect(ctx.Body).toBe("[Audio]\nTranscript:\nwhatsapp transcript"); }); it("skips URL-only audio when remote file is too small", async () => { // Override the default mock to return a tiny buffer (below MIN_AUDIO_FILE_BYTES) mockedFetchRemoteMedia.mockResolvedValueOnce({ buffer: Buffer.alloc(100), contentType: "audio/ogg", fileName: "tiny.ogg", }); const ctx: MsgContext = { Body: "", MediaUrl: "https://example.com/tiny.ogg", MediaType: "audio/ogg", ChatType: "dm", }; const transcribeAudio = vi.fn(async () => ({ text: "should-not-run" })); const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, maxBytes: 1024 * 1024, scope: { default: "deny", rules: [{ action: "allow", match: { chatType: "direct" } }], }, models: [{ provider: "groq" }], }, }, }, }; const result = await applyMediaUnderstanding({ ctx, cfg, providers: { groq: { id: "groq", transcribeAudio }, }, }); expect(transcribeAudio).not.toHaveBeenCalled(); expect(result.appliedAudio).toBe(false); }); it("skips audio transcription when attachment exceeds maxBytes", async () => { const ctx = await createAudioCtx({ fileName: "large.wav", mediaType: "audio/wav", content: Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), }); const transcribeAudio = vi.fn(async () => ({ text: "should-not-run" })); const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, maxBytes: 4, models: [{ provider: "groq" }], }, }, }, }; const result = await applyMediaUnderstanding({ ctx, cfg, providers: { groq: { id: "groq", transcribeAudio } }, }); expect(result.appliedAudio).toBe(false); expect(transcribeAudio).not.toHaveBeenCalled(); expect(ctx.Body).toBe(""); }); it("falls back to CLI model when provider fails", async () => { const ctx = await createAudioCtx(); const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, models: [ { provider: "groq" }, { type: "cli", command: "whisper", args: ["{{MediaPath}}"], }, ], }, }, }, }; mockedRunExec.mockResolvedValue({ stdout: "cli transcript\n", stderr: "", }); const result = await applyMediaUnderstanding({ ctx, cfg, providers: { groq: { id: "groq", transcribeAudio: async () => { throw new Error("boom"); }, }, }, }); expect(result.appliedAudio).toBe(true); expect((ctx as unknown as { Transcript?: string }).Transcript).toBe("cli transcript"); expect(ctx.Body).toBe("[Audio]\nTranscript:\ncli transcript"); }); it("reads parakeet-mlx transcript from output-dir txt file", async () => { const ctx = await createAudioCtx({ fileName: "sample.wav", mediaType: "audio/wav" }); const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, models: [ { type: "cli", command: "parakeet-mlx", args: ["{{MediaPath}}", "--output-format", "txt", "--output-dir", "{{OutputDir}}"], }, ], }, }, }, }; mockedRunExec.mockImplementationOnce(async (_cmd, args) => { const mediaPath = args[0]; const outputDirArgIndex = args.indexOf("--output-dir"); const outputDir = outputDirArgIndex >= 0 ? args[outputDirArgIndex + 1] : undefined; const transcriptPath = mediaPath && outputDir ? path.join(outputDir, `${path.parse(mediaPath).name}.txt`) : ""; if (transcriptPath) { await fs.writeFile(transcriptPath, "parakeet transcript\n"); } return { stdout: "", stderr: "" }; }); const result = await applyMediaUnderstanding({ ctx, cfg }); expect(result.appliedAudio).toBe(true); expect(ctx.Transcript).toBe("parakeet transcript"); expect(ctx.Body).toBe("[Audio]\nTranscript:\nparakeet transcript"); }); it("falls back to stdout for parakeet-mlx when output format is not txt", async () => { const ctx = await createAudioCtx({ fileName: "sample.wav", mediaType: "audio/wav" }); const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, models: [ { type: "cli", command: "parakeet-mlx", args: ["{{MediaPath}}", "--output-format", "json", "--output-dir", "{{OutputDir}}"], }, ], }, }, }, }; mockedRunExec.mockImplementationOnce(async (_cmd, args) => { const mediaPath = args[0]; const outputDirArgIndex = args.indexOf("--output-dir"); const outputDir = outputDirArgIndex >= 0 ? args[outputDirArgIndex + 1] : undefined; const transcriptPath = mediaPath && outputDir ? path.join(outputDir, `${path.parse(mediaPath).name}.txt`) : ""; if (transcriptPath) { await fs.writeFile(transcriptPath, "should-not-be-used\n"); } return { stdout: "stdout transcript\n", stderr: "" }; }); const result = await applyMediaUnderstanding({ ctx, cfg }); expect(result.appliedAudio).toBe(true); expect(ctx.Transcript).toBe("stdout transcript"); expect(ctx.Body).toBe("[Audio]\nTranscript:\nstdout transcript"); }); it("auto-detects sherpa for audio when binary and model files are available", async () => { const binDir = await createTempMediaDir(); const modelDir = await createTempMediaDir(); await createMockExecutable(binDir, "sherpa-onnx-offline"); await fs.writeFile(path.join(modelDir, "tokens.txt"), "a"); await fs.writeFile(path.join(modelDir, "encoder.onnx"), "a"); await fs.writeFile(path.join(modelDir, "decoder.onnx"), "a"); await fs.writeFile(path.join(modelDir, "joiner.onnx"), "a"); const { ctx, cfg } = await setupAudioAutoDetectCase('{"text":"sherpa ok"}'); await withMediaAutoDetectEnv( { PATH: binDir, SHERPA_ONNX_MODEL_DIR: modelDir, }, async () => { const result = await applyMediaUnderstanding({ ctx, cfg }); expect(result.appliedAudio).toBe(true); }, ); expect(ctx.Transcript).toBe("sherpa ok"); expect(mockedRunExec).toHaveBeenCalledWith( "sherpa-onnx-offline", expect.any(Array), expect.any(Object), ); }); it("auto-detects whisper-cli when sherpa is unavailable", async () => { const binDir = await createTempMediaDir(); const modelDir = await createTempMediaDir(); await createMockExecutable(binDir, "whisper-cli"); const modelPath = path.join(modelDir, "tiny.bin"); await fs.writeFile(modelPath, "model"); const { ctx, cfg } = await setupAudioAutoDetectCase("whisper cpp ok\n"); await withMediaAutoDetectEnv( { PATH: binDir, WHISPER_CPP_MODEL: modelPath, }, async () => { const result = await applyMediaUnderstanding({ ctx, cfg }); expect(result.appliedAudio).toBe(true); }, ); expect(ctx.Transcript).toBe("whisper cpp ok"); expect(mockedRunExec).toHaveBeenCalledWith( "whisper-cli", expect.any(Array), expect.any(Object), ); }); it("skips audio auto-detect when no supported binaries or provider keys are available", async () => { const emptyBinDir = await createTempMediaDir(); const isolatedAgentDir = await createTempMediaDir(); const ctx = await createAudioCtx({ fileName: "sample.wav", mediaType: "audio/wav", content: createSafeAudioFixtureBuffer(2048), }); const cfg: OpenClawConfig = { tools: { media: { audio: {} } } }; mockedResolveApiKey.mockResolvedValue({ source: "none", mode: "api-key", }); await withMediaAutoDetectEnv( { PATH: emptyBinDir, OPENCLAW_AGENT_DIR: isolatedAgentDir, PI_CODING_AGENT_DIR: isolatedAgentDir, }, async () => { const result = await applyMediaUnderstanding({ ctx, cfg }); expect(result.appliedAudio).toBe(false); }, ); expect(ctx.Transcript).toBeUndefined(); expect(ctx.Body).toBe(""); expect(mockedRunExec).not.toHaveBeenCalled(); }); it("uses CLI image understanding and preserves caption for commands", async () => { const imagePath = await createTempMediaFile({ fileName: "photo.jpg", content: "image-bytes", }); const ctx: MsgContext = { Body: " show Dom", MediaPath: imagePath, MediaType: "image/jpeg", }; const cfg: OpenClawConfig = { tools: { media: { image: { enabled: true, models: [ { type: "cli", command: "gemini", args: ["--file", "{{MediaPath}}", "--prompt", "{{Prompt}}"], }, ], }, }, }, }; mockedRunExec.mockResolvedValue({ stdout: "image description\n", stderr: "", }); const result = await applyMediaUnderstanding({ ctx, cfg, }); expect(result.appliedImage).toBe(true); expect(ctx.Body).toBe("[Image]\nUser text:\nshow Dom\nDescription:\nimage description"); expect(ctx.CommandBody).toBe("show Dom"); expect(ctx.RawBody).toBe("show Dom"); expect(ctx.BodyForAgent).toBe(ctx.Body); expect(ctx.BodyForCommands).toBe("show Dom"); }); it("uses shared media models list when capability config is missing", async () => { const imagePath = await createTempMediaFile({ fileName: "shared.jpg", content: "image-bytes", }); const ctx: MsgContext = { Body: "", MediaPath: imagePath, MediaType: "image/jpeg", }; const cfg: OpenClawConfig = { tools: { media: { models: [ { type: "cli", command: "gemini", args: ["--allowed-tools", "read_file", "{{MediaPath}}"], capabilities: ["image"], }, ], }, }, }; mockedRunExec.mockResolvedValue({ stdout: "shared description\n", stderr: "", }); const result = await applyMediaUnderstanding({ ctx, cfg, }); expect(result.appliedImage).toBe(true); expect(ctx.Body).toBe("[Image]\nDescription:\nshared description"); }); it("uses active model when enabled and models are missing", async () => { const audioPath = await createTempMediaFile({ fileName: "fallback.ogg", content: createSafeAudioFixtureBuffer(2048), }); const ctx: MsgContext = { Body: "", MediaPath: audioPath, MediaType: "audio/ogg", }; const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, }, }, }, }; const result = await applyMediaUnderstanding({ ctx, cfg, activeModel: { provider: "groq", model: "whisper-large-v3" }, providers: { groq: { id: "groq", transcribeAudio: async () => ({ text: "fallback transcript" }), }, }, }); expect(result.appliedAudio).toBe(true); expect(ctx.Transcript).toBe("fallback transcript"); }); it("handles multiple audio attachments when attachment mode is all", async () => { const dir = await createTempMediaDir(); const audioBytes = createSafeAudioFixtureBuffer(2048); const audioPathA = path.join(dir, "note-a.ogg"); const audioPathB = path.join(dir, "note-b.ogg"); await fs.writeFile(audioPathA, audioBytes); await fs.writeFile(audioPathB, audioBytes); const ctx: MsgContext = { Body: "", MediaPaths: [audioPathA, audioPathB], MediaTypes: ["audio/ogg", "audio/ogg"], }; const cfg: OpenClawConfig = { tools: { media: { audio: { enabled: true, attachments: { mode: "all", maxAttachments: 2 }, models: [{ provider: "groq" }], }, }, }, }; const result = await applyMediaUnderstanding({ ctx, cfg, providers: { groq: { id: "groq", transcribeAudio: async (req) => ({ text: req.fileName }), }, }, }); expect(result.appliedAudio).toBe(true); expect(ctx.Transcript).toBe("Audio 1:\nnote-a.ogg\n\nAudio 2:\nnote-b.ogg"); expect(ctx.Body).toBe( ["[Audio 1/2]\nTranscript:\nnote-a.ogg", "[Audio 2/2]\nTranscript:\nnote-b.ogg"].join("\n\n"), ); }); it("orders mixed media outputs as image, audio, video", async () => { const dir = await createTempMediaDir(); const imagePath = path.join(dir, "photo.jpg"); const audioPath = path.join(dir, "note.ogg"); const videoPath = path.join(dir, "clip.mp4"); await fs.writeFile(imagePath, "image-bytes"); await fs.writeFile(audioPath, createSafeAudioFixtureBuffer(2048)); await fs.writeFile(videoPath, "video-bytes"); const ctx: MsgContext = { Body: "", MediaPaths: [imagePath, audioPath, videoPath], MediaTypes: ["image/jpeg", "audio/ogg", "video/mp4"], }; const cfg: OpenClawConfig = { tools: { media: { image: { enabled: true, models: [{ provider: "openai", model: "gpt-5.2" }] }, audio: { enabled: true, models: [{ provider: "groq" }] }, video: { enabled: true, models: [{ provider: "google", model: "gemini-3" }] }, }, }, }; const result = await applyMediaUnderstanding({ ctx, cfg, agentDir: dir, providers: { openai: { id: "openai", describeImage: async () => ({ text: "image ok" }), }, groq: { id: "groq", transcribeAudio: async () => ({ text: "audio ok" }), }, google: { id: "google", describeVideo: async () => ({ text: "video ok" }), }, }, }); expect(result.appliedImage).toBe(true); expect(result.appliedAudio).toBe(true); expect(result.appliedVideo).toBe(true); expect(ctx.Body).toBe( [ "[Image]\nDescription:\nimage ok", "[Audio]\nTranscript:\naudio ok", "[Video]\nDescription:\nvideo ok", ].join("\n\n"), ); expect(ctx.Transcript).toBe("audio ok"); expect(ctx.CommandBody).toBe("audio ok"); expect(ctx.BodyForCommands).toBe("audio ok"); }); it("treats text-like attachments as CSV (comma wins over tabs)", async () => { const csvText = '"a","b"\t"c"\n"1","2"\t"3"'; const csvPath = await createTempMediaFile({ fileName: "data.bin", content: csvText, }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: csvPath, }); expect(result.appliedFile).toBe(true); expect(ctx.Body).toContain(''); expect(ctx.Body).toContain('"a","b"\t"c"'); }); it("infers TSV when tabs are present without commas", async () => { const tsvText = "a\tb\tc\n1\t2\t3"; const tsvPath = await createTempMediaFile({ fileName: "report.bin", content: tsvText, }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: tsvPath, }); expect(result.appliedFile).toBe(true); expect(ctx.Body).toContain(''); expect(ctx.Body).toContain("a\tb\tc"); }); it("treats cp1252-like attachments as text", async () => { const cp1252Bytes = Buffer.from([0x93, 0x48, 0x69, 0x94, 0x20, 0x54, 0x65, 0x73, 0x74]); const filePath = await createTempMediaFile({ fileName: "legacy.bin", content: cp1252Bytes, }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, }); expect(result.appliedFile).toBe(true); expect(ctx.Body).toContain(" { const bytes = Buffer.from(Array.from({ length: 256 }, (_, index) => index)); const filePath = await createTempMediaFile({ fileName: "binary.mp3", content: bytes, }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "audio/mpeg", }); expectFileNotApplied({ ctx, result, body: "" }); }); it("does not reclassify PDF attachments as text/plain", async () => { const pseudoPdf = Buffer.from("%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\n", "utf8"); const filePath = await createTempMediaFile({ fileName: "report.pdf", content: pseudoPdf, }); const cfg = createMediaDisabledConfigWithAllowedMimes(["text/plain"]); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "application/pdf", cfg, }); expectFileNotApplied({ ctx, result, body: "" }); }); it("respects configured allowedMimes for text-like attachments", async () => { const tsvText = "a\tb\tc\n1\t2\t3"; const tsvPath = await createTempMediaFile({ fileName: "report.bin", content: tsvText, }); const cfg = createMediaDisabledConfigWithAllowedMimes(["text/plain"]); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: tsvPath, cfg, }); expectFileNotApplied({ ctx, result, body: "" }); }); it("escapes XML special characters in filenames to prevent injection", async () => { // Use & in filename — valid on all platforms (including Windows, which // forbids < and > in NTFS filenames) and still requires XML escaping. // Note: The sanitizeFilename in store.ts would strip most dangerous chars, // but we test that even if some slip through, they get escaped in output const filePath = await createTempMediaFile({ fileName: "file&test.txt", content: "safe content", }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "text/plain", }); expect(result.appliedFile).toBe(true); // Verify XML special chars are escaped in the output expect(ctx.Body).toContain("&"); // The name attribute should contain the escaped form, not a raw unescaped & expect(ctx.Body).toMatch(/name="file&test\.txt"/); }); it("escapes file block content to prevent structure injection", async () => { const filePath = await createTempMediaFile({ fileName: "content.txt", content: 'before after', }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "text/plain", }); const body = ctx.Body ?? ""; expect(result.appliedFile).toBe(true); expect(body).toContain("</file>"); expect(body).toContain("<file"); expect((body.match(/<\/file>/g) ?? []).length).toBe(1); }); it("normalizes MIME types to prevent attribute injection", async () => { const filePath = await createTempMediaFile({ fileName: "data.json", content: JSON.stringify({ ok: true }), }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, // Attempt to inject via MIME type with quotes - normalization should strip this mediaType: 'application/json" onclick="alert(1)', }); expect(result.appliedFile).toBe(true); // MIME normalization strips everything after first ; or " - verify injection is blocked expect(ctx.Body).not.toContain("onclick="); expect(ctx.Body).not.toContain("alert(1)"); // Verify the MIME type is normalized to just "application/json" expect(ctx.Body).toContain('mime="application/json"'); }); it("handles path traversal attempts in filenames safely", async () => { // Even if a file somehow got a path-like name, it should be handled safely const filePath = await createTempMediaFile({ fileName: "normal.txt", content: "legitimate content", }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "text/plain", }); expect(result.appliedFile).toBe(true); // Verify the file was processed and output contains expected structure expect(ctx.Body).toContain(' { const filePath = await createTempMediaFile({ fileName: "notes.txt", content: "file content", }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "text/plain", }); expect(result.appliedFile).toBe(true); expect(ctx.Body).toContain(''); expect(ctx.BodyForCommands).toBe(ctx.Body); }); it("handles files with non-ASCII Unicode filenames", async () => { const filePath = await createTempMediaFile({ fileName: "文档.txt", content: "中文内容", }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "text/plain", }); expect(result.appliedFile).toBe(true); expect(ctx.Body).toContain("中文内容"); }); it("skips binary application/vnd office attachments even when bytes look printable", async () => { // ZIP-based Office docs can have printable-leading bytes. const pseudoZip = Buffer.from("PK\u0003\u0004[Content_Types].xml xl/workbook.xml", "utf8"); const filePath = await createTempMediaFile({ fileName: "report.xlsx", content: pseudoZip, }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); expectFileNotApplied({ ctx, result, body: "" }); }); it("keeps vendor +json attachments eligible for text extraction", async () => { const filePath = await createTempMediaFile({ fileName: "payload.bin", content: '{"ok":true,"source":"vendor-json"}', }); const { ctx, result } = await applyWithDisabledMedia({ body: "", mediaPath: filePath, mediaType: "application/vnd.api+json", }); expect(result.appliedFile).toBe(true); expect(ctx.Body).toContain("