1import {
2 EnvironmentId,
3 MessageId,
4 ProjectId,
5 ProviderInstanceId,
6 ThreadId,
7} from "@t3tools/contracts";
8import { serializeAssistantCitation } from "@t3tools/shared/assistantCitations";
9import { describe, expect, it } from "vite-plus/test";
10
11import {
12 buildProjectThreadStartTurnInput,
13 deriveThreadTitleFromPrompt,
14} from "./projectThreadStartTurn";
15
16describe("project thread title", () => {
17 it("keeps ordinary titles and the empty-prompt fallback", () => {
18 expect(deriveThreadTitleFromPrompt(" Fix\n the parser ")).toBe("Fix the parser");
19 expect(deriveThreadTitleFromPrompt(" \n ")).toBe("New thread");
20 });
21
22 it("derives attachment-only titles from prepared image metadata", () => {
23 const uploadedAttachments = [
24 {
25 type: "image" as const,
26 id: "prepared-photo",
27 name: "photo.png",
28 mimeType: "image/png",
29 sizeBytes: 3,
30 },
31 ];
32 const input = buildProjectThreadStartTurnInput({
33 projectId: ProjectId.make("project"),
34 projectCwd: "/workspace",
35 threadId: "image-thread",
36 commandId: "image-command",
37 messageId: "image-message",
38 createdAt: "2026-09-04T00:00:00Z",
39 text: "",
40 uploadedAttachments,
41 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol" },
42 runtimeMode: "full-access",
43 interactionMode: "default",
44 workspaceMode: "local",
45 branch: null,
46 worktreePath: null,
47 startFromOrigin: false,
48 worktreeBranchName: "unused",
49 });
50
51 expect(input.titleSeed).toBe("Image: photo.png");
52 expect(input.bootstrap.createThread.title).toBe(input.titleSeed);
53 expect(input.message.attachments).toEqual(uploadedAttachments);
54 });
55
56 it.each([
57 {
58 comment: undefined,
59 title: "Keep `cache[key]` & <parser> shared. Retry!",
60 },
61 {
62 comment: 'Why "shared"?',
63 title: "Keep `cache[key]` & <parser> shared. Retry! Commen...",
64 },
65 ])("uses readable titles and intact links with comment $comment", ({ comment, title }) => {
66 const quoteText = "Keep `cache[key]` & <parser> shared.\n Retry!";
67 const text = serializeAssistantCitation({
68 version: 1,
69 environmentId: EnvironmentId.make("source-environment"),
70 threadId: ThreadId.make("source-thread"),
71 messageId: MessageId.make("source-message"),
72 text: quoteText,
73 ...(comment === undefined ? {} : { comment }),
74 start: 0,
75 end: quoteText.length,
76 prefix: "",
77 suffix: "",
78 });
79 const input = buildProjectThreadStartTurnInput({
80 projectId: ProjectId.make("project"),
81 projectCwd: "/workspace",
82 threadId: "new-thread",
83 commandId: "command",
84 messageId: "message",
85 createdAt: "2026-09-01T00:00:00Z",
86 text,
87 uploadedAttachments: [],
88 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol" },
89 runtimeMode: "full-access",
90 interactionMode: "default",
91 workspaceMode: "local",
92 branch: null,
93 worktreePath: null,
94 startFromOrigin: false,
95 worktreeBranchName: "unused",
96 });
97
98 expect(input.titleSeed).toBe(title);
99 expect(input.bootstrap.createThread.title).toBe(input.titleSeed);
100 expect(input.message.text).toBe(text);
101 });
102});
103
104describe("new thread on an existing branch", () => {
105 it.each([null, "/worktrees/existing"])(
106 "reuses the selected workspace %s without preparing a new worktree",
107 (worktreePath) => {
108 const input = buildProjectThreadStartTurnInput({
109 projectId: ProjectId.make("project"),
110 projectCwd: "/workspace",
111 threadId: "new-thread",
112 commandId: "command",
113 messageId: "message",
114 createdAt: "2026-09-06T00:00:00Z",
115 text: "Start fresh",
116 uploadedAttachments: [],
117 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol" },
118 runtimeMode: "full-access",
119 interactionMode: "default",
120 workspaceMode: "local",
121 branch: "feature/existing",
122 worktreePath,
123 startFromOrigin: false,
124 worktreeBranchName: "unused",
125 });
126
127 expect(input.bootstrap.createThread).toMatchObject({
128 projectId: "project",
129 branch: "feature/existing",
130 worktreePath,
131 });
132 expect(input.bootstrap).not.toHaveProperty("prepareWorktree");
133 expect(input.bootstrap).not.toHaveProperty("runSetupScript");
134 expect(input.threadId).toBe("new-thread");
135 },
136 );
137});