diff --git a/ui/storybook/fixtures/workFolders.test.ts b/ui/storybook/fixtures/workFolders.test.ts new file mode 100644 index 0000000000..b286ad521a --- /dev/null +++ b/ui/storybook/fixtures/workFolders.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import type { WorkFile } from "@paperclipai/shared"; +import { createWorkFolderFixture } from "./workFolders"; + +const root = + "http://storybook.test/api/companies/company-storybook/work-folders/task/issue-storybook-1"; +type Fixture = ReturnType; +function request(fixture: Fixture, suffix = "", init?: RequestInit) { + return fixture.handle(new Request(`${root}${suffix}`, init)); +} +async function files(fixture: Fixture, trash = false): Promise { + return (await (await request(fixture, `?trash=${trash}`))!.json()).files; +} +function operation( + fixture: Fixture, + action: string, + fields: Record, +) { + return request(fixture, "/operations", { + method: "POST", + body: JSON.stringify({ action, ...fields }), + }); +} + +describe("work-folder Storybook fixtures", () => { + it("keeps uploads within one owner and resets them for the next story", async () => { + const fixture = createWorkFolderFixture("empty"); + const bytes = new Uint8Array([0, 255, 128, 10]); + await request(fixture, "/content?path=nested%2Fsample.bin", { + method: "PUT", + body: bytes, + }); + const downloaded = await request( + fixture, + "/content?path=nested%2Fsample.bin", + ); + expect(new Uint8Array(await downloaded!.arrayBuffer())).toEqual(bytes); + expect(await files(fixture)).toHaveLength(1); + const otherOwner = await fixture.handle( + new Request(root.replace("/task/issue-storybook-1", "/user/user-board")), + ); + expect((await otherOwner!.json()).files).toEqual([]); + expect(await files(createWorkFolderFixture("empty"))).toEqual([]); + expect( + await fixture.handle( + new Request( + "http://storybook.test/api/companies/real-company/work-folders/task/task-1", + ), + ), + ).toBeNull(); + }); + + it("restores a deleted directory and purges its content without affecting siblings", async () => { + const fixture = createWorkFolderFixture("empty"); + await operation(fixture, "mkdir", { path: "notes" }); + await request(fixture, "/content?path=notes%2Fempty.txt", { + method: "PUT", + body: "", + }); + await request(fixture, "/content?path=notes-extra.txt", { + method: "PUT", + body: "Keep me", + }); + await operation(fixture, "delete", { path: "notes" }); + expect((await files(fixture)).map((file) => file.path)).toEqual([ + "notes-extra.txt", + ]); + const directory = (await files(fixture, true)).find( + (file) => file.kind === "directory", + )!; + await operation(fixture, "restore", { fileId: directory.id }); + expect(await files(fixture, true)).toEqual([]); + expect( + await (await request(fixture, "/content?path=notes%2Fempty.txt"))!.text(), + ).toBe(""); + await operation(fixture, "delete", { path: "notes" }); + await operation(fixture, "purge", { fileId: directory.id }); + expect(await files(fixture, true)).toEqual([]); + expect( + (await request(fixture, "/content?path=notes%2Fempty.txt"))!.status, + ).toBe(404); + expect( + await (await request(fixture, "/content?path=notes-extra.txt"))!.text(), + ).toBe("Keep me"); + }); + + it("preserves existing content when an upload failure is demonstrated", async () => { + const fixture = createWorkFolderFixture("uploadFailed"); + const before = await (await request( + fixture, + "/content?path=README.md", + ))!.text(); + const response = await request(fixture, "/content?path=README.md", { + method: "PUT", + body: "replacement", + }); + expect(response!.status).toBe(503); + expect( + await (await request(fixture, "/content?path=README.md"))!.text(), + ).toBe(before); + }); +}); diff --git a/ui/storybook/fixtures/workFolders.ts b/ui/storybook/fixtures/workFolders.ts index f8a2ebbb23..7949c39ea9 100644 --- a/ui/storybook/fixtures/workFolders.ts +++ b/ui/storybook/fixtures/workFolders.ts @@ -180,6 +180,11 @@ export function createWorkFolderFixture( } const path = url.searchParams.get("path") ?? ""; if (action === "content" && request.method === "GET") { + if ( + !state.files.some((file) => file.path === path && file.kind === "file") + ) { + return fail("File not found", 404); + } if (path === "assets/paperclip.png") return fetch("/android-chrome-192x192.png", { signal: request.signal }); const blob = state.contents.get(path); @@ -245,8 +250,12 @@ export function createWorkFolderFixture( state.files.forEach((f) => { if (inDeletedTree(f)) f.deletedAt = null; }); - if (op.action === "purge") + if (op.action === "purge") { + for (const file of state.files.filter(inDeletedTree)) { + state.contents.delete(file.path); + } state.files = state.files.filter((f) => !inDeletedTree(f)); + } } else return fail("Unsupported fixture operation", 405); state.changed = true; return Response.json({ ok: true });