// @vitest-environment jsdom
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { WorkspaceReconstructionRecovery } from "./WorkspaceReconstructionRecovery";
import { queryKeys } from "../lib/queryKeys";
const api = vi.hoisted(() => ({ listAttachments: vi.fn(), reconstructWorkspace: vi.fn() }));
vi.mock("../api/issues", () => ({ issuesApi: api }));
describe("rebuild a destroyed workspace from partial sources", () => {
let root: Root, container: HTMLDivElement, client: QueryClient;
const onRecorded = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
api.listAttachments.mockResolvedValue([{ id: "source", originalFilename: "baseline.ts", byteSize: 100 }]);
api.reconstructWorkspace.mockResolvedValue({ status: "queued" });
container = document.createElement("div"); document.body.append(container); root = createRoot(container);
client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
});
afterEach(async () => { await act(async () => root.unmount()); client.clear(); container.remove(); });
async function mount(canManage = true) {
await act(async () => root.render(
));
await act(async () => { await new Promise(resolve => setTimeout(resolve, 10)); });
}
async function fill() {
await act(async () => {
for (const area of container.querySelectorAll("textarea")) {
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")!.set!.call(area, "Verified source and receipts show what must be rebuilt without repeating published work.");
area.dispatchEvent(new Event("input", { bubbles: true }));
}
const checkboxes = container.querySelectorAll('[role="checkbox"]');
checkboxes[0].click(); checkboxes[1].click();
});
}
it("requires surviving sources, lost-work acknowledgement and outcome evidence without claiming original-file recovery", async () => {
await mount();
const button = Array.from(container.querySelectorAll("button")).find(button => button.textContent === "Rebuild and continue")!;
expect(button.disabled).toBe(true);
expect(container.textContent).toContain("missing edits have not been recovered");
expect(container.textContent).toContain("Existing work limits still apply");
await fill(); expect(button.disabled).toBe(false);
await act(async () => button.click());
expect(api.reconstructWorkspace).toHaveBeenCalledWith("task", expect.objectContaining({ runId: "lost-run", expectedStatusVersion: 3, attachmentIds: ["source"], acknowledgeLostWork: true, actionOutcome: "mixed" }));
expect(onRecorded).toHaveBeenCalledOnce();
});
it("preserves the reconstruction decision when current source checks reject it", async () => {
api.reconstructWorkspace.mockRejectedValue(new Error("The source owner changed."));
await mount(); await fill();
await act(async () => Array.from(container.querySelectorAll("button")).find(button => button.textContent === "Rebuild and continue")!.click());
await act(async () => { await new Promise(resolve => setTimeout(resolve, 10)); });
expect(container.querySelector('[role="alert"]')?.textContent).toContain("source owner changed");
expect(container.querySelector("textarea")?.value).toContain("Verified source");
expect(onRecorded).not.toHaveBeenCalled();
});
it("offers no reconstruction form to a reader without runtime management authority", async () => {
await mount(false); expect(container.querySelector("button")).toBeNull();
expect(api.listAttachments).not.toHaveBeenCalled(); expect(api.reconstructWorkspace).not.toHaveBeenCalled();
});
it("refreshes uploaded sources with the task attachment cache and drops deleted selections", async () => {
await mount(); await fill();
await act(async () => client.setQueryData(queryKeys.issues.attachments("task"), [
{ id: "new-source", originalFilename: "uploaded-source.ts", byteSize: 200 },
]));
await act(async () => { await new Promise(resolve => setTimeout(resolve, 10)); });
expect(container.textContent).toContain("uploaded-source.ts");
expect(container.textContent).not.toContain("baseline.ts");
const button = Array.from(container.querySelectorAll("button")).find(button => button.textContent === "Rebuild and continue")!;
expect(button.disabled).toBe(true);
expect(api.reconstructWorkspace).not.toHaveBeenCalled();
});
});