Browse / github.com/storytold/artcraft / patch
fix(promptbox): route M4A references as audio despite video MIME
fix(promptbox): route M4A references as audio despite video MIME ## Summary Route dropped and pasted audio references with the existing audio-file guard before trusting their MIME container type. Some platforms report `.m4a` as `video/mp4`, a case already documented by the audio picker guard. The prompt drop router currently rejects those files in audio prompts or sends them to the video bucket when video is supported. ## Verification - Actual mounted `usePromptBoxDrop` hook, ReactDOM, DOM drop and clipboard events: four failures plus five passing controls before the fix; all nine pass afterward. - Controls preserve genuine MP4 video, audio MIME without
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- rudycelekli (imported from pull request #2101, open)
- Base
28a39a0de4a2fc2eccc237df2046c24245bd12ab- Size
- +111 −1 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
b928d71667d6c008a7131fac
How it went
No other operator has reported using it yet.
Diff
raw · frontend/libs/components/promptbox/src/lib/deck/usePromptBoxDrop.spec.tsx frontend/libs/components/promptbox/src/lib/deck/usePromptBoxDrop.tsx
new file mode 100644@@ -0,0 +1,105 @@+import { act } from "react";+import { createRoot, Root } from "react-dom/client";+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";+import { usePromptBoxDrop, UsePromptBoxDropOptions } from "./usePromptBoxDrop";++const { error } = vi.hoisted(() => ({ error: vi.fn() }));+vi.mock("@storyteller/ui-toaster", () => ({ toast: { error } }));+vi.mock("@storyteller/tauri-utils", () => ({ IsDesktopApp: () => false }));+vi.mock("@storyteller/icons", () => ({ DynamicIcon: () => null }));+vi.mock("lucide-react", () => ({ ImagesIcon: () => null, MusicIcon: () => null, VideoIcon: () => null }));+vi.mock("tailwind-merge", () => ({ twMerge: (...values: string[]) => values.join(" ") }));++let root: Root;+let container: HTMLDivElement;++beforeEach(() => {+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;+ error.mockReset();+ container = document.createElement("div");+ document.body.appendChild(container);+ root = createRoot(container);+});++afterEach(() => {+ act(() => root.unmount());+ container.remove();+});++describe("prompt reference file routing", () => {+ it.each(["drop", "paste"] as const)("accepts M4A reported as video/mp4 through %s", (transport) => {+ const receive = mount({ acceptsAudio: true });+ const file = new File(["audio"], "voice.m4a", { type: "video/mp4" });+ deliver(transport, [file]);+ expect(receive).toHaveBeenCalledWith({ images: [], videos: [], audios: [file] });+ expect(error).not.toHaveBeenCalled();+ });++ it("routes M4A into audio when both video and audio are supported", () => {+ const receive = mount({ acceptsAudio: true, acceptsVideos: true });+ const file = new File(["audio"], "VOICE.M4A", { type: "video/mp4" });+ deliver("drop", [file]);+ expect(receive).toHaveBeenCalledWith({ images: [], videos: [], audios: [file] });+ });++ it("does not send M4A to a video-only model", () => {+ const receive = mount({ acceptsVideos: true });+ deliver("drop", [new File(["audio"], "voice.m4a", { type: "video/mp4" })]);+ expect(receive).not.toHaveBeenCalled();+ expect(error).toHaveBeenCalledWith("Audio references aren't available here");+ });++ it("preserves genuine MP4 video references", () => {+ const receive = mount({ acceptsVideos: true });+ const file = new File(["video"], "clip.mp4", { type: "video/mp4" });+ deliver("drop", [file]);+ expect(receive).toHaveBeenCalledWith({ images: [], videos: [file], audios: [] });+ });++ it("preserves audio MIME routing without a recognized extension", () => {+ const receive = mount({ acceptsAudio: true });+ const file = new File(["audio"], "recording", { type: "audio/mpeg" });+ deliver("paste", [file]);+ expect(receive).toHaveBeenCalledWith({ images: [], videos: [], audios: [file] });+ });++ it("preserves extension fallback for empty MIME", () => {+ const receive = mount({ acceptsAudio: true });+ const file = new File(["audio"], "voice.m4a");+ deliver("drop", [file]);+ expect(receive).toHaveBeenCalledWith({ images: [], videos: [], audios: [file] });+ });++ it("preserves image routing", () => {+ const receive = mount({ acceptsImages: true });+ const file = new File(["image"], "photo.png", { type: "image/png" });+ deliver("drop", [file]);+ expect(receive).toHaveBeenCalledWith({ images: [file], videos: [], audios: [] });+ });++ it("still rejects audio when the model only accepts images", () => {+ const receive = mount({ acceptsImages: true });+ deliver("paste", [new File(["audio"], "voice.mp3", { type: "audio/mpeg" })]);+ expect(receive).not.toHaveBeenCalled();+ expect(error).toHaveBeenCalledWith("Audio references aren't available here");+ });+});++function mount(capabilities: Partial<UsePromptBoxDropOptions>) {+ const receive = vi.fn();+ function Zone() {+ const drop = usePromptBoxDrop({ acceptsAudio: false, acceptsVideos: false, acceptsImages: false, ...capabilities, onDropFiles: receive });+ return <div {...drop.dropZoneProps} data-testid="zone" />;+ }+ act(() => root.render(<Zone />));+ return receive;+}++function deliver(transport: "drop" | "paste", files: File[]) {+ const event = new Event(transport, { bubbles: true, cancelable: true });+ Object.defineProperty(event, transport === "drop" ? "dataTransfer" : "clipboardData", { value: { files, types: ["Files"] } });+ act(() => {+ const target = transport === "drop" ? container.querySelector('[data-testid="zone"]')! : window;+ target.dispatchEvent(event);+ });+}@@ -4,6 +4,7 @@ import { ImagesIcon, MusicIcon, VideoIcon } from "lucide-react"; import { DynamicIcon } from "@storyteller/icons"; import { toast } from "@storyteller/ui-toaster"; import { IsDesktopApp } from "@storyteller/tauri-utils";+import { isAudioFile } from "../common/audioFiles"; // ── Types ─────────────────────────────────────────────────────────────── @@ -175,7 +176,11 @@ export function usePromptBoxDrop({ const rejectedKinds = new Set<DropKind>(); let unknownCount = 0; for (const file of files) {- const kind = kindOfMime(file.type) ?? kindOfPath(file.name);+ // Some platforms report M4A as video/mp4; use the same audio guard+ // as the reference picker before trusting that container MIME type.+ const kind = isAudioFile(file)+ ? "audio"+ : (kindOfMime(file.type) ?? kindOfPath(file.name)); if (kind === null) { unknownCount++; } else if (!acceptsKind(kind)) {