From a0d10049092edf2850df248185a3c34db61479e9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 2 Dec 2025 21:09:26 +0000 Subject: [PATCH] test(media): add redirect coverage and update changelog --- CHANGELOG.md | 5 +++ src/media/store.redirect.test.ts | 73 ++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/media/store.redirect.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b75347f..37884c256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ ### Performance - Web auto-replies using the Pi agent now keep a single long-lived `tau` process in RPC mode instead of spawning per message, eliminating cold-start latency while preserving session/cwd handling. +### Bug Fixes +- Media downloads now follow up to 5 redirects and still derive MIME/extension from sniffed content or headers; added regression test for redirected downloads. +- Hosted media responses set `Content-Type` from sniffed MIME (not the file name) and still clean up single-use files after send. +- Claude system prompt is guaranteed to be included on the first session turn even when `sendSystemOnce` is enabled, while later turns stay system-free. + ## 1.3.0 — 2025-12-02 ### Highlights diff --git a/src/media/store.redirect.test.ts b/src/media/store.redirect.test.ts new file mode 100644 index 000000000..b2f1dff11 --- /dev/null +++ b/src/media/store.redirect.test.ts @@ -0,0 +1,73 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { PassThrough } from "node:stream"; + +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +const realOs = await vi.importActual("node:os"); +const HOME = path.join(realOs.tmpdir(), "warelay-home-redirect"); +const mockRequest = vi.fn(); + +vi.doMock("node:os", () => ({ + default: { homedir: () => HOME }, + homedir: () => HOME, +})); + +vi.doMock("node:https", () => ({ + request: (...args: unknown[]) => mockRequest(...args), +})); + +const { saveMediaSource } = await import("./store.js"); + +describe("media store redirects", () => { + beforeAll(async () => { + await fs.rm(HOME, { recursive: true, force: true }); + }); + + afterAll(async () => { + await fs.rm(HOME, { recursive: true, force: true }); + vi.clearAllMocks(); + }); + + it("follows redirects and keeps detected mime/extension", async () => { + let call = 0; + mockRequest.mockImplementation((url, _opts, cb) => { + call += 1; + const res = new PassThrough(); + const req = { + on: (event: string, handler: (...args: unknown[]) => void) => { + if (event === "error") res.on("error", handler); + return req; + }, + end: () => undefined, + destroy: () => res.destroy(), + } as const; + + if (call === 1) { + res.statusCode = 302; + res.headers = { location: "https://example.com/final" }; + setImmediate(() => { + cb(res as unknown as Parameters[0]); + res.end(); + }); + } else { + res.statusCode = 200; + res.headers = { "content-type": "text/plain" }; + setImmediate(() => { + cb(res as unknown as Parameters[0]); + res.write("redirected"); + res.end(); + }); + } + + return req; + }); + + const saved = await saveMediaSource("https://example.com/start"); + + expect(mockRequest).toHaveBeenCalledTimes(2); + expect(saved.contentType).toBe("text/plain"); + expect(path.extname(saved.path)).toBe(".txt"); + expect(await fs.readFile(saved.path, "utf8")).toBe("redirected"); + }); +});