Open History to see the last commit
1import { EnvironmentId, ThreadId, type OrchestrationV2ThreadShell } from "@t3tools/contracts";
2import * as Cause from "effect/Cause";
3import { expect, it, vi } from "vite-plus/test";
4
5import { act, render, type Screen } from "../../../test/render";
6
7const { read, toast } = vi.hoisted(() => ({
8 read: vi.fn<(input: unknown) => Promise<unknown>>(),
9 toast: vi.fn(),
10}));
11
12const childEnvironmentId = EnvironmentId.make("child-environment");
13const childThreadId = ThreadId.make("child-thread");
14const parentEnvironmentId = EnvironmentId.make("parent-environment");
15// The panel reads only identity, lineage and delegation from the current thread.
16const child = {
17 id: childThreadId,
18 title: "Child",
19 lineage: { rootThreadId: childThreadId, parentThreadId: null, relationshipToParent: null },
20 forkedFrom: null,
21 delegatedFrom: {
22 environmentId: parentEnvironmentId,
23 threadId: ThreadId.make("parent-thread"),
24 title: "Parent",
25 },
26} as Partial<OrchestrationV2ThreadShell> as OrchestrationV2ThreadShell;
27
28vi.mock(import("../../state/orchestration"), async (original) => ({
29 ...(await original()),
30 threadTitlesRead: {
31 label: "thread-titles-read",
32 run: (_registry: unknown, input: unknown) => read(input),
33 } as never,
34}));
35vi.mock(import("../../state/entities"), async (original) => ({
36 ...(await original()),
37 useThreadProjection: () => null,
38 useThreadRelationshipShells: () => [child],
39 // The parent is absent from this client's shell, as for a parent never loaded here.
40 useQuietThreadShell: () => null,
41}));
42vi.mock(import("../../state/environments"), async (original) => ({
43 ...(await original()),
44 useEnvironment: (() => ({ label: "Linked" })) as never,
45}));
46vi.mock(import("../ui/toast"), async (original) => {
47 const actual = await original();
48 return { ...actual, toastManager: { ...actual.toastManager, add: toast } };
49});
50
51import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl";
52
53const childPath = `/${childEnvironmentId}/${childThreadId}`;
54const parentPath = `/${parentEnvironmentId}/parent-thread`;
55
56const live = { _tag: "Success", value: { "parent-thread": "Parent" } };
57const deleted = { _tag: "Success", value: {} };
58
59// Renders the child's panel; each read of its parent answers with the next of `replies`.
60function renderChild(...replies: ReadonlyArray<unknown>) {
61 read.mockReset();
62 for (const reply of replies) read.mockImplementationOnce(async () => reply);
63 toast.mockReset();
64 return render(
65 <ThreadRelationshipsPanel
66 environmentId={childEnvironmentId}
67 threadId={childThreadId}
68 showLineage
69 showSideChats={false}
70 />,
71 { router: childPath },
72 );
73}
74
75const parentRow = (screen: Screen) => screen.getByRole("button", { name: /Parent/ });
76
77it.each([
78 {
79 parent: "deleted there",
80 reply: deleted,
81 feedback: { type: "info", title: "The parent thread was deleted" },
82 },
83 {
84 parent: "unreadable",
85 reply: {
86 _tag: "Failure",
87 cause: Cause.fail(new Error("Failed to load orchestration V2 thread")),
88 },
89 feedback: { type: "error", title: "Could not open the parent thread" },
90 },
91])("keeps the child open with feedback when its remote parent is $parent", async (row) => {
92 await using screen = await renderChild(row.reply);
93 await parentRow(screen).click();
94 await vi.waitFor(() => expect(toast).toHaveBeenCalledWith(row.feedback));
95 expect(read).toHaveBeenCalledWith({
96 environmentId: parentEnvironmentId,
97 input: { threadIds: ["parent-thread"] },
98 });
99 expect(screen.router?.state.location.pathname).toBe(childPath);
100});
101
102it("opens a remote parent this client has not loaded", async () => {
103 await using screen = await renderChild(live);
104 await parentRow(screen).click();
105 await vi.waitFor(() => expect(screen.router?.state.location.pathname).toBe(parentPath));
106 expect(toast).not.toHaveBeenCalled();
107});
108
109it("leaves the user where they went when the parent read answers late", async () => {
110 let answerLate!: (reply: unknown) => void;
111 const late = new Promise((resolve) => {
112 answerLate = resolve;
113 });
114 await using screen = await renderChild(late, deleted);
115 await parentRow(screen).click();
116 await screen.router?.navigate({ to: "/new" });
117 await act(async () => {
118 answerLate(live);
119 await late;
120 });
121 expect(toast).not.toHaveBeenCalled();
122 expect(screen.router?.latestLocation.pathname).toBe("/new");
123 await parentRow(screen).click();
124 await vi.waitFor(() => expect(read).toHaveBeenCalledTimes(2));
125 await vi.waitFor(() => expect(toast).toHaveBeenCalledOnce());
126 expect(screen.router?.latestLocation.pathname).toBe("/new");
127});