Skip to content
Source
Sign inSign up

pending-thread-creation.test.ts

219 lines6.3 KB

1import {
2 CommandId,
3 EnvironmentId,
4 MessageId,
5 ProjectId,
6 ProviderInstanceId,
7 RunId,
8 ThreadId,
9} from "@t3tools/contracts";
10import { isThreadWorking } from "@t3tools/client-runtime/state/thread-inbox";
11import { describe, expect, it } from "vite-plus/test";
12​
13import {
14 pendingThreadCreationShell,
15 resolvePendingThreadCreation,
16 selectThreadCreationDetail,
17 withLaunchingThreads,
18 type PendingThreadCreation,
19} from "./pending-thread-creation";
20import type { QueuedThreadMessage } from "./thread-outbox-model";
21​
22const creation: QueuedThreadMessage = {
23 environmentId: EnvironmentId.make("environment-1"),
24 threadId: ThreadId.make("thread-1"),
25 messageId: MessageId.make("message-1"),
26 commandId: CommandId.make("command-1"),
27 text: "Fix the flaky login test",
28 attachments: [
29 {
30 id: "draft-image",
31 type: "image",
32 name: "screen.png",
33 mimeType: "image/png",
34 sizeBytes: 10,
35 previewUri: "data:image/png;base64,AAAA",
36 },
37 ],
38 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.6-sol" },
39 runtimeMode: "full-access",
40 creation: {
41 projectId: ProjectId.make("project-1"),
42 workspaceMode: "worktree",
43 branch: "main",
44 worktreePath: null,
45 },
46 createdAt: "2026-08-24T12:00:00.000Z",
47};
48​
49describe("resolvePendingThreadCreation", () => {
50 const threadKey = `${creation.environmentId}:${creation.threadId}`;
51 const pending: PendingThreadCreation = { message: creation, outcome: null };
52 const prompt = { id: creation.messageId };
53​
54 it("keeps setup visible through the prompt echo and shell cleanup until detail has a turn", () => {
55 let previous = resolvePendingThreadCreation({
56 threadKey,
57 pending,
58 previous: null,
59 detail: null,
60 });
61 expect(previous).toBe(pending);
62​
63 previous = resolvePendingThreadCreation({
64 threadKey,
65 pending,
66 previous,
67 detail: { messages: [], runs: [] },
68 });
69 expect(previous).toBe(pending);
70​
71 previous = resolvePendingThreadCreation({
72 threadKey,
73 pending,
74 previous,
75 detail: { messages: [prompt], runs: [] },
76 });
77 expect(previous).toBe(pending);
78​
79 previous = resolvePendingThreadCreation({
80 threadKey,
81 pending: null,
82 previous,
83 detail: { messages: [prompt], runs: [] },
84 });
85 expect(previous).toBe(pending);
86​
87 expect(
88 resolvePendingThreadCreation({
89 threadKey,
90 pending: null,
91 previous,
92 detail: {
93 messages: [prompt],
94 runs: [{ status: "running" }],
95 },
96 }),
97 ).toBeNull();
98 });
99​
100 it("keeps the prompt until both the turn and its message have arrived", () => {
101 expect(
102 resolvePendingThreadCreation({
103 threadKey,
104 pending,
105 previous: null,
106 detail: { messages: [], runs: [{ status: "running" }] },
107 }),
108 ).toBe(pending);
109 });
110​
111 it("preserves rejected task recovery", () => {
112 const failed: PendingThreadCreation = {
113 message: creation,
114 outcome: { kind: "failed", message: creation, reason: "Checkout failed" },
115 };
116 expect(
117 resolvePendingThreadCreation({
118 threadKey,
119 pending: failed,
120 previous: pending,
121 detail: { messages: [], runs: [] },
122 }),
123 ).toBe(failed);
124 });
125​
126 it("does not carry setup into another thread or invent it for existing threads", () => {
127 expect(
128 resolvePendingThreadCreation({
129 threadKey: "another-thread",
130 pending: null,
131 previous: pending,
132 detail: null,
133 }),
134 ).toBeNull();
135 expect(
136 resolvePendingThreadCreation({
137 threadKey,
138 pending: null,
139 previous: null,
140 detail: null,
141 }),
142 ).toBeNull();
143 });
144});
145​
146describe("selectThreadCreationDetail", () => {
147 const threadKey = `${creation.environmentId}:${creation.threadId}`;
148 const threadRef = { environmentId: creation.environmentId, threadId: creation.threadId };
149​
150 it("withholds the detail ref until the creation is delivered", () => {
151 const queued = selectThreadCreationDetail({
152 threadRef,
153 threadKey,
154 queuedMessagesByThreadKey: { [threadKey]: [creation] },
155 outcomes: {},
156 });
157 expect(queued).toEqual({
158 pendingCreation: { message: creation, outcome: null },
159 detailRef: null,
160 });
161​
162 const failed = selectThreadCreationDetail({
163 threadRef,
164 threadKey,
165 queuedMessagesByThreadKey: {},
166 outcomes: { [threadKey]: { kind: "failed", message: creation, reason: "Checkout failed" } },
167 });
168 expect(failed.pendingCreation?.outcome?.kind).toBe("failed");
169 expect(failed.detailRef).toBeNull();
170​
171 const delivered = selectThreadCreationDetail({
172 threadRef,
173 threadKey,
174 queuedMessagesByThreadKey: {},
175 outcomes: { [threadKey]: { kind: "delivered", message: creation } },
176 });
177 expect(delivered.pendingCreation?.outcome?.kind).toBe("delivered");
178 expect(delivered.detailRef).toBe(threadRef);
179 });
180​
181 it("does not hold back existing threads with only follow-ups queued", () => {
182 const { creation: _creation, ...followUp } = creation;
183 expect(
184 selectThreadCreationDetail({
185 threadRef,
186 threadKey,
187 queuedMessagesByThreadKey: { [threadKey]: [followUp] },
188 outcomes: {},
189 }),
190 ).toEqual({ pendingCreation: null, detailRef: threadRef });
191 });
192});
193​
194describe("withLaunchingThreads", () => {
195 it("reads as working from Send until the thread's first run arrives", () => {
196 const launch = (threads: Parameters<typeof withLaunchingThreads>[0]["threads"]) =>
197 withLaunchingThreads({ threads, launches: [creation], hiddenThreadKeys: new Set() });
198 // Before the shell arrives, and after the server commits the thread without its run.
199 const created = { ...pendingThreadCreationShell(creation)!, title: "Server title" };
200 for (const threads of [launch([]), launch([created])]) {
201 expect(threads).toHaveLength(1);
202 expect(isThreadWorking(threads[0]!)).toBe(true);
203 }
204 expect(launch([created])[0]!.title).toBe("Server title");
205​
206 const started = {
207 ...created,
208 latestRun: {
209 runId: RunId.make("run-1"),
210 status: "completed" as const,
211 requestedAt: null,
212 startedAt: null,
213 completedAt: null,
214 assistantMessageId: null,
215 },
216 };
217 expect(launch([started])[0]).toBe(started);
218 });
219});