import { EnvironmentId, ThreadId } from "@t3tools/contracts";import { afterEach, describe, expect, it, vi } from "vite-plus/test"; import { navigateAfterThreadDeletion, requestThreadUnpinConfirmation, ThreadArchiveBlockedError,} from "./useThreadActions";import { toastManager } from "../components/ui/toast"; describe("navigateAfterThreadDeletion", () => { afterEach(() => vi.restoreAllMocks()); it("reports a rejected navigation without failing the completed deletion", async () => { const addToast = vi.spyOn(toastManager, "add").mockReturnValue("navigation-error"); await expect( navigateAfterThreadDeletion(() => Promise.reject(new Error("route unavailable"))), ).resolves.toBeUndefined(); expect(addToast).toHaveBeenCalledOnce(); expect(addToast).toHaveBeenCalledWith( expect.objectContaining({ title: "Thread deleted, but navigation failed", description: "route unavailable", }), ); }); it("does not report an error after successful navigation", async () => { const addToast = vi.spyOn(toastManager, "add"); await navigateAfterThreadDeletion(() => Promise.resolve()); expect(addToast).not.toHaveBeenCalled(); });}); describe("ThreadArchiveBlockedError", () => { it("keeps the blocked thread context with the fixed message", () => { const error = new ThreadArchiveBlockedError({ environmentId: EnvironmentId.make("environment-1"), threadId: ThreadId.make("thread-1"), }); expect(error).toMatchObject({ environmentId: "environment-1", threadId: "thread-1", }); expect(error.message).toBe("Cannot archive a running thread."); });}); describe("requestThreadUnpinConfirmation", () => { it("skips the dialog when confirmation is disabled", async () => { let callCount = 0; const result = await requestThreadUnpinConfirmation({ enabled: false, title: "Pinned thread", confirm: async () => { callCount += 1; return false; }, }); expect(result).toMatchObject({ _tag: "Success", value: true }); expect(callCount).toBe(0); }); it("degrades gracefully when dialogs are unavailable", async () => { const result = await requestThreadUnpinConfirmation({ enabled: true, title: "Pinned thread", confirm: null, }); expect(result).toMatchObject({ _tag: "Success", value: true }); }); it("uses the thread title and returns the user's decision", async () => { let message = ""; const result = await requestThreadUnpinConfirmation({ enabled: true, title: "Release prep", confirm: async (nextMessage) => { message = nextMessage; return false; }, }); expect(message).toBe( 'Unpin thread "Release prep"?\nThis will move the thread out of your pinned section.', ); expect(result).toMatchObject({ _tag: "Success", value: false }); }); it("keeps dialog failures observable", async () => { const result = await requestThreadUnpinConfirmation({ enabled: true, title: "Pinned thread", confirm: () => Promise.reject(new Error("dialog unavailable")), }); expect(result._tag).toBe("Failure"); });});
import { EnvironmentId, ThreadId } from "@t3tools/contracts";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import {
navigateAfterThreadDeletion,
requestThreadUnpinConfirmation,
ThreadArchiveBlockedError,
} from "./useThreadActions";
import { toastManager } from "../components/ui/toast";
describe("navigateAfterThreadDeletion", () => {
afterEach(() => vi.restoreAllMocks());
it("reports a rejected navigation without failing the completed deletion", async () => {
const addToast = vi.spyOn(toastManager, "add").mockReturnValue("navigation-error");
await expect(
navigateAfterThreadDeletion(() => Promise.reject(new Error("route unavailable"))),
).resolves.toBeUndefined();
expect(addToast).toHaveBeenCalledOnce();
expect(addToast).toHaveBeenCalledWith(
expect.objectContaining({
title: "Thread deleted, but navigation failed",
description: "route unavailable",
}),
);
});
it("does not report an error after successful navigation", async () => {
const addToast = vi.spyOn(toastManager, "add");
await navigateAfterThreadDeletion(() => Promise.resolve());
expect(addToast).not.toHaveBeenCalled();
});
});
describe("ThreadArchiveBlockedError", () => {
it("keeps the blocked thread context with the fixed message", () => {
const error = new ThreadArchiveBlockedError({
environmentId: EnvironmentId.make("environment-1"),
threadId: ThreadId.make("thread-1"),
});
expect(error).toMatchObject({
environmentId: "environment-1",
threadId: "thread-1",
});
expect(error.message).toBe("Cannot archive a running thread.");
});
});
describe("requestThreadUnpinConfirmation", () => {
it("skips the dialog when confirmation is disabled", async () => {
let callCount = 0;
const result = await requestThreadUnpinConfirmation({
enabled: false,
title: "Pinned thread",
confirm: async () => {
callCount += 1;
return false;
},
});
expect(result).toMatchObject({ _tag: "Success", value: true });
expect(callCount).toBe(0);
});
it("degrades gracefully when dialogs are unavailable", async () => {
const result = await requestThreadUnpinConfirmation({
enabled: true,
title: "Pinned thread",
confirm: null,
});
expect(result).toMatchObject({ _tag: "Success", value: true });
});
it("uses the thread title and returns the user's decision", async () => {
let message = "";
const result = await requestThreadUnpinConfirmation({
enabled: true,
title: "Release prep",
confirm: async (nextMessage) => {
message = nextMessage;
return false;
},
});
expect(message).toBe(
'Unpin thread "Release prep"?\nThis will move the thread out of your pinned section.',
);
expect(result).toMatchObject({ _tag: "Success", value: false });
});
it("keeps dialog failures observable", async () => {
const result = await requestThreadUnpinConfirmation({
enabled: true,
title: "Pinned thread",
confirm: () => Promise.reject(new Error("dialog unavailable")),
});
expect(result._tag).toBe("Failure");
});
});