Skip to content
Source
Sign inSign up

threadInbox.test.ts

263 lines9.9 KB

1import { EnvironmentId, ProviderInstanceId, RunId, ThreadId } from "@t3tools/contracts";
2import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
3​
4import {
5 createInboxReturnTracker,
6 isThreadWorking,
7 sortInboxThreadsByReturn,
8 sortWorkingThreadsBySend,
9} from "./threadInbox.ts";
10​
11const environmentId = EnvironmentId.make("environment-1");
12​
13function thread(id: string, working: boolean) {
14 return {
15 id: ThreadId.make(id),
16 environmentId,
17 createdAt: "2026-06-01T00:00:00.000Z",
18 unsettledAt: null,
19 latestRun: null,
20 hasActionableProposedPlan: false,
21 hasPendingApprovals: false,
22 hasPendingUserInput: false,
23 interactionMode: "default" as const,
24 runtime: working
25 ? {
26 status: "running" as const,
27 activeRunId: null,
28 providerInstanceId: ProviderInstanceId.make("codex"),
29 providerName: "Codex",
30 lastError: null,
31 updatedAt: "2026-06-01T00:00:00.000Z",
32 }
33 : null,
34 };
36​
37describe("createInboxReturnTracker", () => {
38 beforeEach(() => {
39 vi.useFakeTimers();
40 vi.setSystemTime(10_000);
41 });
42 afterEach(() => {
43 vi.clearAllTimers();
44 vi.useRealTimers();
45 });
46 it("stamps a thread when it stops working, but never on the first observation", () => {
47 const tracker = createInboxReturnTracker();
48 const first = tracker.observe([thread("a", true), thread("b", false)]);
49 expect(first.returnedAt(thread("a", true))).toBeUndefined();
50 expect(first.returnedAt(thread("b", false))).toBeUndefined();
51​
52 const held = tracker.observe([thread("a", false), thread("b", false)]);
53 vi.advanceTimersByTime(2_000);
54 const second = tracker.observe([thread("a", false), thread("b", false)]);
55 expect(second.returnedAt(thread("a", false))).toBeDefined();
56 expect(second.returnedAt(thread("b", false))).toBeUndefined();
57 expect(tracker.returnedAt(thread("a", false))).toBe(second.returnedAt(thread("a", false)));
58 // An earlier observation keeps its answer, so lists built from it stay valid.
59 expect(held.isWorking(thread("a", false))).toBe(true);
60 expect(held.returnedAt(thread("a", false))).toBeUndefined();
61 });
62​
63 it("holds only placement, then expires and notifies without another shell event", () => {
64 const tracker = createInboxReturnTracker();
65 const notify = vi.fn();
66 const unsubscribe = tracker.subscribe(notify);
67 tracker.observe([thread("a", true)]);
68 tracker.observe([thread("a", false)]);
69 expect(isThreadWorking(thread("a", false))).toBe(false);
70 expect(tracker.isWorking(thread("a", false))).toBe(true);
71 vi.advanceTimersByTime(1_999);
72 tracker.observe([thread("a", false)]);
73 expect(tracker.isWorking(thread("a", false))).toBe(true);
74 expect(tracker.returnedAt(thread("a", false))).toBeUndefined();
75 vi.advanceTimersByTime(1);
76 expect(tracker.isWorking(thread("a", false))).toBe(false);
77 expect(tracker.returnedAt(thread("a", false))).toBe(12_000);
78 expect(notify).toHaveBeenCalledTimes(1);
79 unsubscribe();
80 });
81​
82 it("moves a thread whose run ended long ago at once, as after a reconnect", () => {
83 const tracker = createInboxReturnTracker();
84 const finished = {
85 ...thread("a", false),
86 latestRun: {
87 runId: RunId.make("ended-while-away"),
88 status: "completed" as const,
89 requestedAt: "1970-01-01T00:00:02.000Z",
90 startedAt: "1970-01-01T00:00:02.000Z",
91 completedAt: "1970-01-01T00:00:09.000Z",
92 assistantMessageId: null,
93 },
94 };
95 tracker.observe([thread("a", true)]);
96 vi.setSystemTime(200_000);
97 tracker.observe([finished]);
98 expect(tracker.isWorking(finished)).toBe(false);
99 expect(tracker.returnedAt(finished)).toBe(200_000);
100 });
101​
102 it("cancels a transient return on resume and grants a fresh grace after the next finish", () => {
103 const tracker = createInboxReturnTracker();
104 tracker.observe([thread("a", false)]);
105 expect(tracker.isWorking(thread("a", false))).toBe(false);
106 tracker.observe([thread("a", true)]);
107 expect(tracker.isWorking(thread("a", true))).toBe(true);
108 tracker.observe([thread("a", false)]);
109 vi.advanceTimersByTime(1_500);
110 tracker.observe([thread("a", true)]);
111 vi.advanceTimersByTime(1_000);
112 expect(tracker.returnedAt(thread("a", true))).toBeUndefined();
113 tracker.observe([thread("a", false)]);
114 vi.advanceTimersByTime(1_999);
115 expect(tracker.isWorking(thread("a", false))).toBe(true);
116 vi.advanceTimersByTime(1);
117 expect(tracker.returnedAt(thread("a", false))).toBe(14_500);
118 });
119​
120 it.each([
121 { hasPendingApprovals: true },
122 { hasPendingUserInput: true },
123 { pendingSubagentRequest: "approval" as const },
124 { pendingSubagentRequest: "user_input" as const },
125 { pendingGuestMessageCount: 1 },
126 { interactionMode: "plan" as const, hasActionableProposedPlan: true },
127 { runtime: { ...thread("a", true).runtime!, status: "failed" as const, lastError: "failed" } },
128 { pinnedAt: "2026-06-01T00:00:01Z" },
129 { settledOverride: "settled" as const },
130 { snoozedUntil: "2026-06-02T00:00:00Z" },
131 { activeOrderKey: "manual-order" },
132 ])("ends a grace immediately for attention or manual placement: %j", (change) => {
133 const tracker = createInboxReturnTracker();
134 tracker.observe([thread("a", true)]);
135 tracker.observe([thread("a", false)]);
136 tracker.observe([{ ...thread("a", false), ...change }]);
137 expect(tracker.isWorking(thread("a", false))).toBe(false);
138 expect(tracker.returnedAt(thread("a", false))).toBe(10_000);
139 expect(vi.getTimerCount()).toBe(0);
140 });
141​
142 it.each(["interrupted", "cancelled", "rolled_back"] as const)(
143 "holds a %s run, because the server can still settle it",
144 (status) => {
145 const tracker = createInboxReturnTracker();
146 const stopped = {
147 ...thread("a", false),
148 latestRun: {
149 runId: RunId.make("stopped"),
150 status,
151 requestedAt: null,
152 startedAt: null,
153 completedAt: null,
154 assistantMessageId: null,
155 },
156 };
157 tracker.observe([thread("a", true)]);
158 tracker.observe([stopped]);
159 expect(tracker.isWorking(stopped)).toBe(true);
160 tracker.observe([{ ...stopped, settledOverride: "settled" as const }]);
161 expect(tracker.isWorking(stopped)).toBe(false);
162 },
163 );
164​
165 it("grants grace after a retained expired snooze", () => {
166 const tracker = createInboxReturnTracker();
167 const retained = {
168 snoozedAt: "1970-01-01T00:00:01.000Z",
169 snoozedUntil: "1970-01-01T00:00:05.000Z",
170 latestRun: {
171 runId: RunId.make("completed-wake"),
172 status: "completed" as const,
173 requestedAt: "1970-01-01T00:00:02.000Z",
174 startedAt: "1970-01-01T00:00:02.000Z",
175 completedAt: "1970-01-01T00:00:09.000Z",
176 assistantMessageId: null,
177 },
178 };
179 const active = { ...thread("a", true), ...retained };
180 const finished = { ...thread("a", false), ...retained };
181 tracker.observe([active]);
182 tracker.observe([finished]);
183 expect(tracker.isWorking(finished)).toBe(true);
184 expect(tracker.returnedAt(finished)).toBeUndefined();
185 vi.advanceTimersByTime(1_999);
186 expect(tracker.isWorking(finished)).toBe(true);
187 vi.advanceTimersByTime(1);
188 expect(tracker.isWorking(finished)).toBe(false);
189 expect(tracker.returnedAt(finished)).toBe(12_000);
190 });
191​
192 it("does not grant grace to a future snooze after background completion", () => {
193 const tracker = createInboxReturnTracker();
194 const retained = {
195 snoozedAt: "1970-01-01T00:00:01.000Z",
196 snoozedUntil: "1970-01-01T00:00:30.000Z",
197 };
198 tracker.observe([{ ...thread("a", true), ...retained }]);
199 const finished = { ...thread("a", false), ...retained };
200 tracker.observe([finished]);
201 expect(tracker.isWorking(finished)).toBe(false);
202 expect(vi.getTimerCount()).toBe(0);
203 });
204​
205 it("stamps only stable returns and sorts them in their actual return order", () => {
206 const tracker = createInboxReturnTracker();
207 const a = { ...thread("a", false), createdAt: "1970-01-01T00:00:00Z" };
208 const b = { ...thread("b", false), createdAt: "1970-01-01T00:00:00Z" };
209 tracker.observe([thread("a", true), thread("b", true)]);
210 tracker.observe([a, thread("b", true)]);
211 vi.advanceTimersByTime(500);
212 tracker.observe([a, b]);
213 vi.advanceTimersByTime(1_500);
214 expect(tracker.returnedAt(a)).toBe(12_000);
215 expect(tracker.returnedAt(b)).toBeUndefined();
216 vi.advanceTimersByTime(500);
217 expect(sortInboxThreadsByReturn([a, b], tracker.returnedAt).map((entry) => entry.id)).toEqual([
218 "b",
219 "a",
220 ]);
221 });
222​
223 it("cancels pending timers for removed threads and disabled shelves", () => {
224 const tracker = createInboxReturnTracker();
225 const notify = vi.fn();
226 tracker.subscribe(notify);
227 tracker.observe([thread("a", true)]);
228 tracker.observe([thread("a", false)]);
229 tracker.observe([]);
230 expect(vi.getTimerCount()).toBe(0);
231 tracker.observe([thread("a", true)]);
232 tracker.observe([thread("a", false)]);
233 tracker.observe(null);
234 vi.advanceTimersByTime(2_000);
235 expect(notify).not.toHaveBeenCalled();
236 expect(tracker.returnedAt(thread("a", false))).toBeUndefined();
237 });
238});
239​
240describe("sortWorkingThreadsBySend", () => {
241 it("orders by the last message the user sent, not by later runs", () => {
242 const sentFirst = {
243 ...thread("sent-first", true),
244 latestUserAuthoredMessageAt: "2026-06-01T01:00:00.000Z",
245 latestRun: {
246 runId: RunId.make("run:wake"),
247 status: "running" as const,
248 requestedAt: "2026-06-01T04:00:00.000Z",
249 startedAt: "2026-06-01T04:00:00.000Z",
250 completedAt: null,
251 assistantMessageId: null,
252 },
253 };
254 const sentLast = {
255 ...thread("sent-last", true),
256 latestUserAuthoredMessageAt: "2026-06-01T02:00:00.000Z",
257 };
258 const launched = { ...thread("launched", true), latestUserAuthoredMessageAt: null };
259 expect(
260 sortWorkingThreadsBySend([launched, sentFirst, sentLast]).map((thread) => thread.id),
261 ).toEqual(["sent-last", "sent-first", "launched"]);
262 });
263});