1import { RegistryContext } from "@effect/atom-react";
2import {
3 AuthProvidersManageScope,
4 type AuthSessionState,
5 EnvironmentId,
6 ProviderDriverKind,
7 ProviderInstanceId,
8 type ServerProvider,
9} from "@t3tools/contracts";
10import * as Cause from "effect/Cause";
11import { AsyncResult, Atom } from "effect/reactivity";
12import { useEffect } from "react";
13import { expect, it, vi } from "vite-plus/test";
14
15import { act, render } from "../../test/render";
16
17const state = vi.hoisted(() => ({
18 environments: [] as {
19 environmentId: EnvironmentId;
20 label: string;
21 connection: { phase: "connected" | "disconnected" };
22 }[],
23 providers: new Map<EnvironmentId, ReadonlyArray<ServerProvider>>(),
24 requests: [] as EnvironmentId[],
25 checks: [] as { environmentId: EnvironmentId; input: unknown }[],
26 checkResult: null as AsyncResult.AsyncResult<unknown, unknown> | null,
27 sessions: new Map<EnvironmentId, Atom.Writable<AsyncResult.AsyncResult<AuthSessionState>>>(),
28}));
29
30vi.mock(import("~/state/environments"), () => ({
31 useEnvironments: (() => ({ environments: state.environments })) as never,
32 useEnvironmentProviders: () => state.providers,
33}));
34vi.mock(import("@t3tools/client-runtime/state/session"), async (original) => {
35 const actual = await original();
36 return {
37 ...actual,
38 createEnvironmentSessionAtoms: ((
39 ...args: Parameters<typeof actual.createEnvironmentSessionAtoms>
40 ) => ({
41 ...actual.createEnvironmentSessionAtoms(...args),
42 sessionStateAtom: (id: EnvironmentId) => state.sessions.get(id)!,
43 })) as typeof actual.createEnvironmentSessionAtoms,
44 };
45});
46vi.mock(import("~/state/server"), () => ({
47 serverEnvironment: { updateProvider: "update", refreshProviders: "refresh" } as never,
48}));
49vi.mock(import("~/state/use-atom-command"), () => ({
50 useAtomCommand: ((command: string) =>
51 async ({ environmentId, input }: { environmentId: EnvironmentId; input: unknown }) => {
52 if (command !== "refresh") {
53 state.requests.push(environmentId);
54 return AsyncResult.success(undefined);
55 }
56 state.checks.push({ environmentId, input });
57 return state.checkResult ?? AsyncResult.success(undefined);
58 }) as never,
59}));
60
61import { appAtomRegistry } from "~/rpc/atomRegistry";
62import { toastManager } from "./ui/toast";
63import { useCheckProviderUpdates, useProviderUpdates } from "./ProviderUpdatesAction";
64
65const provider: ServerProvider = {
66 instanceId: ProviderInstanceId.make("codex"),
67 driver: ProviderDriverKind.make("codex"),
68 enabled: true,
69 installed: true,
70 version: "1.0.0",
71 status: "ready",
72 auth: { status: "authenticated" },
73 checkedAt: "2026-10-21T00:00:00.000Z",
74 models: [],
75 slashCommands: [],
76 skills: [],
77 versionAdvisory: {
78 status: "behind_latest",
79 currentVersion: "1.0.0",
80 latestVersion: "1.1.0",
81 updateCommand: "npm install -g @openai/codex",
82 canUpdate: true,
83 checkedAt: "2026-10-21T00:00:00.000Z",
84 message: "Update available.",
85 },
86};
87
88function session(
89 scopes: NonNullable<AuthSessionState["scopes"]>,
90): AsyncResult.AsyncResult<AuthSessionState> {
91 return AsyncResult.success<AuthSessionState>({
92 authenticated: true,
93 scopes,
94 auth: {
95 policy: "remote-reachable",
96 bootstrapMethods: ["one-time-token"],
97 sessionMethods: ["bearer-access-token"],
98 sessionCookieName: "t3_session",
99 },
100 });
101}
102
103function setup() {
104 state.environments = [
105 { environmentId: EnvironmentId.make("read-only"), label: "Read only" },
106 { environmentId: EnvironmentId.make("writable"), label: "Writable" },
107 { environmentId: EnvironmentId.make("unresolved"), label: "Unresolved" },
108 ].map((environment) => ({ ...environment, connection: { phase: "connected" } }));
109 state.providers = new Map(
110 state.environments.map(({ environmentId }) => [environmentId, [provider]]),
111 );
112 state.requests = [];
113 state.checks = [];
114 state.checkResult = null;
115 const sessions = new Map<EnvironmentId, Atom.Writable<AsyncResult.AsyncResult<AuthSessionState>>>(
116 [
117 [state.environments[0]!.environmentId, Atom.make(session(["orchestration:read"]))],
118 [state.environments[1]!.environmentId, Atom.make(session([AuthProvidersManageScope]))],
119 [
120 state.environments[2]!.environmentId,
121 Atom.make<AsyncResult.AsyncResult<AuthSessionState>>(
122 AsyncResult.initial<AuthSessionState>(),
123 ),
124 ],
125 ],
126 );
127 state.sessions = sessions;
128 return { sessions };
129}
130
131it("provider updates and checks exclude connections without a confirmed management grant", async () => {
132 const fixture = setup();
133 let updates!: ReturnType<typeof useProviderUpdates>;
134 let check!: ReturnType<typeof useCheckProviderUpdates>;
135 function Probe() {
136 const nextUpdates = useProviderUpdates();
137 const nextCheck = useCheckProviderUpdates();
138 useEffect(() => {
139 updates = nextUpdates;
140 check = nextCheck;
141 }, [nextUpdates, nextCheck]);
142 return null;
143 }
144 await using _screen = await render(
145 <RegistryContext.Provider value={appAtomRegistry}>
146 <Probe />
147 </RegistryContext.Provider>,
148 );
149
150 const writableId = state.environments[1]!.environmentId;
151 expect(updates.machines.map(({ environmentId }) => environmentId)).toEqual([writableId]);
152 await act(async () => {
153 await updates.updateAll({ quiet: true });
154 });
155 expect(state.requests).toEqual([writableId]);
156 await act(async () => {
157 await check();
158 });
159 // A model refresh always drops the server's cached latest provider versions.
160 expect(state.checks).toEqual([{ environmentId: writableId, input: { refreshModels: true } }]);
161
162 using toast = vi.spyOn(toastManager, "add");
163 state.checkResult = AsyncResult.failure(Cause.interrupt());
164 await act(async () => {
165 await check();
166 });
167 expect(toast).not.toHaveBeenCalled();
168 state.checkResult = AsyncResult.failure(Cause.fail(new Error("offline")));
169 await act(async () => {
170 await check({ quiet: true });
171 });
172 expect(toast).not.toHaveBeenCalled();
173 await act(async () => {
174 await check();
175 });
176 expect(toast).toHaveBeenCalledWith(
177 expect.objectContaining({ title: "Could not check provider updates" }),
178 );
179
180 await act(() => {
181 appAtomRegistry.set(fixture.sessions.get(writableId)!, session(["orchestration:read"]));
182 });
183 expect(updates.machines).toEqual([]);
184 state.requests = [];
185 await act(async () => {
186 await updates.updateAll({ quiet: true });
187 });
188 expect(state.requests).toEqual([]);
189});