1import { EnvironmentId, ThreadId, TurnItemId } from "@t3tools/contracts";
2import * as DateTime from "effect/DateTime";
3import { scopedThreadKey } from "@t3tools/client-runtime/environment";
4import { beforeEach, describe, expect, it } from "vite-plus/test";
5
6import {
7 resolveToolCallDiffTarget,
8 useDiffPanelSessionStore,
9 useDiffPanelStore,
10} from "./diffPanelStore";
11
12describe("diffPanelStore", () => {
13 beforeEach(() => {
14 useDiffPanelStore.setState({
15 byThreadKey: {},
16 branchBaseRefByThreadKey: {},
17 });
18 useDiffPanelSessionStore.setState({ collapsedFileKeysByScopeKey: {} });
19 });
20
21 it("keeps a tool call selection independent of checkpoints and allows returning to Git changes", () => {
22 const ref = { environmentId: EnvironmentId.make("env"), threadId: ThreadId.make("thread") };
23 const target = {
24 threadId: ThreadId.make("child"),
25 itemId: TurnItemId.make("edit"),
26 revision: "1",
27 };
28 const store = useDiffPanelStore.getState();
29 store.selectToolCall(ref, target);
30 store.reconcileTurnSelection(ref, []);
31 expect(useDiffPanelStore.getState().byThreadKey[scopedThreadKey(ref)]).toEqual({
32 kind: "tool-call",
33 ...target,
34 });
35 store.selectGitScope(ref, "branch");
36 expect(useDiffPanelStore.getState().byThreadKey[scopedThreadKey(ref)]).toEqual({
37 kind: "branch",
38 baseRef: null,
39 });
40 });
41
42 it("refreshes a live tool-call target when its source item completes", () => {
43 const target = {
44 threadId: ThreadId.make("child"),
45 itemId: TurnItemId.make("edit"),
46 revision: "live",
47 };
48 const now = DateTime.makeUnsafe("2026-10-28T00:00:00Z");
49 const row = {
50 position: 0,
51 visibility: "local" as const,
52 sourceThreadId: target.threadId,
53 sourceItemId: target.itemId,
54 item: {
55 id: target.itemId,
56 threadId: target.threadId,
57 runId: null,
58 nodeId: null,
59 providerThreadId: null,
60 providerTurnId: null,
61 nativeItemRef: null,
62 parentItemId: null,
63 ordinal: 1,
64 status: "running" as const,
65 title: null,
66 startedAt: now,
67 completedAt: null,
68 updatedAt: now,
69 type: "file_change" as const,
70 fileName: "a.ts",
71 },
72 };
73 expect(resolveToolCallDiffTarget(target, [row])).toBe(target);
74 const completed = {
75 ...row,
76 item: { ...row.item, status: "completed" as const, completedAt: now },
77 };
78 expect(resolveToolCallDiffTarget(target, [completed])).toEqual({
79 ...target,
80 revision: DateTime.formatIso(now),
81 });
82 expect(
83 resolveToolCallDiffTarget(target, [{ ...completed, sourceThreadId: ThreadId.make("other") }]),
84 ).toBe(target);
85 expect(resolveToolCallDiffTarget(target, [])).toBe(target);
86 });
87
88 it("retains fold choices when a panel consumer returns with fresh defaults", () => {
89 const scopeKey = "environment-1:thread-1:branch";
90 const defaults = new Set(["src/app.ts", "src/index.ts"]);
91 useDiffPanelSessionStore.getState().updateCollapsedFileKeys(scopeKey, defaults, (current) => {
92 const next = new Set(current);
93 next.delete("src/app.ts");
94 return next;
95 });
96
97 expect(useDiffPanelSessionStore.getState().collapsedFileKeysByScopeKey[scopeKey]).toEqual(
98 new Set(["src/index.ts"]),
99 );
100
101 useDiffPanelSessionStore.getState().updateCollapsedFileKeys(scopeKey, new Set(), (current) => {
102 const next = new Set(current);
103 next.delete("src/index.ts");
104 return next;
105 });
106 expect(useDiffPanelSessionStore.getState().collapsedFileKeysByScopeKey[scopeKey]).toEqual(
107 new Set(),
108 );
109
110 useDiffPanelSessionStore
111 .getState()
112 .updateCollapsedFileKeys(scopeKey, defaults, (current) => current);
113 expect(useDiffPanelSessionStore.getState().collapsedFileKeysByScopeKey[scopeKey]).toEqual(
114 new Set(),
115 );
116 expect(defaults).toEqual(new Set(["src/app.ts", "src/index.ts"]));
117 });
118});