1import * as NodeServices from "@effect/platform-node/NodeServices";
2import {
3 defaultInstanceIdForDriver,
4 ProviderDriverKind,
5 ProviderInstanceId,
6 type ServerProvider,
7} from "@t3tools/contracts";
8import { createModelCapabilities } from "@t3tools/shared/model";
9import { assert, it } from "@effect/vitest";
10import * as Effect from "effect/Effect";
11import * as FileSystem from "effect/FileSystem";
12import * as Logger from "effect/Logger";
13
14import {
15 hydrateCachedProvider,
16 isCachedProviderCorrelated,
17 readProviderStatusCache,
18 resolveProviderStatusCachePath,
19 writeProviderStatusCache,
20} from "./providerStatusCache.ts";
21
22const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] });
23const CODEX_DRIVER = ProviderDriverKind.make("codex");
24const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent");
25const OPENCODE_DRIVER = ProviderDriverKind.make("opencode");
26
27const makeProvider = (
28 provider: ProviderDriverKind,
29 overrides?: Partial<ServerProvider>,
30): ServerProvider => ({
31 instanceId: defaultInstanceIdForDriver(provider),
32 driver: provider,
33 enabled: true,
34 installed: true,
35 version: "1.0.0",
36 status: "ready",
37 auth: { status: "authenticated" },
38 checkedAt: "2026-04-11T00:00:00.000Z",
39 models: [],
40 slashCommands: [],
41 skills: [],
42 ...overrides,
43});
44
45it.layer(NodeServices.layer)("providerStatusCache", (it) => {
46 it.effect("logs structural diagnostics without retaining invalid cache contents", () => {
47 const messages: Array<unknown> = [];
48 const logger = Logger.make<unknown, void>((options) => {
49 if (Array.isArray(options.message)) {
50 messages.push(...options.message);
51 } else {
52 messages.push(options.message);
53 }
54 });
55
56 return Effect.gen(function* () {
57 const fs = yield* FileSystem.FileSystem;
58 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-cache-invalid-" });
59 const cachePath = `${tempDir}/provider.json`;
60 const secretCacheValue = "secret-cache-value";
61 yield* fs.writeFileString(cachePath, `{ "token": "${secretCacheValue}" }`);
62
63 const result = yield* readProviderStatusCache(cachePath);
64
65 assert.strictEqual(result, undefined);
66 const failure = messages.find(
67 (message): message is Record<string, unknown> =>
68 typeof message === "object" && message !== null && "path" in message,
69 );
70 assert.exists(failure);
71 assert.strictEqual(failure.path, cachePath);
72 assert.strictEqual(typeof failure.errorTag, "string");
73 assert.ok(!("cause" in failure));
74 assert.ok(!("issues" in failure));
75 assert.ok(!Object.values(failure).map(String).join("\n").includes(secretCacheValue));
76 }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false })));
77 });
78
79 it.effect("writes and reads provider status snapshots", () =>
80 Effect.gen(function* () {
81 const fs = yield* FileSystem.FileSystem;
82 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-cache-" });
83 const codexProvider = makeProvider(CODEX_DRIVER);
84 const claudeProvider = makeProvider(CLAUDE_AGENT_DRIVER, {
85 status: "warning",
86 auth: { status: "unknown" },
87 });
88 const openCodeProvider = makeProvider(OPENCODE_DRIVER, {
89 status: "warning",
90 auth: { status: "unknown", type: "opencode" },
91 });
92 const codexPath = yield* resolveProviderStatusCachePath({
93 cacheDir: tempDir,
94 instanceId: defaultInstanceIdForDriver(ProviderDriverKind.make("codex")),
95 });
96 const claudePath = yield* resolveProviderStatusCachePath({
97 cacheDir: tempDir,
98 instanceId: defaultInstanceIdForDriver(ProviderDriverKind.make("claudeAgent")),
99 });
100 const openCodePath = yield* resolveProviderStatusCachePath({
101 cacheDir: tempDir,
102 instanceId: defaultInstanceIdForDriver(ProviderDriverKind.make("opencode")),
103 });
104
105 yield* writeProviderStatusCache({
106 filePath: codexPath,
107 provider: codexProvider,
108 });
109 yield* writeProviderStatusCache({
110 filePath: claudePath,
111 provider: claudeProvider,
112 });
113 yield* writeProviderStatusCache({
114 filePath: openCodePath,
115 provider: openCodeProvider,
116 });
117
118 assert.deepStrictEqual(yield* readProviderStatusCache(codexPath), codexProvider);
119 assert.deepStrictEqual(yield* readProviderStatusCache(claudePath), claudeProvider);
120 assert.deepStrictEqual(yield* readProviderStatusCache(openCodePath), openCodeProvider);
121 }),
122 );
123
124 it("hydrates cached provider status while preserving current settings-derived models", () => {
125 const cachedCodex = makeProvider(CODEX_DRIVER, {
126 checkedAt: "2026-04-10T12:00:00.000Z",
127 models: [
128 {
129 slug: "gpt-5-mini",
130 name: "GPT-5 Mini",
131 isCustom: false,
132 capabilities: emptyCapabilities,
133 },
134 ],
135 message: "Cached message",
136 skills: [
137 {
138 name: "github:gh-fix-ci",
139 path: "/tmp/skills/gh-fix-ci/SKILL.md",
140 enabled: true,
141 displayName: "CI Debug",
142 },
143 ],
144 });
145 const fallbackCodex = makeProvider(CODEX_DRIVER, {
146 models: [
147 {
148 slug: "gpt-5.4",
149 name: "GPT-5.4",
150 isCustom: false,
151 capabilities: emptyCapabilities,
152 },
153 ],
154 message: "Pending refresh",
155 });
156
157 assert.deepStrictEqual(
158 hydrateCachedProvider({
159 cachedProvider: cachedCodex,
160 fallbackProvider: fallbackCodex,
161 }),
162 {
163 ...fallbackCodex,
164 models: [
165 ...fallbackCodex.models,
166 {
167 slug: "gpt-5-mini",
168 name: "GPT-5 Mini",
169 isCustom: false,
170 capabilities: emptyCapabilities,
171 },
172 ],
173 installed: cachedCodex.installed,
174 version: cachedCodex.version,
175 status: cachedCodex.status,
176 auth: cachedCodex.auth,
177 checkedAt: cachedCodex.checkedAt,
178 slashCommands: cachedCodex.slashCommands,
179 skills: cachedCodex.skills,
180 message: cachedCodex.message,
181 },
182 );
183 });
184
185 it("does not resurrect cached custom models that settings no longer declare", () => {
186 const builtIn = {
187 slug: "gpt-5.4",
188 name: "GPT-5.4",
189 isCustom: false,
190 capabilities: emptyCapabilities,
191 } as const;
192 const cachedCodex = makeProvider(CODEX_DRIVER, {
193 models: [
194 builtIn,
195 {
196 slug: "removed-custom",
197 name: "removed-custom",
198 isCustom: true,
199 capabilities: emptyCapabilities,
200 },
201 ],
202 });
203 const fallbackCodex = makeProvider(CODEX_DRIVER, { models: [builtIn] });
204
205 assert.deepStrictEqual(
206 hydrateCachedProvider({
207 cachedProvider: cachedCodex,
208 fallbackProvider: fallbackCodex,
209 }).models,
210 [builtIn],
211 );
212 });
213
214 it("ignores stale cached enabled state when the provider is now disabled", () => {
215 const cachedCodex = makeProvider(CODEX_DRIVER, {
216 checkedAt: "2026-04-10T12:00:00.000Z",
217 message: "Cached ready status",
218 });
219 const disabledFallback = makeProvider(CODEX_DRIVER, {
220 enabled: false,
221 installed: false,
222 version: null,
223 status: "disabled",
224 auth: { status: "unknown" },
225 message: "Codex is disabled in T3 Code settings.",
226 });
227
228 assert.deepStrictEqual(
229 hydrateCachedProvider({
230 cachedProvider: cachedCodex,
231 fallbackProvider: disabledFallback,
232 }),
233 disabledFallback,
234 );
235 });
236
237 it("rejects cached snapshots that are not correlated to the fallback instance", () => {
238 const fallbackCodex = makeProvider(CODEX_DRIVER, {
239 models: [
240 {
241 slug: "gpt-5.4",
242 name: "GPT-5.4",
243 isCustom: false,
244 capabilities: emptyCapabilities,
245 },
246 ],
247 });
248 const legacyCachedCodex = {
249 provider: ProviderDriverKind.make("codex"),
250 enabled: true,
251 installed: true,
252 version: "1.0.0",
253 status: "ready",
254 auth: { status: "authenticated" },
255 checkedAt: "2026-04-10T12:00:00.000Z",
256 models: [
257 {
258 slug: "cached-legacy-model",
259 name: "Cached Legacy Model",
260 isCustom: false,
261 capabilities: emptyCapabilities,
262 },
263 ],
264 slashCommands: [],
265 skills: [],
266 } as unknown as ServerProvider;
267 const mismatchedCachedCodex = makeProvider(CODEX_DRIVER, {
268 instanceId: ProviderInstanceId.make("codex_personal"),
269 });
270
271 assert.strictEqual(
272 isCachedProviderCorrelated({
273 cachedProvider: legacyCachedCodex,
274 fallbackProvider: fallbackCodex,
275 }),
276 false,
277 );
278 assert.deepStrictEqual(
279 hydrateCachedProvider({
280 cachedProvider: legacyCachedCodex,
281 fallbackProvider: fallbackCodex,
282 }),
283 fallbackCodex,
284 );
285 assert.strictEqual(
286 isCachedProviderCorrelated({
287 cachedProvider: mismatchedCachedCodex,
288 fallbackProvider: fallbackCodex,
289 }),
290 false,
291 );
292 assert.deepStrictEqual(
293 hydrateCachedProvider({
294 cachedProvider: mismatchedCachedCodex,
295 fallbackProvider: fallbackCodex,
296 }),
297 fallbackCodex,
298 );
299 });
300});