From 9ebfc99c1b035dc0b8be1e21d8e5310968db7836 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 19:09:14 +0000 Subject: [PATCH] refactor(test): dedupe temp media fixture setup in apply e2e --- src/media-understanding/apply.e2e.test.ts | 148 +++++++++++++--------- 1 file changed, 90 insertions(+), 58 deletions(-) diff --git a/src/media-understanding/apply.e2e.test.ts b/src/media-understanding/apply.e2e.test.ts index f128a7cda4d..3c3b40412cd 100644 --- a/src/media-understanding/apply.e2e.test.ts +++ b/src/media-understanding/apply.e2e.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, 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"; @@ -33,6 +33,17 @@ async function loadApply() { return await import("./apply.js"); } +const TEMP_MEDIA_PREFIX = "openclaw-media-"; +const tempMediaDirs: string[] = []; + +async function createTempMediaDir() { + const baseDir = resolvePreferredOpenClawTmpDir(); + await fs.mkdir(baseDir, { recursive: true }); + const dir = await fs.mkdtemp(path.join(baseDir, TEMP_MEDIA_PREFIX)); + tempMediaDirs.push(dir); + return dir; +} + function createGroqAudioConfig(): OpenClawConfig { return { tools: { @@ -82,16 +93,12 @@ function createMediaDisabledConfig(): OpenClawConfig { } async function createTempMediaFile(params: { fileName: string; content: Buffer | string }) { - const dir = await createMediaTempDir(); + const dir = await createTempMediaDir(); const mediaPath = path.join(dir, params.fileName); await fs.writeFile(mediaPath, params.content); return mediaPath; } -async function createMediaTempDir() { - return await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-media-")); -} - async function createAudioCtx(params?: { body?: string; fileName?: string; @@ -142,6 +149,14 @@ describe("applyMediaUnderstanding", () => { }); }); + afterEach(async () => { + await Promise.all( + tempMediaDirs.splice(0).map(async (dir) => { + await fs.rm(dir, { recursive: true, force: true }); + }), + ); + }); + it("sets Transcript and replaces Body when audio transcription succeeds", async () => { const { applyMediaUnderstanding } = await loadApply(); const ctx = await createAudioCtx(); @@ -318,9 +333,10 @@ describe("applyMediaUnderstanding", () => { it("uses CLI image understanding and preserves caption for commands", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await createMediaTempDir(); - const imagePath = path.join(dir, "photo.jpg"); - await fs.writeFile(imagePath, "image-bytes"); + const imagePath = await createTempMediaFile({ + fileName: "photo.jpg", + content: "image-bytes", + }); const ctx: MsgContext = { Body: " show Dom", @@ -365,9 +381,10 @@ describe("applyMediaUnderstanding", () => { it("uses shared media models list when capability config is missing", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await createMediaTempDir(); - const imagePath = path.join(dir, "shared.jpg"); - await fs.writeFile(imagePath, "image-bytes"); + const imagePath = await createTempMediaFile({ + fileName: "shared.jpg", + content: "image-bytes", + }); const ctx: MsgContext = { Body: "", @@ -406,9 +423,10 @@ describe("applyMediaUnderstanding", () => { it("uses active model when enabled and models are missing", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await createMediaTempDir(); - const audioPath = path.join(dir, "fallback.ogg"); - await fs.writeFile(audioPath, Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6])); + const audioPath = await createTempMediaFile({ + fileName: "fallback.ogg", + content: Buffer.from([0, 255, 0, 1, 2, 3, 4, 5, 6]), + }); const ctx: MsgContext = { Body: "", @@ -443,11 +461,12 @@ describe("applyMediaUnderstanding", () => { it("handles multiple audio attachments when attachment mode is all", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await createMediaTempDir(); + const dir = await createTempMediaDir(); + const audioBytes = Buffer.from([200, 201, 202, 203, 204, 205, 206, 207, 208]); const audioPathA = path.join(dir, "note-a.ogg"); const audioPathB = path.join(dir, "note-b.ogg"); - await fs.writeFile(audioPathA, Buffer.from([200, 201, 202, 203, 204, 205, 206, 207, 208])); - await fs.writeFile(audioPathB, Buffer.from([200, 201, 202, 203, 204, 205, 206, 207, 208])); + await fs.writeFile(audioPathA, audioBytes); + await fs.writeFile(audioPathB, audioBytes); const ctx: MsgContext = { Body: "", @@ -486,7 +505,7 @@ describe("applyMediaUnderstanding", () => { it("orders mixed media outputs as image, audio, video", async () => { const { applyMediaUnderstanding } = await loadApply(); - const dir = await createMediaTempDir(); + 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"); @@ -545,10 +564,11 @@ describe("applyMediaUnderstanding", () => { }); it("treats text-like attachments as CSV (comma wins over tabs)", async () => { - const dir = await createMediaTempDir(); - const csvPath = path.join(dir, "data.bin"); const csvText = '"a","b"\t"c"\n"1","2"\t"3"'; - await fs.writeFile(csvPath, csvText); + const csvPath = await createTempMediaFile({ + fileName: "data.bin", + content: csvText, + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -561,10 +581,11 @@ describe("applyMediaUnderstanding", () => { }); it("infers TSV when tabs are present without commas", async () => { - const dir = await createMediaTempDir(); - const tsvPath = path.join(dir, "report.bin"); const tsvText = "a\tb\tc\n1\t2\t3"; - await fs.writeFile(tsvPath, tsvText); + const tsvPath = await createTempMediaFile({ + fileName: "report.bin", + content: tsvText, + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -577,10 +598,11 @@ describe("applyMediaUnderstanding", () => { }); it("treats cp1252-like attachments as text", async () => { - const dir = await createMediaTempDir(); - const filePath = path.join(dir, "legacy.bin"); const cp1252Bytes = Buffer.from([0x93, 0x48, 0x69, 0x94, 0x20, 0x54, 0x65, 0x73, 0x74]); - await fs.writeFile(filePath, cp1252Bytes); + const filePath = await createTempMediaFile({ + fileName: "legacy.bin", + content: cp1252Bytes, + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -593,10 +615,11 @@ describe("applyMediaUnderstanding", () => { }); it("skips binary audio attachments that are not text-like", async () => { - const dir = await createMediaTempDir(); - const filePath = path.join(dir, "binary.mp3"); const bytes = Buffer.from(Array.from({ length: 256 }, (_, index) => index)); - await fs.writeFile(filePath, bytes); + const filePath = await createTempMediaFile({ + fileName: "binary.mp3", + content: bytes, + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -610,10 +633,11 @@ describe("applyMediaUnderstanding", () => { }); it("respects configured allowedMimes for text-like attachments", async () => { - const dir = await createMediaTempDir(); - const tsvPath = path.join(dir, "report.bin"); const tsvText = "a\tb\tc\n1\t2\t3"; - await fs.writeFile(tsvPath, tsvText); + const tsvPath = await createTempMediaFile({ + fileName: "report.bin", + content: tsvText, + }); const cfg: OpenClawConfig = { ...createMediaDisabledConfig(), @@ -639,13 +663,14 @@ describe("applyMediaUnderstanding", () => { }); it("escapes XML special characters in filenames to prevent injection", async () => { - const dir = await createMediaTempDir(); // 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 = path.join(dir, "file&test.txt"); - await fs.writeFile(filePath, "safe content"); + const filePath = await createTempMediaFile({ + fileName: "file&test.txt", + content: "safe content", + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -661,9 +686,10 @@ describe("applyMediaUnderstanding", () => { }); it("escapes file block content to prevent structure injection", async () => { - const dir = await createMediaTempDir(); - const filePath = path.join(dir, "content.txt"); - await fs.writeFile(filePath, 'before after'); + const filePath = await createTempMediaFile({ + fileName: "content.txt", + content: 'before after', + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -679,9 +705,10 @@ describe("applyMediaUnderstanding", () => { }); it("normalizes MIME types to prevent attribute injection", async () => { - const dir = await createMediaTempDir(); - const filePath = path.join(dir, "data.json"); - await fs.writeFile(filePath, JSON.stringify({ ok: true })); + const filePath = await createTempMediaFile({ + fileName: "data.json", + content: JSON.stringify({ ok: true }), + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -699,10 +726,11 @@ describe("applyMediaUnderstanding", () => { }); it("handles path traversal attempts in filenames safely", async () => { - const dir = await createMediaTempDir(); // Even if a file somehow got a path-like name, it should be handled safely - const filePath = path.join(dir, "normal.txt"); - await fs.writeFile(filePath, "legitimate content"); + const filePath = await createTempMediaFile({ + fileName: "normal.txt", + content: "legitimate content", + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -718,9 +746,10 @@ describe("applyMediaUnderstanding", () => { }); it("forces BodyForCommands when only file blocks are added", async () => { - const dir = await createMediaTempDir(); - const filePath = path.join(dir, "notes.txt"); - await fs.writeFile(filePath, "file content"); + const filePath = await createTempMediaFile({ + fileName: "notes.txt", + content: "file content", + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -734,9 +763,10 @@ describe("applyMediaUnderstanding", () => { }); it("handles files with non-ASCII Unicode filenames", async () => { - const dir = await createMediaTempDir(); - const filePath = path.join(dir, "文档.txt"); - await fs.writeFile(filePath, "中文内容"); + const filePath = await createTempMediaFile({ + fileName: "文档.txt", + content: "中文内容", + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -749,11 +779,12 @@ describe("applyMediaUnderstanding", () => { }); it("skips binary application/vnd office attachments even when bytes look printable", async () => { - const dir = await createMediaTempDir(); - const filePath = path.join(dir, "report.xlsx"); // ZIP-based Office docs can have printable-leading bytes. const pseudoZip = Buffer.from("PK\u0003\u0004[Content_Types].xml xl/workbook.xml", "utf8"); - await fs.writeFile(filePath, pseudoZip); + const filePath = await createTempMediaFile({ + fileName: "report.xlsx", + content: pseudoZip, + }); const { ctx, result } = await applyWithDisabledMedia({ body: "", @@ -767,9 +798,10 @@ describe("applyMediaUnderstanding", () => { }); it("keeps vendor +json attachments eligible for text extraction", async () => { - const dir = await createMediaTempDir(); - const filePath = path.join(dir, "payload.bin"); - await fs.writeFile(filePath, '{"ok":true,"source":"vendor-json"}'); + const filePath = await createTempMediaFile({ + fileName: "payload.bin", + content: '{"ok":true,"source":"vendor-json"}', + }); const { ctx, result } = await applyWithDisabledMedia({ body: "",