/** * @file Tests for the `/artifacts` page: the library lists what the server * answers (one merged list; All/My library/Hub filter and Share only in * server mode), an installed skill shows Installed, its install count lists * who installed it and where, a skill is * installed only after a confirmation (and a taken name asks to replace), * an HTML preview is a sandboxed iframe with no `allow-*` token, and a * skill's SKILL.md is rendered by the page itself. Adding: * a plugin is picked from the list its marketplace link answers (a link the * server cannot read opens the by-hand form), a skill is uploaded from one * Markdown file under the name the user types, or written in the dialog. * A card's right-click menu runs the same actions (Preview, Delete). A file * the sandbox cannot show (Office, zip, PDF) offers Download only, the * Add menu closes on Escape or a click outside it, the front page is shelved * by kind until a category is picked, and a card opens its detail sheet. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { Artifacts } from "../Artifacts"; import { api, type Artifact } from "../../lib/api"; import { confirmDialog } from "../../lib/confirm"; vi.mock("../../lib/confirm", () => ({ confirmDialog: vi.fn() })); vi.mock("../../lib/api", () => ({ api: { lanes: { list: vi.fn() }, artifacts: { settings: vi.fn(), list: vi.fn(), update: vi.fn(), remove: vi.fn(), install: vi.fn(), uninstall: vi.fn(), installs: vi.fn(), setInstallsEnabled: vi.fn(), skills: vi.fn(), addSkill: vi.fn(), addPlugin: vi.fn(), inspectMarket: vi.fn(), uploadSkills: vi.fn(), writeSkill: vi.fn(), uploadFile: vi.fn(), candidates: vi.fn(), rawUrl: (id: number, path?: string) => `/api/artifacts/${id}/raw?path=${path ?? ""}`, }, }, })); const item = (over: Partial): Artifact => ({ id: 1, kind: "file", title: "report.html", description: "", ownerId: 1, owner: "vi", mine: true, canChange: true, laneId: 1, sessionId: null, source: "out/report.html", market: null, url: null, files: [{ path: "report.html", size: 10 }], inLibrary: true, size: 10, shared: false, sharedAt: null, installs: 0, createdAt: "2026-10-14T00:00:00Z", updatedAt: "2026-10-14T00:00:00Z", ...over, }); const open = () => render( ); beforeEach(() => { vi.mocked(api.artifacts.settings).mockResolvedValue({ serverMode: false, canShare: false, installsEnabled: true, canManage: true, }); vi.mocked(api.artifacts.list).mockResolvedValue({ items: [ item({}), item({ id: 2, kind: "skill", title: "deploy", source: "deploy", files: [{ path: "SKILL.md", size: 3 }], }), ], }); vi.mocked(confirmDialog).mockReset(); vi.mocked(api.artifacts.install).mockReset(); vi.mocked(api.lanes.list).mockResolvedValue({ lanes: [], counts: {} } as never); vi.mocked(api.artifacts.skills).mockResolvedValue({ items: [] }); }); const addMenu = async (user: ReturnType, label: string) => { open(); await screen.findByText("deploy"); await user.click(screen.getByTestId("artifacts-add")); await user.click(screen.getByRole("button", { name: label })); }; describe("Adding", () => { it("adds the plugins picked from a marketplace link", async () => { const user = userEvent.setup(); vi.mocked(api.artifacts.inspectMarket).mockResolvedValue({ market: "https://git.x.vn/o/skills.git", name: "sg", description: "", plugins: [ { name: "sg-dev", key: "sg-dev@sg", description: "Dev", version: "", category: "" }, { name: "sg-common", key: "sg-common@sg", description: "", version: "", category: "" }, ], }); vi.mocked(api.artifacts.addPlugin).mockResolvedValue({ item: item({ kind: "plugin" }) }); await addMenu(user, "Plugin"); await user.type(screen.getByLabelText("Marketplace link"), "https://git.x.vn/o/skills.git"); await user.click(screen.getByRole("button", { name: "Read plugins" })); await screen.findByTestId("market-plugins"); await user.click(screen.getByLabelText("Select all")); await user.click(screen.getByRole("button", { name: "Add 2 plugin(s)" })); await waitFor(() => expect(api.artifacts.addPlugin).toHaveBeenCalledTimes(2)); expect(api.artifacts.addPlugin).toHaveBeenCalledWith({ key: "sg-dev@sg", market: "https://git.x.vn/o/skills.git", title: "sg-dev", description: "Dev", }); }); it("opens the by-hand form when the link cannot be read", async () => { const user = userEvent.setup(); vi.mocked(api.artifacts.inspectMarket).mockRejectedValue( Object.assign(new Error("needs a login"), { code: "ENEEDLOGIN" }) ); await addMenu(user, "Plugin"); await user.type(screen.getByLabelText("Marketplace link"), "https://git.x.vn/o/private.git"); await user.click(screen.getByRole("button", { name: "Read plugins" })); expect(await screen.findByRole("alert")).toHaveTextContent("needs a login"); expect(screen.getByLabelText("Marketplace (owner/repo or git URL)")).toHaveValue( "https://git.x.vn/o/private.git" ); }); it("uploads one Markdown file as a skill under the typed name", async () => { const user = userEvent.setup(); vi.mocked(api.artifacts.uploadSkills).mockResolvedValue({ items: [item({ kind: "skill" })] }); await addMenu(user, "Skill"); const md = new File(["# Steps"], "notes.md", { type: "text/markdown" }); await user.upload(screen.getByTestId("skill-files"), md); expect(await screen.findByTestId("skill-shape")).toHaveTextContent( "becomes the skill's SKILL.md" ); const name = screen.getByLabelText(/Skill name/); await waitFor(() => expect(name).toHaveValue("notes")); await user.clear(name); await user.type(name, "Viết báo cáo"); expect(screen.getByTestId("skill-slug")).toHaveTextContent("/Viet-bao-cao"); await user.type(screen.getByLabelText(/Description/), "Use for reports"); await user.click(within(screen.getByRole("dialog")).getByRole("button", { name: "Add" })); await waitFor(() => expect(api.artifacts.uploadSkills).toHaveBeenCalled()); const [files, fields] = vi.mocked(api.artifacts.uploadSkills).mock.calls[0]!; expect(files.map((f) => f.path)).toEqual(["notes.md"]); expect(fields).toMatchObject({ name: "Viết báo cáo", description: "Use for reports" }); }); it("writes a new skill", async () => { const user = userEvent.setup(); vi.mocked(api.artifacts.writeSkill).mockResolvedValue({ item: item({}), items: [] }); await addMenu(user, "Skill"); await user.click(screen.getByRole("tab", { name: "Write new" })); const add = within(screen.getByRole("dialog")).getByRole("button", { name: "Add" }); await user.type(screen.getByLabelText(/Skill name/), "standup"); await user.type(screen.getByLabelText(/Instructions/), "Summarise."); expect(add).toBeDisabled(); // no description yet await user.type(screen.getByLabelText(/Description/), "Daily standup"); await user.click(add); await waitFor(() => expect(api.artifacts.writeSkill).toHaveBeenCalledWith({ name: "standup", description: "Daily standup", content: "Summarise.", title: undefined, }) ); }); }); describe("Artifacts", () => { it("lists the library, with no Hub or Share in local mode", async () => { open(); expect(await screen.findByText("report.html")).toBeInTheDocument(); expect(screen.getByText("deploy")).toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Hub" })).toBeNull(); expect(screen.queryByRole("button", { name: /Share/ })).toBeNull(); }); it("shows the Hub and Share in server mode", async () => { vi.mocked(api.artifacts.settings).mockResolvedValue({ serverMode: true, canShare: true, installsEnabled: true, canManage: false, }); const user = userEvent.setup(); open(); expect(await screen.findByRole("button", { name: "Hub" })).toBeInTheDocument(); expect(screen.getAllByRole("button", { name: /^Share$/ }).length).toBe(2); // One merged list by default; Hub is a filter over it. expect(api.artifacts.list).toHaveBeenLastCalledWith(expect.objectContaining({ scope: "all" })); await user.click(screen.getByRole("button", { name: "Hub" })); await waitFor(() => expect(api.artifacts.list).toHaveBeenLastCalledWith(expect.objectContaining({ scope: "hub" })) ); }); it("lists who installed an artifact, when and where, from its install count", async () => { vi.mocked(api.artifacts.list).mockResolvedValue({ items: [item({ id: 2, kind: "skill", title: "deploy", source: "deploy", installs: 3 })], }); vi.mocked(api.artifacts.installs).mockResolvedValue({ items: [ { user: "vi", mine: true, at: "2026-10-19T10:00:00Z", replaced: false, paths: ["/h/.claude/skills/deploy"], }, { user: "khoa", mine: false, at: "2026-10-18T10:00:00Z", replaced: true }, ], unrecorded: 1, }); const user = userEvent.setup(); open(); await user.click(await screen.findByRole("button", { name: "3 installs" })); const box = await screen.findByTestId("artifact-installs"); expect(api.artifacts.installs).toHaveBeenCalledWith(2); expect(await within(box).findByText("/h/.claude/skills/deploy")).toBeInTheDocument(); expect(within(box).getByText(/khoa/)).toBeInTheDocument(); expect(within(box).getByText(/1 earlier installs/)).toBeInTheDocument(); await user.keyboard("{Escape}"); expect(screen.queryByTestId("artifact-installs")).toBeNull(); }); it("keeps a shared row in the Hub when its owner deletes it, and removes it from there on ask", async () => { vi.mocked(api.artifacts.settings).mockResolvedValue({ serverMode: true, canShare: true, installsEnabled: true, canManage: false, }); vi.mocked(api.artifacts.list).mockResolvedValue({ items: [item({ id: 3, title: "hub-only", shared: true, inLibrary: false })], }); vi.mocked(api.artifacts.update).mockResolvedValue({ item: item({ id: 3 }) } as never); const user = userEvent.setup(); open(); const card = await screen.findByTestId("artifact-3"); expect(within(card).getByText("Hub only")).toBeInTheDocument(); expect(within(card).queryByRole("button", { name: "Delete" })).toBeNull(); vi.mocked(confirmDialog).mockResolvedValueOnce(true); await user.click(within(card).getByRole("button", { name: "Remove from Hub" })); await waitFor(() => expect(api.artifacts.update).toHaveBeenCalledWith(3, { shared: false })); const calls = vi.mocked(confirmDialog).mock.calls; expect(calls[calls.length - 1]?.[0].title).toMatch(/Remove “hub-only” from the Hub/); }); it("shows Installed instead of Install for a skill already installed", async () => { vi.mocked(api.artifacts.list).mockResolvedValue({ items: [item({ id: 2, kind: "skill", title: "deploy", source: "deploy", installed: true })], }); open(); const card = await screen.findByTestId("artifact-2"); expect(within(card).getByTestId("artifact-installed")).toHaveTextContent("Installed"); expect(within(card).queryByRole("button", { name: "Install" })).toBeNull(); vi.mocked(confirmDialog).mockResolvedValueOnce(true); vi.mocked(api.artifacts.install).mockResolvedValueOnce({ installed: "/h/.claude/skills/deploy", }); await userEvent.setup().click(within(card).getByRole("button", { name: "Reinstall" })); await waitFor(() => expect(api.artifacts.install).toHaveBeenLastCalledWith(2, true)); }); it("uninstalls an installed plugin only after a confirmation", async () => { vi.mocked(api.artifacts.list).mockResolvedValue({ items: [ item({ id: 4, kind: "plugin", title: "tool", source: "tool@market", installed: true }), ], }); vi.mocked(api.artifacts.uninstall).mockReset(); open(); const card = await screen.findByTestId("artifact-4"); const user = userEvent.setup(); vi.mocked(confirmDialog).mockResolvedValueOnce(false); await user.click(within(card).getByRole("button", { name: "Uninstall" })); await waitFor(() => expect(confirmDialog).toHaveBeenCalled()); expect(api.artifacts.uninstall).not.toHaveBeenCalled(); vi.mocked(confirmDialog).mockResolvedValueOnce(true); vi.mocked(api.artifacts.uninstall).mockResolvedValueOnce({ uninstalled: "tool@market", paths: [], }); await user.click(within(card).getByRole("button", { name: "Uninstall" })); await waitFor(() => expect(api.artifacts.uninstall).toHaveBeenCalledWith(4)); }); it("installs a skill only after a confirmation, and asks before replacing", async () => { const user = userEvent.setup(); open(); await screen.findByText("deploy"); vi.mocked(confirmDialog).mockResolvedValueOnce(false); await user.click(screen.getByRole("button", { name: "Install" })); expect(api.artifacts.install).not.toHaveBeenCalled(); vi.mocked(confirmDialog).mockResolvedValue(true); vi.mocked(api.artifacts.install) .mockRejectedValueOnce(Object.assign(new Error("taken"), { code: "EEXISTS" })) .mockResolvedValueOnce({ installed: "/h/.claude/skills/deploy", replaced: true }); await user.click(screen.getByRole("button", { name: "Install" })); await waitFor(() => expect(api.artifacts.install).toHaveBeenLastCalledWith(2, true)); const calls = vi.mocked(confirmDialog).mock.calls; expect(calls[calls.length - 1]?.[0].title).toMatch(/Replace/); }); it("previews a file in a sandboxed iframe that may run nothing", async () => { const user = userEvent.setup(); open(); const card = await screen.findByTestId("artifact-1"); await user.click(within(card).getByRole("button", { name: "Preview" })); const frame = await screen.findByTestId("artifact-preview"); expect(frame.getAttribute("sandbox")).toBe(""); expect(frame.getAttribute("src")).toBe("/api/artifacts/1/raw?path=report.html"); }); it("renders a skill's SKILL.md itself instead of an iframe", async () => { vi.mocked(api.artifacts.list).mockResolvedValue({ items: [ item({ kind: "skill", title: "standup", files: [ { path: "SKILL.md", size: 10 }, { path: "notes.txt", size: 5 }, ], }), ], }); const fetchMock = vi.fn(async (url: string) => url.includes("SKILL.md") ? new Response("---\nname: standup\n---\n# Daily standup\n\nSummarise.") : new Response("plain notes") ); vi.stubGlobal("fetch", fetchMock); const user = userEvent.setup(); open(); await screen.findByText("standup"); await user.click(screen.getByRole("button", { name: "Preview" })); expect(await screen.findByRole("heading", { name: "Daily standup" })).toBeInTheDocument(); expect(screen.getByTestId("skill-summary")).toHaveTextContent("/out/report.html"); expect(screen.queryByTestId("artifact-preview")).toBeNull(); await user.click(screen.getByRole("button", { name: "notes.txt" })); expect(await screen.findByText("plain notes")).toBeInTheDocument(); vi.unstubAllGlobals(); }); it("edits a skill's name, description and instructions from its SKILL.md", async () => { vi.mocked(api.artifacts.list).mockResolvedValue({ items: [ item({ kind: "skill", title: "standup", source: "standup", files: [{ path: "SKILL.md", size: 10 }], }), ], }); vi.mocked(api.artifacts.update).mockResolvedValue({ item: item({}) } as never); vi.stubGlobal( "fetch", vi.fn(async () => new Response("---\nname: standup\ndescription: Old\n---\n\nOld body")) ); const user = userEvent.setup(); open(); await screen.findByText("standup"); await user.click(screen.getByRole("button", { name: "Edit" })); const name = await screen.findByLabelText(/Skill name/); expect(name).toHaveValue("standup"); expect(screen.getByLabelText(/Instructions/)).toHaveValue("Old body"); await user.clear(screen.getByLabelText(/Description/)); await user.type(screen.getByLabelText(/Description/), "New"); await user.click(screen.getByRole("button", { name: "Save" })); await waitFor(() => expect(api.artifacts.update).toHaveBeenCalledWith(1, { title: "standup", skill: { name: "standup", description: "New", content: "Old body" }, }) ); vi.unstubAllGlobals(); }); it("opens a card's actions on right-click", async () => { const user = userEvent.setup(); open(); await screen.findByText("report.html"); fireEvent.contextMenu(screen.getByTestId("artifact-1")); const menu = screen.getByTestId("artifact-menu"); expect(within(menu).getByRole("menuitem", { name: "Delete" })).toBeInTheDocument(); expect(within(menu).queryByRole("menuitem", { name: "Install" })).toBeNull(); await user.click(within(menu).getByRole("menuitem", { name: "Preview" })); expect(screen.queryByTestId("artifact-menu")).toBeNull(); expect((await screen.findByTestId("artifact-preview")).getAttribute("src")).toBe( "/api/artifacts/1/raw?path=report.html" ); }); it("offers no preview for a file the sandbox cannot show, only its download", async () => { vi.mocked(api.artifacts.list).mockResolvedValue({ items: [item({ title: "plan.docx", files: [{ path: "plan.docx", size: 10 }] })], }); open(); const card = await screen.findByTestId("artifact-1"); expect(within(card).queryByRole("button", { name: "Preview" })).toBeNull(); expect(within(card).getByRole("link", { name: "Download" })).toBeInTheDocument(); }); it("closes the Add menu on Escape and on a click outside it", async () => { const user = userEvent.setup(); open(); await screen.findByText("deploy"); await user.click(screen.getByTestId("artifacts-add")); expect(screen.getByRole("button", { name: "Skill" })).toBeInTheDocument(); await user.keyboard("{Escape}"); expect(screen.queryByRole("button", { name: "Skill" })).toBeNull(); await user.click(screen.getByTestId("artifacts-add")); await user.click(screen.getByText("report.html")); expect(screen.queryByRole("button", { name: "Skill" })).toBeNull(); }); it("shelves the front page by kind, and a card opens its detail sheet", async () => { const user = userEvent.setup(); open(); await screen.findByText("deploy"); const shelves = screen.getAllByRole("heading", { level: 2 }).map((h) => h.textContent); expect(shelves).toEqual(["Skills1", "Files1"]); await user.click(screen.getAllByRole("button", { name: "See all" })[0]!); await waitFor(() => expect(api.artifacts.list).toHaveBeenLastCalledWith( expect.objectContaining({ kind: "skill" }) ) ); await user.click(screen.getByText("deploy")); const sheet = await screen.findByTestId("artifact-detail"); expect(within(sheet).getByText("/deploy")).toBeInTheDocument(); expect(within(sheet).getByRole("button", { name: "Install" })).toBeInTheDocument(); }); });