1import { EnvironmentId, ThreadId } from "@t3tools/contracts";
2import { afterEach, describe, expect, it, vi } from "vite-plus/test";
3
4import {
5 navigateAfterThreadDeletion,
6 requestThreadUnpinConfirmation,
7 ThreadArchiveBlockedError,
8} from "./useThreadActions";
9import { toastManager } from "../components/ui/toast";
10
11describe("navigateAfterThreadDeletion", () => {
12 afterEach(() => vi.restoreAllMocks());
13
14 it("reports a rejected navigation without failing the completed deletion", async () => {
15 const addToast = vi.spyOn(toastManager, "add").mockReturnValue("navigation-error");
16
17 await expect(
18 navigateAfterThreadDeletion(() => Promise.reject(new Error("route unavailable"))),
19 ).resolves.toBeUndefined();
20
21 expect(addToast).toHaveBeenCalledOnce();
22 expect(addToast).toHaveBeenCalledWith(
23 expect.objectContaining({
24 title: "Thread deleted, but navigation failed",
25 description: "route unavailable",
26 }),
27 );
28 });
29
30 it("does not report an error after successful navigation", async () => {
31 const addToast = vi.spyOn(toastManager, "add");
32
33 await navigateAfterThreadDeletion(() => Promise.resolve());
34
35 expect(addToast).not.toHaveBeenCalled();
36 });
37});
38
39describe("ThreadArchiveBlockedError", () => {
40 it("keeps the blocked thread context with the fixed message", () => {
41 const error = new ThreadArchiveBlockedError({
42 environmentId: EnvironmentId.make("environment-1"),
43 threadId: ThreadId.make("thread-1"),
44 });
45
46 expect(error).toMatchObject({
47 environmentId: "environment-1",
48 threadId: "thread-1",
49 });
50 expect(error.message).toBe("Cannot archive while the provider is active.");
51 });
52});
53
54describe("requestThreadUnpinConfirmation", () => {
55 it("skips the dialog when confirmation is disabled", async () => {
56 let callCount = 0;
57 const result = await requestThreadUnpinConfirmation({
58 enabled: false,
59 title: "Pinned thread",
60 confirm: async () => {
61 callCount += 1;
62 return false;
63 },
64 });
65
66 expect(result).toMatchObject({ _tag: "Success", value: true });
67 expect(callCount).toBe(0);
68 });
69
70 it("degrades gracefully when dialogs are unavailable", async () => {
71 const result = await requestThreadUnpinConfirmation({
72 enabled: true,
73 title: "Pinned thread",
74 confirm: null,
75 });
76
77 expect(result).toMatchObject({ _tag: "Success", value: true });
78 });
79
80 it("uses the thread title and returns the user's decision", async () => {
81 let message = "";
82 const result = await requestThreadUnpinConfirmation({
83 enabled: true,
84 title: "Release prep",
85 confirm: async (nextMessage) => {
86 message = nextMessage;
87 return false;
88 },
89 });
90
91 expect(message).toBe(
92 'Unpin thread "Release prep"?\nThis will move the thread out of your pinned section.',
93 );
94 expect(result).toMatchObject({ _tag: "Success", value: false });
95 });
96
97 it("keeps dialog failures observable", async () => {
98 const result = await requestThreadUnpinConfirmation({
99 enabled: true,
100 title: "Pinned thread",
101 confirm: () => Promise.reject(new Error("dialog unavailable")),
102 });
103
104 expect(result._tag).toBe("Failure");
105 });
106});