265 lines · 8.6 KB · TypeScript
RawDownload
import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";import { EnvironmentId } from "@t3tools/contracts"; import { removeLocalStorageItem } from "./hooks/useLocalStorage"; import {  MAX_STASH_ENTRIES,  PROMPT_STASH_STORAGE_KEY,  MAX_STASH_ENTRY_ATTACHMENT_CHARS,  partitionStashAttachments,  usePromptStashStore,  writePromptStashStorageForTest,  type PromptStashEntry,} from "./promptStashStore"; function makeEntry(input: {  id: string;  prompt?: string;  attachmentChars?: number;}): PromptStashEntry {  return {    id: input.id,    createdAt: "2026-07-24T12:00:00.000Z",    prompt: input.prompt ?? `prompt ${input.id}`,    attachments:      input.attachmentChars !== undefined        ? [            {              id: `${input.id}-img`,              name: "shot.png",              mimeType: "image/png",              sizeBytes: input.attachmentChars,              dataUrl: "x".repeat(input.attachmentChars),            },          ]        : [],    droppedImageNames: [],  };} function resetPromptStashStore() {  usePromptStashStore.setState({ entries: [] });  writePromptStashStorageForTest("");  removeLocalStorageItem(PROMPT_STASH_STORAGE_KEY);} describe("partitionStashAttachments", () => {  it("keeps attachments within the budget and reports dropped names in order", () => {    const small = {      id: "a",      name: "small.png",      mimeType: "image/png",      sizeBytes: 10,      dataUrl: "x".repeat(10),    };    const huge = {      id: "b",      name: "huge.png",      mimeType: "image/png",      sizeBytes: MAX_STASH_ENTRY_ATTACHMENT_CHARS,      dataUrl: "x".repeat(MAX_STASH_ENTRY_ATTACHMENT_CHARS),    };    const alsoSmall = {      id: "c",      name: "also-small.png",      mimeType: "image/png",      sizeBytes: 10,      dataUrl: "x".repeat(10),    };    const { kept, droppedNames } = partitionStashAttachments([small, huge, alsoSmall]);    expect(kept.map((attachment) => attachment.id)).toEqual(["a", "c"]);    expect(droppedNames).toEqual(["huge.png"]);  });   it("admits a single attachment that exactly fits the budget", () => {    const exact = {      id: "a",      name: "exact.png",      mimeType: "image/png",      sizeBytes: MAX_STASH_ENTRY_ATTACHMENT_CHARS,      dataUrl: "x".repeat(MAX_STASH_ENTRY_ATTACHMENT_CHARS),    };    const { kept, droppedNames } = partitionStashAttachments([exact]);    expect(kept).toHaveLength(1);    expect(droppedNames).toEqual([]);  });}); describe("promptStashStore", () => {  beforeEach(() => {    resetPromptStashStore();  });   afterEach(() => {    resetPromptStashStore();  });   it("prepends entries so the newest stash is first", () => {    const store = usePromptStashStore.getState();    store.stashEntry(makeEntry({ id: "first" }));    store.stashEntry(makeEntry({ id: "second" }));    const entries = usePromptStashStore.getState().entries;    expect(entries.map((entry) => entry.id)).toEqual(["second", "first"]);  });   it("evicts the oldest entry past the cap and returns it", () => {    const store = usePromptStashStore.getState();    for (let index = 0; index < MAX_STASH_ENTRIES; index += 1) {      expect(store.stashEntry(makeEntry({ id: `entry-${index}` })).evicted).toBeNull();    }    const { evicted } = store.stashEntry(makeEntry({ id: "overflow" }));    expect(evicted?.id).toBe("entry-0");    const entries = usePromptStashStore.getState().entries;    expect(entries).toHaveLength(MAX_STASH_ENTRIES);    expect(entries[0]?.id).toBe("overflow");  });   // This test environment has no `localStorage`, so the store runs on its  // in-memory fallback — the exact "kept for this session, gone on reload"  // case the composer must distinguish from an outright write failure.  it("distinguishes a memory-only write (written, not durable) from a failed one", () => {    const store = usePromptStashStore.getState();    const result = store.stashEntry(makeEntry({ id: "memory-only" }));    expect(result.written).toBe(true);    expect(result.durable).toBe(false);    expect(usePromptStashStore.getState().entries.map((entry) => entry.id)).toEqual([      "memory-only",    ]);  });   it("takeEntry removes and returns the entry; second take returns null", () => {    const store = usePromptStashStore.getState();    store.stashEntry(makeEntry({ id: "keep" }));    store.stashEntry(makeEntry({ id: "take" }));    expect(store.takeEntry("take").entry?.id).toBe("take");    expect(store.takeEntry("take").entry).toBeNull();    const entries = usePromptStashStore.getState().entries;    expect(entries.map((entry) => entry.id)).toEqual(["keep"]);  });   it("finalizeEntryImages attaches images and clears the pending count", () => {    const store = usePromptStashStore.getState();    store.stashEntry({ ...makeEntry({ id: "pending" }), pendingImageCount: 2 });     const { attached } = store.finalizeEntryImages("pending", {      attachments: [        {          id: "img-1",          name: "a.webp",          mimeType: "image/webp",          sizeBytes: 10,          dataUrl: "data:image/webp;base64,AAAA",        },      ],      droppedImageNames: ["big.png"],      unreadableImageNames: [],    });     expect(attached).toBe(true);    const entry = usePromptStashStore.getState().entries[0];    expect(entry?.attachments).toHaveLength(1);    expect(entry?.droppedImageNames).toEqual(["big.png"]);    expect(entry?.pendingImageCount).toBe(0);  });   it("takeEntry returns images and drop metadata finalized after a menu snapshot", () => {    const store = usePromptStashStore.getState();    store.stashEntry({ ...makeEntry({ id: "pending-restore" }), pendingImageCount: 1 });    const menuSnapshot = usePromptStashStore.getState().entries[0];    expect(menuSnapshot?.attachments).toEqual([]);     store.finalizeEntryImages("pending-restore", {      attachments: [        {          id: "img-finalized",          name: "finalized.webp",          mimeType: "image/webp",          sizeBytes: 12,          dataUrl: "data:image/webp;base64,BBBB",        },      ],      droppedImageNames: ["too-large.png"],      unreadableImageNames: ["unreadable.png"],    });     const { entry: taken } = store.takeEntry("pending-restore");    expect(taken).not.toBe(menuSnapshot);    expect(taken).toMatchObject({      attachments: [        {          id: "img-finalized",          name: "finalized.webp",        },      ],      droppedImageNames: ["too-large.png"],      unreadableImageNames: ["unreadable.png"],      pendingImageCount: 0,    });  });   it("preserves uploaded file references without storing file contents", () => {    const store = usePromptStashStore.getState();    const file = {      id: "file-1",      name: "report.pdf",      mimeType: "application/pdf",      sizeBytes: 42,      attachmentId: "pending-report-pdf",      environmentId: EnvironmentId.make("environment-1"),    };     store.stashEntry({ ...makeEntry({ id: "with-file" }), files: [file] });    store.finalizeEntryImages("with-file", {      attachments: [],      droppedImageNames: [],      unreadableImageNames: [],    });     expect(usePromptStashStore.getState().entries[0]?.files).toEqual([file]);  });   it("finalizeEntryImages reports false when the entry was already taken", () => {    const store = usePromptStashStore.getState();    store.stashEntry({ ...makeEntry({ id: "racing" }), pendingImageCount: 1 });    // Restored (or deleted) while its images were still encoding.    store.takeEntry("racing");     const { attached } = store.finalizeEntryImages("racing", {      attachments: [],      droppedImageNames: [],      unreadableImageNames: [],    });     expect(attached).toBe(false);  });   it("settles a pending count left behind by a crashed or closed session", () => {    writePromptStashStorageForTest(      JSON.stringify({        version: 2,        state: {          entries: [{ ...makeEntry({ id: "orphan" }), pendingImageCount: 2 }],        },      }),    );     // Hydration must settle the stale count, or the entry would stay stuck    // showing "saving…" with images that no longer exist anywhere.    const entry = usePromptStashStore.getState().entries[0];    expect(entry?.pendingImageCount).toBe(0);    expect(entry?.unreadableImageNames).toHaveLength(2);  });   it("ignores an unreadable v1 payload seeded under the current key", () => {    // The v1 shape (per-provider queues) does not decode as v2; hydration    // must fall back to an empty stash rather than throw.    writePromptStashStorageForTest(      JSON.stringify({        version: 1,        state: { queuesByScopeKey: { "provider:claudeAgent": [] } },      }),    );    expect(usePromptStashStore.getState().entries).toEqual([]);  });});