Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions packages/provider-core/src/server/managedProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> = [];
const releaseInitialCheck = yield* Deferred.make<void>();
const markerApplied = yield* Deferred.make<void>();
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* () {
Expand Down
10 changes: 8 additions & 2 deletions packages/provider-core/src/server/managedProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Loading