1import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
2import {
3 EnvironmentId,
4 ProjectId,
5 ProviderInstanceId,
6 ThreadId,
7 type ServerConfig,
8} from "@t3tools/contracts";
9import { describe, expect, it } from "vite-plus/test";
10
11import {
12 createThreadRowProviderInstanceResolver,
13 resolveThreadProviderInstance,
14} from "./thread-provider-instance";
15
16function makeConfig(
17 providers: ReadonlyArray<{
18 readonly instanceId: string;
19 readonly driver: string;
20 readonly displayName?: string;
21 readonly accentColor?: string;
22 }>,
23): ServerConfig {
24 return { providers } as unknown as ServerConfig;
25}
26
27function makeThread(environmentId: EnvironmentId, instanceId: string): EnvironmentThreadShell {
28 return {
29 environmentId,
30 id: ThreadId.make("thread-1"),
31 projectId: ProjectId.make("project-1"),
32 title: "Thread",
33 modelSelection: { instanceId: ProviderInstanceId.make(instanceId), model: "gpt-5.4" },
34 runtimeMode: "full-access",
35 interactionMode: "default",
36 branch: null,
37 worktreePath: null,
38 latestRun: null,
39 createdAt: "2026-06-01T00:00:00.000Z",
40 updatedAt: "2026-06-01T00:00:00.000Z",
41 archivedAt: null,
42 settledOverride: null,
43 settledAt: null,
44 runtime: null,
45 latestUserMessageAt: null,
46 hasPendingApprovals: false,
47 hasPendingUserInput: false,
48 hasActionableProposedPlan: false,
49 } as unknown as EnvironmentThreadShell;
50}
51
52describe("resolveThreadProviderInstance", () => {
53 it("resolves two environments with the same default instance id independently", () => {
54 const environmentA = EnvironmentId.make("environment-a");
55 const environmentB = EnvironmentId.make("environment-b");
56 const serverConfigs = new Map<EnvironmentId, ServerConfig>([
57 [
58 environmentA,
59 makeConfig([{ instanceId: "codex", driver: "codex", accentColor: "#ff8800" }]),
60 ],
61 [environmentB, makeConfig([{ instanceId: "codex", driver: "codex" }])],
62 ]);
63
64 const threadA = makeThread(environmentA, "codex");
65 const threadB = makeThread(environmentB, "codex");
66
67 expect(
68 resolveThreadProviderInstance(serverConfigs.get(environmentA)?.providers, threadA)
69 ?.accentColor,
70 ).toBe("#ff8800");
71 expect(
72 resolveThreadProviderInstance(serverConfigs.get(environmentB)?.providers, threadB)
73 ?.accentColor,
74 ).toBeUndefined();
75 });
76
77 it("labels a custom instance by its id so its initials differ from the default", () => {
78 const environmentId = EnvironmentId.make("environment-a");
79 const serverConfigs = new Map<EnvironmentId, ServerConfig>([
80 [
81 environmentId,
82 makeConfig([
83 { instanceId: "codex", driver: "codex", displayName: "Codex" },
84 { instanceId: "codex_personal", driver: "codex", displayName: "Codex" },
85 ]),
86 ],
87 ]);
88
89 expect(
90 resolveThreadProviderInstance(
91 serverConfigs.get(environmentId)?.providers,
92 makeThread(environmentId, "codex"),
93 )?.displayName,
94 ).toBe("Codex");
95 expect(
96 resolveThreadProviderInstance(
97 serverConfigs.get(environmentId)?.providers,
98 makeThread(environmentId, "codex_personal"),
99 )?.displayName,
100 ).toBe("Codex Personal");
101 });
102
103 it("uses the current runtime owner after a provider handoff", () => {
104 const environmentId = EnvironmentId.make("environment-a");
105 const serverConfigs = new Map<EnvironmentId, ServerConfig>([
106 [
107 environmentId,
108 makeConfig([
109 { instanceId: "claudeAgent", driver: "claudeAgent" },
110 { instanceId: "codex", driver: "codex", displayName: "Codex" },
111 { instanceId: "codex_work", driver: "codex", displayName: "Codex" },
112 ]),
113 ],
114 ]);
115 const thread = {
116 ...makeThread(environmentId, "claudeAgent"),
117 runtime: {
118 status: "running" as const,
119 activeRunId: null,
120 providerInstanceId: ProviderInstanceId.make("codex_work"),
121 providerName: "Codex",
122 lastError: null,
123 updatedAt: "2026-06-01T00:01:00.000Z",
124 },
125 };
126
127 expect(
128 resolveThreadProviderInstance(serverConfigs.get(environmentId)?.providers, thread),
129 ).toMatchObject({
130 driverKind: "codex",
131 displayName: "Codex Work",
132 showBadge: true,
133 });
134 });
135
136 it("hides the badge for a single instance with no accent color", () => {
137 const environmentId = EnvironmentId.make("environment-a");
138 const serverConfigs = new Map<EnvironmentId, ServerConfig>([
139 [environmentId, makeConfig([{ instanceId: "codex", driver: "codex" }])],
140 ]);
141 const thread = makeThread(environmentId, "codex");
142
143 expect(
144 resolveThreadProviderInstance(serverConfigs.get(environmentId)?.providers, thread)?.showBadge,
145 ).toBe(false);
146 });
147});
148
149describe("createThreadRowProviderInstanceResolver", () => {
150 const environmentId = EnvironmentId.make("environment-a");
151 const serverConfigs = new Map<EnvironmentId, ServerConfig>([
152 [
153 environmentId,
154 makeConfig([
155 { instanceId: "codex", driver: "codex", displayName: "Codex" },
156 { instanceId: "codex_work", driver: "codex", displayName: "Codex" },
157 ]),
158 ],
159 ]);
160
161 it("hands out the same reference for repeated lookups of one instance", () => {
162 const resolve = createThreadRowProviderInstanceResolver(
163 new Map([...serverConfigs].map(([id, config]) => [id, config.providers])),
164 );
165 const first = resolve(makeThread(environmentId, "codex"));
166 const second = resolve(makeThread(environmentId, "codex"));
167 // Memoized rows compare props by reference: a fresh object per call would
168 // re-render every row on every parent render (minute tick included).
169 expect(first).not.toBeNull();
170 expect(second).toBe(first);
171 });
172
173 it("distinguishes instances of the same driver", () => {
174 const resolve = createThreadRowProviderInstanceResolver(
175 new Map([...serverConfigs].map(([id, config]) => [id, config.providers])),
176 );
177 const personal = resolve(makeThread(environmentId, "codex"));
178 const work = resolve(makeThread(environmentId, "codex_work"));
179 expect(personal).not.toBeNull();
180 expect(work).not.toBeNull();
181 expect(work).not.toBe(personal);
182 expect(work?.displayName).toBe("Codex Work");
183 });
184
185 it("hands out a new identity when the server-config generation changes", () => {
186 const before = createThreadRowProviderInstanceResolver(
187 new Map([...serverConfigs].map(([id, config]) => [id, config.providers])),
188 );
189 const nextConfigs = new Map<EnvironmentId, ServerConfig>([
190 [environmentId, makeConfig([{ instanceId: "codex", driver: "codex" }])],
191 ]);
192 const after = createThreadRowProviderInstanceResolver(
193 new Map([...nextConfigs].map(([id, config]) => [id, config.providers])),
194 );
195 expect(after(makeThread(environmentId, "codex"))).not.toBe(
196 before(makeThread(environmentId, "codex")),
197 );
198 });
199
200 it("resolves unknown instances to null without throwing", () => {
201 const resolve = createThreadRowProviderInstanceResolver(
202 new Map([...serverConfigs].map(([id, config]) => [id, config.providers])),
203 );
204 expect(resolve(makeThread(environmentId, "ghost"))).toBeNull();
205 expect(resolve(makeThread(environmentId, "ghost"))).toBeNull();
206 });
207});