| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { |
| 3 | defaultInstanceIdForDriver, |
| 4 | ProviderDriverKind, |
| 5 | ProviderInstanceId, |
| 6 | type ServerProvider, |
| 7 | } from "@t3tools/contracts"; |
| 8 | import { createModelCapabilities } from "@t3tools/shared/model"; |
| 9 | import { assert, it } from "@effect/vitest"; |
| 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as FileSystem from "effect/FileSystem"; |
| 12 | import * as Logger from "effect/Logger"; |
| 13 | |
| 14 | import { |
| 15 | hydrateCachedProvider, |
| 16 | isCachedProviderCorrelated, |
| 17 | readProviderStatusCache, |
| 18 | resolveProviderStatusCachePath, |
| 19 | writeProviderStatusCache, |
| 20 | } from "./providerStatusCache.ts"; |
| 21 | |
| 22 | const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] }); |
| 23 | const CODEX_DRIVER = ProviderDriverKind.make("codex"); |
| 24 | const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent"); |
| 25 | const OPENCODE_DRIVER = ProviderDriverKind.make("opencode"); |
| 26 | |
| 27 | const 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 | |
| 45 | it.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 | }); |