1import { describe, expect, it } from "vite-plus/test";
2import {
3 DEFAULT_RUNTIME_MODE,
4 EnvironmentId,
5 ProjectId,
6 ProviderInstanceId,
7 ThreadId,
8} from "@t3tools/contracts";
9import type { Thread } from "../types";
10import { makeThreadFixture, type ThreadFixtureOverrides } from "../test-fixtures";
11import { getLatestThreadForProject, sortThreads } from "./threadSort";
12
13const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("environment-local");
14const PROJECT_ID = ProjectId.make("project-1");
15
16function makeThread(overrides: ThreadFixtureOverrides = {}): Thread {
17 return makeThreadFixture({
18 id: ThreadId.make("thread-1"),
19 environmentId: LOCAL_ENVIRONMENT_ID,
20 projectId: PROJECT_ID,
21 title: "Thread",
22 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
23 runtimeMode: DEFAULT_RUNTIME_MODE,
24 interactionMode: "default",
25 runtime: null,
26 messages: [],
27 proposedPlans: [],
28 createdAt: "2026-03-09T10:00:00.000Z",
29 archivedAt: null,
30 settledOverride: null,
31 settledAt: null,
32 deletedAt: null,
33 updatedAt: "2026-03-09T10:00:00.000Z",
34 latestRun: null,
35 branch: null,
36 worktreePath: null,
37 ...overrides,
38 });
39}
40
41describe("sortThreads", () => {
42 it("sorts threads by the latest user message in recency mode", () => {
43 const sorted = sortThreads(
44 [
45 makeThread({
46 id: ThreadId.make("thread-1"),
47 updatedAt: "2026-03-09T10:10:00.000Z",
48 messages: [
49 {
50 id: "message-1" as never,
51 role: "user",
52 text: "older",
53 runId: null,
54 createdAt: "2026-03-09T10:01:00.000Z",
55 updatedAt: "2026-03-09T10:01:00.000Z",
56 streaming: false,
57 },
58 ],
59 }),
60 makeThread({
61 id: ThreadId.make("thread-2"),
62 createdAt: "2026-03-09T10:05:00.000Z",
63 updatedAt: "2026-03-09T10:05:00.000Z",
64 messages: [
65 {
66 id: "message-2" as never,
67 role: "user",
68 text: "newer",
69 runId: null,
70 createdAt: "2026-03-09T10:06:00.000Z",
71 updatedAt: "2026-03-09T10:06:00.000Z",
72 streaming: false,
73 },
74 ],
75 }),
76 ],
77 "updated_at",
78 );
79
80 expect(sorted.map((thread) => thread.id)).toEqual([
81 ThreadId.make("thread-2"),
82 ThreadId.make("thread-1"),
83 ]);
84 });
85
86 it("falls back to thread timestamps when there is no user message", () => {
87 const sorted = sortThreads(
88 [
89 makeThread({
90 id: ThreadId.make("thread-1"),
91 updatedAt: "2026-03-09T10:01:00.000Z",
92 messages: [
93 {
94 id: "message-1" as never,
95 role: "assistant",
96 text: "assistant only",
97 runId: null,
98 createdAt: "2026-03-09T10:02:00.000Z",
99 updatedAt: "2026-03-09T10:02:00.000Z",
100 streaming: false,
101 },
102 ],
103 }),
104 makeThread({
105 id: ThreadId.make("thread-2"),
106 createdAt: "2026-03-09T10:05:00.000Z",
107 updatedAt: "2026-03-09T10:05:00.000Z",
108 messages: [],
109 }),
110 ],
111 "updated_at",
112 );
113
114 expect(sorted.map((thread) => thread.id)).toEqual([
115 ThreadId.make("thread-2"),
116 ThreadId.make("thread-1"),
117 ]);
118 });
119
120 it("can sort threads by createdAt when configured", () => {
121 const sorted = sortThreads(
122 [
123 makeThread({
124 id: ThreadId.make("thread-1"),
125 createdAt: "2026-03-09T10:05:00.000Z",
126 updatedAt: "2026-03-09T10:05:00.000Z",
127 }),
128 makeThread({
129 id: ThreadId.make("thread-2"),
130 createdAt: "2026-03-09T10:00:00.000Z",
131 updatedAt: "2026-03-09T10:10:00.000Z",
132 }),
133 ],
134 "created_at",
135 );
136
137 expect(sorted.map((thread) => thread.id)).toEqual([
138 ThreadId.make("thread-1"),
139 ThreadId.make("thread-2"),
140 ]);
141 });
142
143 it("uses updatedAt as a fallback for created_at sorting when createdAt is invalid", () => {
144 const sorted = sortThreads(
145 [
146 makeThread({
147 id: ThreadId.make("thread-1"),
148 createdAt: "invalid-date" as never,
149 updatedAt: "2026-03-09T10:05:00.000Z",
150 }),
151 makeThread({
152 id: ThreadId.make("thread-2"),
153 createdAt: "2026-03-09T10:00:00.000Z",
154 updatedAt: "2026-03-09T10:10:00.000Z",
155 }),
156 ],
157 "created_at",
158 );
159
160 expect(sorted.map((thread) => thread.id)).toEqual([
161 ThreadId.make("thread-1"),
162 ThreadId.make("thread-2"),
163 ]);
164 });
165
166 it("returns the latest active thread for a project", () => {
167 const latestThread = getLatestThreadForProject(
168 [
169 makeThread({
170 id: ThreadId.make("thread-1"),
171 createdAt: "2026-03-09T10:00:00.000Z",
172 updatedAt: "2026-03-09T10:01:00.000Z",
173 archivedAt: null,
174 }),
175 makeThread({
176 id: ThreadId.make("thread-2"),
177 createdAt: "2026-03-09T10:05:00.000Z",
178 updatedAt: "2026-03-09T10:10:00.000Z",
179 archivedAt: "2026-03-10T00:00:00.000Z",
180 }),
181 makeThread({
182 id: ThreadId.make("thread-3"),
183 createdAt: "2026-03-09T10:06:00.000Z",
184 updatedAt: "2026-03-09T10:06:00.000Z",
185 archivedAt: null,
186 }),
187 ],
188 PROJECT_ID,
189 "updated_at",
190 );
191
192 expect(latestThread?.id).toBe(ThreadId.make("thread-3"));
193 });
194});