1import {
2 type ManagedPolicySummary,
3 ProjectId,
4 resolveProviderInstanceEnabled,
5 ProviderDriverKind,
6 ProviderInstanceId,
7 type ServerProvider,
8 type ServerSettings,
9} from "@t3tools/contracts";
10import { describe, expect, it } from "vite-plus/test";
11
12import {
13 cliTerminalStep,
14 codexCardProvider,
15 codexNeedsSetup,
16 groupWelcomeAgents,
17 turnedOnInstance,
18 welcomeOffersPhone,
19} from "./welcomePage.logic";
20
21function provider(name: string, overrides: Partial<ServerProvider> = {}): ServerProvider {
22 return {
23 instanceId: ProviderInstanceId.make(name.toLowerCase()),
24 driver: ProviderDriverKind.make(name.toLowerCase()),
25 displayName: name,
26 enabled: true,
27 installed: true,
28 version: "1.0.0",
29 status: "ready",
30 auth: { status: "authenticated" },
31 checkedAt: "2026-08-23T00:00:00.000Z",
32 models: [],
33 slashCommands: [],
34 skills: [],
35 ...overrides,
36 };
37}
38
39const nameOf = (entry: ServerProvider) => entry.displayName ?? entry.driver;
40const names = (entries: ReadonlyArray<ServerProvider>) => entries.map(nameOf);
41const allowAll = () => true;
42
43describe("groupWelcomeAgents", () => {
44 it("lists agents on the computer by state, then name, and everything else by name", () => {
45 const groups = groupWelcomeAgents(
46 [
47 provider("Zeta", { auth: { status: "unauthenticated" }, status: "error" }),
48 provider("Codex"),
49 provider("Pi", { enabled: false, status: "disabled" }),
50 provider("Grok", { installed: false, status: "error", auth: { status: "unknown" } }),
51 provider("Claude"),
52 provider("Cursor", { installed: false, status: "warning", auth: { status: "unknown" } }),
53 ],
54 undefined,
55 nameOf,
56 allowAll,
57 );
58
59 expect(names(groups.found)).toEqual(["Claude", "Codex", "Zeta"]);
60 // Cursor is still being checked; it stays listed instead of disappearing.
61 expect(names(groups.available)).toEqual(["Cursor", "Grok", "Pi"]);
62 expect(groups.checking).toBe(true);
63 expect(groups.readyCount).toBe(2);
64 });
65
66 it("uses this computer's settings and keeps agents that failed to load with the found ones", () => {
67 const acpRegistry = ProviderDriverKind.make("acpRegistry");
68 const settings: Pick<ServerSettings, "providerInstances" | "projectSettingsOverrides"> = {
69 providerInstances: {
70 [ProviderInstanceId.make("pi")]: { driver: ProviderDriverKind.make("pi") },
71 [ProviderInstanceId.make("registry")]: { driver: acpRegistry },
72 },
73 projectSettingsOverrides: {
74 [ProjectId.make("project-a")]: {
75 providerInstances: { [ProviderInstanceId.make("pi")]: { enabled: true } },
76 },
77 },
78 };
79 const groups = groupWelcomeAgents(
80 [
81 // The server checks Pi because project A turns it on; new projects still cannot use it.
82 provider("Pi"),
83 provider("Broken", {
84 enabled: false,
85 installed: false,
86 status: "error",
87 availability: "unavailable",
88 unavailableReason: "Invalid config for instance 'broken'.",
89 }),
90 // Signed in and healthy, but the server knows this CLI version is broken.
91 provider("Codex", {
92 compatibilityAdvisory: {
93 status: "broken",
94 message: "Update Codex to 0.170 or later.",
95 recommendedVersion: "0.170.0",
96 recommendedRange: null,
97 },
98 }),
99 // An ACP agent that is not prepared looks like one still being checked.
100 provider("Registry", {
101 driver: acpRegistry,
102 installed: false,
103 status: "warning",
104 auth: { status: "unknown" },
105 }),
106 ],
107 settings,
108 nameOf,
109 allowAll,
110 );
111
112 expect(names(groups.found)).toEqual(["Broken", "Codex", "Registry"]);
113 expect(names(groups.available)).toEqual(["Pi"]);
114 expect(groups.readyCount).toBe(0);
115 expect(groups.checking).toBe(false);
116 });
117
118 it("leaves out agents the computer's policy does not allow, ready or not", () => {
119 const groups = groupWelcomeAgents(
120 [provider("Codex"), provider("Blocked")],
121 undefined,
122 nameOf,
123 (entry) => entry.driver !== "blocked",
124 );
125
126 expect(names(groups.found)).toEqual(["Codex"]);
127 expect(groups.readyCount).toBe(1);
128 });
129
130 it("reports checking before the computer has sent its providers", () => {
131 expect(groupWelcomeAgents(undefined, undefined, nameOf, allowAll)).toEqual({
132 checking: true,
133 readyCount: 0,
134 found: [],
135 available: [],
136 });
137 });
138});
139
140describe("cliTerminalStep", () => {
141 it("waits for the CLI's own check, then opens its terminal unless Codex is ready", () => {
142 const codex = (overrides: Partial<ServerProvider> = {}) => provider("Codex", overrides);
143 const step = (overrides?: Partial<ServerProvider>) =>
144 cliTerminalStep(codex(overrides), undefined);
145
146 // Still the ChatGPT sign-in runtime, before the switch to the CLI lands.
147 expect(
148 step({
149 setup: { canAuthenticate: true, canInstall: true },
150 auth: { status: "unauthenticated" },
151 }),
152 ).toBe("wait");
153 expect(step({ installed: false, status: "warning", auth: { status: "unknown" } })).toBe("wait");
154 expect(step({ installed: false, status: "error", auth: { status: "unknown" } })).toBe("open");
155 expect(step({ auth: { status: "unauthenticated" } })).toBe("open");
156 // A check that timed out still gets the sign-in command.
157 expect(step({ status: "error", auth: { status: "unknown" } })).toBe("open");
158 // A broken version is updated from its row in Settings, signed in, signed out, or unchecked.
159 const broken = {
160 status: "broken",
161 message: "Update Codex.",
162 recommendedVersion: null,
163 recommendedRange: null,
164 } as const;
165 expect(step({ compatibilityAdvisory: broken })).toBe("done");
166 expect(step({ compatibilityAdvisory: broken, auth: { status: "unauthenticated" } })).toBe(
167 "done",
168 );
169 expect(
170 step({ compatibilityAdvisory: broken, status: "error", auth: { status: "unknown" } }),
171 ).toBe("done");
172 // Signed in but needing a look for another reason: a sign-in would not help.
173 expect(step({ status: "warning" })).toBe("done");
174 // Signed in from its home files with the binary gone: the installer.
175 expect(step({ installed: false, status: "error" })).toBe("open");
176 expect(step()).toBe("done");
177 });
178});
179
180describe("turnedOnInstance", () => {
181 it("turns an agent on even when older settings keep it off inside its config", () => {
182 const pi = ProviderDriverKind.make("pi");
183 expect(resolveProviderInstanceEnabled(turnedOnInstance({ driver: pi }))).toBe(true);
184 const migrated = turnedOnInstance({ driver: pi, config: { enabled: false, binaryPath: "pi" } });
185 expect(resolveProviderInstanceEnabled(migrated)).toBe(true);
186 expect(migrated.config).toEqual({ enabled: true, binaryPath: "pi" });
187 });
188});
189
190describe("codexNeedsSetup", () => {
191 it("opens the setup card only while Codex is not signed in", () => {
192 const setup = (overrides: Partial<ServerProvider>) =>
193 codexNeedsSetup(provider("Codex", overrides), undefined);
194
195 expect(setup({ installed: false, status: "error", auth: { status: "unknown" } })).toBe(true);
196 expect(setup({ auth: { status: "unauthenticated" } })).toBe(true);
197 // A check that timed out can still sign in with ChatGPT.
198 expect(setup({ status: "error", auth: { status: "unknown" } })).toBe(true);
199 // Signed in from its home files but the binary is gone: Install, not the card.
200 expect(setup({ installed: false, status: "error" })).toBe(false);
201 // A broken version gets Update on its row.
202 expect(
203 setup({
204 auth: { status: "unauthenticated" },
205 compatibilityAdvisory: {
206 status: "broken",
207 message: null,
208 recommendedVersion: null,
209 recommendedRange: null,
210 },
211 }),
212 ).toBe(false);
213 expect(setup({})).toBe(false);
214 const claude = provider("Claude", { auth: { status: "unauthenticated" } });
215 expect(codexNeedsSetup(claude, undefined)).toBe(false);
216 });
217});
218
219describe("codexCardProvider", () => {
220 it("keeps the card on Codex's last status while a rebuild drops it, and closes once signed in", () => {
221 const signedOut = provider("Codex", { auth: { status: "unauthenticated" } });
222 const card = (providers: ReadonlyArray<ServerProvider>, last: ServerProvider | null = null) =>
223 codexCardProvider({ providers, instanceId: signedOut.instanceId, last, settings: undefined });
224
225 expect(card([signedOut])).toBe(signedOut);
226 // The instance is missing for one update while it is rebuilt.
227 expect(card([], signedOut)).toBe(signedOut);
228 expect(card([], provider("Other"))).toBeNull();
229 expect(card([provider("Codex")])).toBeNull();
230 expect(
231 codexCardProvider({
232 providers: [signedOut],
233 instanceId: null,
234 last: null,
235 settings: undefined,
236 }),
237 ).toBeNull();
238 });
239});
240
241describe("welcomeOffersPhone", () => {
242 it("offers the phone only through T3 Connect on the primary computer, and only when allowed", () => {
243 const policy = (limits: ManagedPolicySummary["limits"]): ManagedPolicySummary => ({
244 organization: "Acme",
245 lockedSettings: [],
246 ...(limits === undefined ? {} : { limits }),
247 });
248 const offers = (input: Partial<Parameters<typeof welcomeOffersPhone>[0]>) =>
249 welcomeOffersPhone({
250 isPrimary: true,
251 cloud: true,
252 canShare: true,
253 policy: undefined,
254 ...input,
255 });
256
257 expect(offers({})).toBe(true);
258 // There is no pairing-code fallback; without T3 Connect the page leaves the phone out.
259 expect(offers({ cloud: false })).toBe(false);
260 expect(offers({ canShare: false })).toBe(false);
261 expect(offers({ policy: policy({ connect: false }) })).toBe(false);
262 expect(offers({ policy: policy({ clients: ["desktop"] }) })).toBe(false);
263 expect(offers({ isPrimary: false })).toBe(false);
264 });
265});