diff --git a/packages/provider-core/src/server/managedProvider.test.ts b/packages/provider-core/src/server/managedProvider.test.ts index 5295232d3b14c..c9e5a6e01390d 100644 --- a/packages/provider-core/src/server/managedProvider.test.ts +++ b/packages/provider-core/src/server/managedProvider.test.ts @@ -472,6 +472,66 @@ describe("makeManagedServerProvider", () => { ).pipe(Effect.provide(layerAlwaysRunTest)), ); + it.effect( + "rechecks an auth failure when settings are applied, even if this provider's did not change", + () => + Effect.scoped( + Effect.gen(function* () { + const settingsChanges = yield* PubSub.unbounded< + TestSettings & { readonly revision: number } + >(); + const checkCalls = yield* Ref.make(0); + const seenRevisions: Array = []; + const releaseInitialCheck = yield* Deferred.make(); + const markerApplied = yield* Deferred.make(); + const unauthenticatedSnapshot: ServerProvider = { + ...refreshedSnapshot, + status: "error", + auth: { status: "unauthenticated" }, + message: "Codex CLI is not authenticated. Run `codex login` and try again.", + }; + const provider = yield* makeManagedServerProvider< + TestSettings & { readonly revision: number } + >({ + resolveMaintenance: () => Effect.succeed(maintenanceCapabilities), + getSettings: Effect.succeed({ enabled: true, revision: 0 }), + streamSettings: Stream.fromPubSub(settingsChanges), + haveSettingsChanged: (previous, next) => { + seenRevisions.push(next.revision); + if (next.revision === 2) Deferred.doneUnsafe(markerApplied, Effect.void); + return previous.enabled !== next.enabled; + }, + initialSnapshot: () => Effect.succeed(initialSnapshot), + // The credential is fixed (e.g. `codex login`) after the first, failed probe. + checkProvider: Deferred.await(releaseInitialCheck).pipe( + Effect.andThen(Ref.updateAndGet(checkCalls, (count) => count + 1)), + Effect.map((count) => (count === 1 ? unauthenticatedSnapshot : refreshedSnapshot)), + ), + refreshInterval: "1 hour", + }); + const initialProbe = yield* provider.streamChanges.pipe( + Stream.take(1), + Stream.runDrain, + Effect.forkChild, + ); + yield* Effect.yieldNow; + yield* Deferred.succeed(releaseInitialCheck, undefined); + yield* Fiber.join(initialProbe); + assert.deepStrictEqual(yield* provider.getSnapshot, unauthenticatedSnapshot); + + // A settings apply that does not touch this provider's settings, then a marker: + // the consumer is sequential, so the first apply has finished once the marker is seen. + yield* PubSub.publish(settingsChanges, { enabled: true, revision: 1 }); + yield* PubSub.publish(settingsChanges, { enabled: true, revision: 2 }); + yield* Deferred.await(markerApplied); + + assert.deepStrictEqual(seenRevisions, [1, 2]); + assert.isAtLeast(yield* Ref.get(checkCalls), 2); + assert.deepStrictEqual((yield* provider.getSnapshot).auth, refreshedSnapshot.auth); + }), + ).pipe(Effect.provide(layerAlwaysRunTest)), + ); + it.effect("streams supplemental snapshot updates after the base provider check completes", () => Effect.scoped( Effect.gen(function* () { diff --git a/packages/provider-core/src/server/managedProvider.ts b/packages/provider-core/src/server/managedProvider.ts index 39332d013e2ce..a766c95d62535 100644 --- a/packages/provider-core/src/server/managedProvider.ts +++ b/packages/provider-core/src/server/managedProvider.ts @@ -143,9 +143,15 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")( nextSettings: Settings, options?: { readonly forceRefresh?: boolean }, ) { - const forceRefresh = options?.forceRefresh === true; const previousSettings = yield* Ref.get(settingsRef); - if (!forceRefresh && !input.haveSettingsChanged(previousSettings, nextSettings)) { + const settingsChanged = + options?.forceRefresh === true || input.haveSettingsChanged(previousSettings, nextSettings); + // Nothing watches credential files, so any settings apply rechecks a failed sign-in. + const { snapshot: currentSnapshot } = yield* Ref.get(snapshotStateRef); + const forceRefresh = + options?.forceRefresh === true || + (currentSnapshot.enabled && currentSnapshot.auth.status === "unauthenticated"); + if (!forceRefresh && !settingsChanged) { yield* Ref.set(settingsRef, nextSettings); return yield* Ref.get(snapshotStateRef).pipe(Effect.map((state) => state.snapshot)); }