1import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type Thread } from "./types";
5import { makeThreadFixture } from "./test-fixtures";
6import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "./worktreeCleanup";
7
8const localEnvironmentId = EnvironmentId.make("environment-local");
9
10function makeThread(overrides: Partial<Thread> = {}): Thread {
11 return makeThreadFixture({
12 id: ThreadId.make("thread-1"),
13 environmentId: localEnvironmentId,
14 projectId: ProjectId.make("project-1"),
15 title: "Thread",
16 modelSelection: {
17 instanceId: ProviderInstanceId.make("codex"),
18 model: "gpt-5.3-codex",
19 },
20 runtimeMode: DEFAULT_RUNTIME_MODE,
21 interactionMode: DEFAULT_INTERACTION_MODE,
22 runtime: null,
23 messages: [],
24 proposedPlans: [],
25 createdAt: "2026-02-13T00:00:00.000Z",
26 updatedAt: "2026-02-13T00:00:00.000Z",
27 archivedAt: null,
28 settledOverride: null,
29 settledAt: null,
30 deletedAt: null,
31 latestRun: null,
32 branch: null,
33 worktreePath: null,
34 ...overrides,
35 });
36}
37
38describe("getOrphanedWorktreePathForThread", () => {
39 it("returns null when the target thread does not exist", () => {
40 const result = getOrphanedWorktreePathForThread([], ThreadId.make("missing-thread"));
41 expect(result).toBeNull();
42 });
43
44 it("returns null when the target thread has no worktree", () => {
45 const threads = [makeThread()];
46 const result = getOrphanedWorktreePathForThread(threads, ThreadId.make("thread-1"));
47 expect(result).toBeNull();
48 });
49
50 it("returns the path when no other thread links to that worktree", () => {
51 const threads = [makeThread({ worktreePath: "/tmp/repo/worktrees/feature-a" })];
52 const result = getOrphanedWorktreePathForThread(threads, ThreadId.make("thread-1"));
53 expect(result).toBe("/tmp/repo/worktrees/feature-a");
54 });
55
56 it("returns null when another thread links to the same worktree", () => {
57 const threads = [
58 makeThread({
59 id: ThreadId.make("thread-1"),
60 worktreePath: "/tmp/repo/worktrees/feature-a",
61 }),
62 makeThread({
63 id: ThreadId.make("thread-2"),
64 worktreePath: "/tmp/repo/worktrees/feature-a",
65 }),
66 ];
67 const result = getOrphanedWorktreePathForThread(threads, ThreadId.make("thread-1"));
68 expect(result).toBeNull();
69 });
70
71 it("ignores threads linked to different worktrees", () => {
72 const threads = [
73 makeThread({
74 id: ThreadId.make("thread-1"),
75 worktreePath: "/tmp/repo/worktrees/feature-a",
76 }),
77 makeThread({
78 id: ThreadId.make("thread-2"),
79 worktreePath: "/tmp/repo/worktrees/feature-b",
80 }),
81 ];
82 const result = getOrphanedWorktreePathForThread(threads, ThreadId.make("thread-1"));
83 expect(result).toBe("/tmp/repo/worktrees/feature-a");
84 });
85});
86
87describe("formatWorktreePathForDisplay", () => {
88 it("shows only the last path segment for unix-like paths", () => {
89 const result = formatWorktreePathForDisplay(
90 "/Users/julius/.t3/worktrees/t3code-mvp/t3code-4e609bb8",
91 );
92 expect(result).toBe("t3code-4e609bb8");
93 });
94
95 it("normalizes windows separators before selecting the final segment", () => {
96 const result = formatWorktreePathForDisplay(
97 "C:\\Users\\julius\\.t3\\worktrees\\t3code-mvp\\t3code-4e609bb8",
98 );
99 expect(result).toBe("t3code-4e609bb8");
100 });
101
102 it("uses the final segment even when outside ~/.t3/worktrees", () => {
103 const result = formatWorktreePathForDisplay("/tmp/custom-worktrees/my-worktree");
104 expect(result).toBe("my-worktree");
105 });
106
107 it("ignores trailing slashes", () => {
108 const result = formatWorktreePathForDisplay("/tmp/custom-worktrees/my-worktree/");
109 expect(result).toBe("my-worktree");
110 });
111});