diff --git a/apps/server/src/provider/Drivers/CodexDriver.test.ts b/apps/server/src/provider/Drivers/CodexDriver.test.ts index 868dc9c0af03b..0c4fb617a27c5 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.test.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.test.ts @@ -17,9 +17,11 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; +import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; @@ -725,4 +727,54 @@ it.layer(layerTest)("CodexDriver", (it) => { }).pipe(Effect.scoped), { skip: windowsHost }, ); + + it.effect("probes again when auth.json in the Codex home changes", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const codexHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-auth-watch-" }); + const watchedPath = yield* Deferred.make(); + const homeEvents = yield* Queue.unbounded(); + const probes = yield* Queue.unbounded(); + yield* CodexDriver.create({ + instanceId: ProviderInstanceId.make("codex-auth-watch"), + displayName: "Codex auth watch", + enabled: true, + environment: [], + config: { ...CodexDriver.defaultConfig(), homePath: codexHome }, + }).pipe( + // OS watchers can miss events written right after they start, so the test supplies them. + Effect.provideService(FileSystem.FileSystem, { + ...fs, + watch: (path) => + Stream.unwrap( + Deferred.succeed(watchedPath, path).pipe(Effect.as(Stream.fromQueue(homeEvents))), + ), + }), + Effect.provideService( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => + (ChildProcess.isStandardCommand(command) && command.args.includes("app-server") + ? Queue.offer(probes, undefined) + : Effect.void + ).pipe( + Effect.andThen( + PlatformError.badArgument({ + module: "ChildProcessSpawner", + method: "spawn", + description: "The fixture has no Codex executable", + }), + ), + ), + ), + ), + ); + expect(yield* Deferred.await(watchedPath)).toBe(codexHome); + yield* Queue.take(probes); + + yield* Queue.offer(homeEvents, { _tag: "Update", path: "auth.json" }); + yield* TestClock.adjust("250 millis"); + + yield* Queue.take(probes); + }).pipe(Effect.scoped), + ); }); diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index dee8a04aee9fa..58ef7d27142c9 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -2,10 +2,12 @@ import { CodexSettings, ProviderDriverKind } from "@t3tools/contracts"; import * as Digest from "effect/crypto/Digest"; import * as SecureRandom from "effect/crypto/SecureRandom"; import type * as Signature from "effect/crypto/Signature"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; import { HttpClient } from "effect/http"; import { ChildProcessSpawner } from "effect/process"; @@ -371,6 +373,17 @@ export const CodexDriver: ProviderDriver< ), ); + // `codex login` and `codex logout` rewrite auth.json outside T3. + if (effectiveConfig.enabled) { + yield* fileSystem.watch(accountKey).pipe( + Stream.filter((event) => pathService.basename(event.path) === "auth.json"), + Stream.debounce(Duration.millis(250)), + Stream.runForEach(() => snapshot.refresh.pipe(Effect.ignoreCause({ log: true }))), + Effect.catchReason("PlatformError", "NotFound", () => Effect.void), + Effect.ignoreCause({ log: true }), + Effect.forkScoped, + ); + } return { instanceId, driverKind: DRIVER_KIND,