diff --git a/qa/scenarios/media/media-reference-intake.yaml b/qa/scenarios/media/media-reference-intake.yaml new file mode 100644 index 000000000000..b371d54c1471 --- /dev/null +++ b/qa/scenarios/media/media-reference-intake.yaml @@ -0,0 +1,29 @@ +title: Media reference intake + +scenario: + id: media-reference-intake + surface: media + category: media.media-intake-and-access + coverage: + primary: + - media.local-and-remote-media-references + - media.mime-and-type-detection + objective: Prove the raw media loader resolves local, remote, and media-store references while deriving MIME family from bytes. + successCriteria: + - Plain paths and file URLs read the same real local PNG bytes through allowed-root policy. + - A real loopback HTTP response crosses guarded remote fetch and returns the served bytes. + - A media-store URI resolves an isolated real inbound artifact. + - PNG bytes produce image/png and image kind despite misleading text extensions and HTTP headers. + docsRefs: + - docs/concepts/qa-e2e-automation.md + - docs/help/testing.md + codeRefs: + - src/media/web-media.ts + - src/media/fetch.ts + - src/media/store.ts + - packages/media-core/src/mime.ts + - test/e2e/qa-lab/media/media-reference-intake.product.test.ts + execution: + kind: vitest + path: test/e2e/qa-lab/media/media-reference-intake.product.test.ts + summary: Load plain-path, file URL, guarded loopback HTTP, and media-store references and verify byte-sniffed MIME and kind. diff --git a/test/e2e/qa-lab/media/media-reference-intake.product.test.ts b/test/e2e/qa-lab/media/media-reference-intake.product.test.ts new file mode 100644 index 000000000000..3160af1b9abe --- /dev/null +++ b/test/e2e/qa-lab/media/media-reference-intake.product.test.ts @@ -0,0 +1,104 @@ +import fs from "node:fs/promises"; +import { createServer, type Server } from "node:http"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; +import { saveMediaBuffer } from "../../../../src/media/store.js"; +import { loadWebMediaRaw } from "../../../../src/media/web-media.js"; +import { withEnvAsync } from "../../../../src/test-utils/env.js"; +import { createSolidPngBuffer } from "../../../helpers/image-fixtures.js"; +import { useAutoCleanupTempDirTracker } from "../../../helpers/temp-dir.js"; + +async function listenOnLoopback(server: Server): Promise { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("media fixture server did not expose a TCP port"); + } + return address.port; +} + +async function closeServer(server: Server): Promise { + const closed = new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + server.closeAllConnections(); + await closed; +} + +describe("media reference intake product proof", () => { + const tempDirs = useAutoCleanupTempDirTracker(afterEach); + let server: Server | undefined; + + afterEach(async () => { + if (server) { + await closeServer(server); + server = undefined; + } + }); + + it("loads every supported reference and trusts PNG bytes over text hints", async () => { + const root = await fs.realpath(tempDirs.make("openclaw-media-reference-intake-")); + const png = createSolidPngBuffer(2, 2, { r: 32, g: 96, b: 224 }); + const localPath = path.join(root, "misleading.txt"); + await fs.writeFile(localPath, png); + + const requestedPaths: string[] = []; + server = createServer((request, response) => { + requestedPaths.push(request.url ?? ""); + response.writeHead(200, { + "content-disposition": 'attachment; filename="remote.txt"', + "content-length": String(png.byteLength), + "content-type": "text/plain", + }); + response.end(png); + }); + const port = await listenOnLoopback(server); + const remoteUrl = `http://127.0.0.1:${port}/misleading.txt`; + + await withEnvAsync({ OPENCLAW_STATE_DIR: path.join(root, "state") }, async () => { + const stored = await saveMediaBuffer(png, "text/plain", "inbound", 1024 * 1024, "stored.txt"); + expect(stored.contentType).toBe("image/png"); + expect(stored.id).toMatch(/\.txt$/); + + const plainPath = await loadWebMediaRaw(localPath, { + localRoots: [root], + maxBytes: 1024 * 1024, + }); + const fileUrl = await loadWebMediaRaw(pathToFileURL(localPath).toString(), { + localRoots: [root], + maxBytes: 1024 * 1024, + }); + const remote = await loadWebMediaRaw(remoteUrl, { + maxBytes: 1024 * 1024, + ssrfPolicy: { allowedHostnames: ["127.0.0.1"] }, + }); + const mediaStore = await loadWebMediaRaw(`media://inbound/${stored.id}`, { + maxBytes: 1024 * 1024, + }); + + for (const result of [plainPath, fileUrl, remote, mediaStore]) { + expect(result.buffer).toEqual(png); + expect(result.contentType).toBe("image/png"); + expect(result.kind).toBe("image"); + } + + expect(plainPath.fileName).toBe("misleading.txt"); + expect(fileUrl.fileName).toBe("misleading.txt"); + expect(remote.fileName).toBe("remote.txt"); + expect(requestedPaths).toEqual(["/misleading.txt"]); + }); + }); +});