// @vitest-environment jsdom
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter, useNavigate } from "@/lib/router";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import type { DecisionOutcome } from "@/api/decisions";
import type { DecisionHistoryItem } from "@paperclipai/shared";
import { CoordinatorDecisionReceipt } from "./coordinator/CoordinatorDecisionReceipt";
import { TaskDecisionRecord } from "./TaskDecisionRecord";
const mocks = vi.hoisted(() => ({ get: vi.fn(), resolver: vi.fn() }));
vi.mock("@/api/decisions", () => ({ decisionsApi: { get: mocks.get } }));
vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ selectedCompany: { issuePrefix: "ORG" } }) }));
vi.mock("./DecisionResolver", () => ({ DecisionResolver: (props: { recordedDecision?: DecisionOutcome }) => {
mocks.resolver(props);
return
{props.recordedDecision?.executions[0]?.result?.summary as string}
;
} }));
const decision = {
id: "decision-1", companyId: "company", originIssueId: "task-1", status: "decided",
title: "Choose launch channels", body: "Start with the blog", chosenOptionId: "blog",
options: [{ id: "blog", label: "Own blog first", effects: [] }],
inputs: [{ id: "reason", label: "Reason" }], inputValues: { reason: "The draft is ready" },
executions: [{ id: "effect-1", result: { summary: "Blog draft queued" } }],
} as unknown as DecisionOutcome;
let root: Root, host: HTMLDivElement, client: QueryClient;
beforeEach(() => {
vi.clearAllMocks(); mocks.get.mockResolvedValue(decision);
host = document.createElement("div"); document.body.append(host); root = createRoot(host);
client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
});
afterEach(async () => { await act(async () => root.unmount()); client.clear(); host.remove(); });
async function mount(hash: string, viewerId = "person") {
await act(async () => root.render(
));
}
it("opens a compact receipt in its task's named full record with recorded inputs and results", async () => {
function Journey() {
const navigate = useNavigate();
return <>
>;
}
await act(async () => root.render());
expect(mocks.get).not.toHaveBeenCalled();
await act(async () => host.querySelector("button")!.click());
await vi.waitFor(() => expect(host.querySelector("#decision-decision-1")?.textContent).toContain("Blog draft queued"));
expect(host.textContent).toContain("Own blog first");
expect(host.textContent).toContain("The draft is ready");
expect(mocks.get).toHaveBeenCalledWith("decision-1");
expect(mocks.resolver).toHaveBeenLastCalledWith(expect.objectContaining({ recordedDecision: decision }));
});
it.each([{ companyId: "other" }, { originIssueId: "another-task" }])("does not display a decision for a mismatched context %j", async (override) => {
mocks.get.mockResolvedValue({ ...decision, ...override });
await mount("#decision-decision-1");
await vi.waitFor(() => expect(host.textContent).toContain("not available in this task"));
expect(mocks.resolver).not.toHaveBeenCalled();
expect(host.textContent).not.toContain("The draft is ready");
});
it("does not expose cached record text after a viewer change loses access", async () => {
await mount("#decision-decision-1");
await vi.waitFor(() => expect(host.textContent).toContain("The draft is ready"));
mocks.get.mockRejectedValue(new Error("Forbidden"));
await mount("#decision-decision-1", "another-person");
await vi.waitFor(() => expect(host.textContent).toContain("Could not load this decision"));
expect(host.textContent).not.toContain("The draft is ready");
});
it("refreshes the mounted record after company decision events invalidate its results", async () => {
mocks.get.mockResolvedValue({ ...decision, executions: [] });
await mount("#decision-decision-1");
await vi.waitFor(() => expect(host.textContent).toContain("The draft is ready"));
expect(host.textContent).not.toContain("Blog draft queued");
mocks.get.mockResolvedValue(decision);
await act(async () => { await client.invalidateQueries({ queryKey: ["decisions", "company"] }); });
await vi.waitFor(() => expect(host.textContent).toContain("Blog draft queued"));
expect(mocks.get).toHaveBeenCalledTimes(2);
expect(mocks.resolver).toHaveBeenLastCalledWith(expect.objectContaining({ recordedDecision: decision }));
});