fix(media): strip MEDIA: prefix in loadWebMediaInternal (#13107)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 9d95e6af5a
Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
Marcus Castro
2026-02-14 17:41:26 -03:00
committed by GitHub
parent 1bde33c0bc
commit 07850e8a93
9 changed files with 110 additions and 40 deletions

View File

@@ -14,6 +14,10 @@ import {
export { resetBaileysMocks, resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js";
// Avoid exporting inferred vitest mock types (TS2742 under pnpm + d.ts emit).
// oxlint-disable-next-line typescript/no-explicit-any
type AnyExport = any;
export const TEST_NET_IP = "203.0.113.10";
vi.mock("../agents/pi-embedded.js", () => ({
@@ -119,7 +123,7 @@ export function installWebAutoReplyUnitTestHooks(opts?: { pinDns?: boolean }) {
});
}
export function createWebListenerFactoryCapture() {
export function createWebListenerFactoryCapture(): AnyExport {
let capturedOnMessage: ((msg: WebInboundMessage) => Promise<void>) | undefined;
const listenerFactory = async (opts: {
onMessage: (msg: WebInboundMessage) => Promise<void>;
@@ -134,7 +138,7 @@ export function createWebListenerFactoryCapture() {
};
}
export function createWebInboundDeliverySpies() {
export function createWebInboundDeliverySpies(): AnyExport {
return {
sendMedia: vi.fn(),
reply: vi.fn().mockResolvedValue(undefined),

View File

@@ -108,6 +108,51 @@ describe("web media loading", () => {
});
});
it("strips MEDIA: prefix before reading local file", async () => {
const buffer = await sharp({
create: { width: 2, height: 2, channels: 3, background: "#0000ff" },
})
.png()
.toBuffer();
const file = await writeTempFile(buffer, ".png");
const result = await loadWebMedia(`MEDIA:${file}`, 1024 * 1024);
expect(result.kind).toBe("image");
expect(result.buffer.length).toBeGreaterThan(0);
});
it("strips MEDIA: prefix with whitespace after colon", async () => {
const buffer = await sharp({
create: { width: 2, height: 2, channels: 3, background: "#0000ff" },
})
.png()
.toBuffer();
const file = await writeTempFile(buffer, ".png");
const result = await loadWebMedia(`MEDIA: ${file}`, 1024 * 1024);
expect(result.kind).toBe("image");
expect(result.buffer.length).toBeGreaterThan(0);
});
it("strips MEDIA: prefix with extra whitespace (LLM-friendly)", async () => {
const buffer = await sharp({
create: { width: 2, height: 2, channels: 3, background: "#0000ff" },
})
.png()
.toBuffer();
const file = await writeTempFile(buffer, ".png");
const result = await loadWebMedia(` MEDIA : ${file}`, 1024 * 1024);
expect(result.kind).toBe("image");
expect(result.buffer.length).toBeGreaterThan(0);
});
it("compresses large local images under the provided cap", async () => {
const { buffer, file } = await createLargeTestJpeg();

View File

@@ -173,6 +173,9 @@ async function loadWebMediaInternal(
localRoots,
readFile: readFileOverride,
} = options;
// Strip MEDIA: prefix used by agent tools (e.g. TTS) to tag media paths.
// Be lenient: LLM output may add extra whitespace (e.g. " MEDIA : /tmp/x.png").
mediaUrl = mediaUrl.replace(/^\s*MEDIA\s*:\s*/i, "");
// Use fileURLToPath for proper handling of file:// URLs (handles file://localhost/path, etc.)
if (mediaUrl.startsWith("file://")) {
try {

View File

@@ -3,9 +3,12 @@ import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, vi } from "vitest";
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
import { resetLogger, setLoggerOverride } from "../logging.js";
// Avoid exporting vitest mock types (TS2742 under pnpm + d.ts emit).
// oxlint-disable-next-line typescript/no-explicit-any
type AnyMockFn = any;
export const DEFAULT_ACCOUNT_ID = "default";
export const DEFAULT_WEB_INBOX_CONFIG = {
@@ -21,28 +24,24 @@ export const DEFAULT_WEB_INBOX_CONFIG = {
},
} as const;
export const mockLoadConfig: MockFn<() => typeof DEFAULT_WEB_INBOX_CONFIG> = vi
.fn()
.mockReturnValue(DEFAULT_WEB_INBOX_CONFIG);
export const mockLoadConfig: AnyMockFn = vi.fn().mockReturnValue(DEFAULT_WEB_INBOX_CONFIG);
export const readAllowFromStoreMock: MockFn<(...args: unknown[]) => Promise<unknown[]>> = vi
export const readAllowFromStoreMock: AnyMockFn = vi.fn().mockResolvedValue([]);
export const upsertPairingRequestMock: AnyMockFn = vi
.fn()
.mockResolvedValue([]);
export const upsertPairingRequestMock: MockFn<
(...args: unknown[]) => Promise<{ code: string; created: boolean }>
> = vi.fn().mockResolvedValue({ code: "PAIRCODE", created: true });
.mockResolvedValue({ code: "PAIRCODE", created: true });
export type MockSock = {
ev: EventEmitter;
ws: { close: MockFn };
sendPresenceUpdate: MockFn;
sendMessage: MockFn;
readMessages: MockFn;
updateMediaMessage: MockFn;
ws: { close: AnyMockFn };
sendPresenceUpdate: AnyMockFn;
sendMessage: AnyMockFn;
readMessages: AnyMockFn;
updateMediaMessage: AnyMockFn;
logger: Record<string, unknown>;
signalRepository: {
lidMapping: {
getPNForLID: MockFn;
getPNForLID: AnyMockFn;
};
};
user: { id: string };