Skip to content
Open
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
173 changes: 173 additions & 0 deletions apps/server/src/provider/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {
ServerSettingsError,
ServerProvider,
type ServerProviderSlashCommand,
type ServerProviderUpdateState,
type ServerProviderVersionAdvisoryStatus,
type ServerSettings as ContractServerSettings,
} from "@t3tools/contracts";
import { HttpClient, HttpClientResponse } from "effect/http";
Expand Down Expand Up @@ -305,6 +307,85 @@ const awaitPersistedProvider = (
Effect.forkScoped,
);

const codexUpdateSnapshot = (advisory: ServerProviderVersionAdvisoryStatus) =>
({
instanceId: ProviderInstanceId.make("codex"),
driver: ProviderDriverKind.make("codex"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-06-10T00:00:00.000Z",
version: "1.0.0",
models: [],
slashCommands: [],
skills: [],
versionAdvisory: {
status: advisory,
currentVersion: "1.0.0",
latestVersion: advisory === "unknown" ? null : advisory === "current" ? "1.0.0" : "2.0.0",
updateCommand: null,
canUpdate: true,
checkedAt: "2026-06-10T00:00:00.000Z",
message: null,
},
}) as const satisfies ServerProvider;

const makeUpdateState = (status: ServerProviderUpdateState["status"]) =>
({
status,
startedAt: "2026-06-10T00:00:00.000Z",
finishedAt: status === "queued" || status === "running" ? null : "2026-06-10T00:01:00.000Z",
message: `update ${status}`,
output: null,
}) satisfies ServerProviderUpdateState;

// One Codex instance whose probe returns whatever `snapshot` currently holds.
const buildRegistryOverSnapshot = Effect.fn("buildRegistryOverSnapshot")(function* (
snapshot: Ref.Ref<ServerProvider>,
) {
const driver = ProviderDriverKind.make("codex");
const instance = {
instanceId: ProviderInstanceId.make("codex"),
driverKind: driver,
continuationIdentity: { driverKind: driver, continuationKey: "codex:instance:codex" },
displayName: undefined,
enabled: true,
snapshot: {
resolveMaintenance: () =>
Effect.succeed(
makeManualOnlyProviderMaintenanceCapabilities({ provider: driver, packageName: null }),
),
getSnapshot: Ref.get(snapshot),
refresh: Ref.get(snapshot),
streamChanges: Stream.empty,
applyUsageLimits: () => Effect.void,
},
orchestrationAdapter: {} as ProviderInstance["orchestrationAdapter"],
textGeneration: {} as ProviderInstance["textGeneration"],
} satisfies ProviderInstance;
const layerInstanceRegistry = Layer.succeed(ProviderInstanceRegistry.ProviderInstanceRegistry, {
getInstance: (instanceId) =>
Effect.succeed(instanceId === instance.instanceId ? instance : undefined),
listInstances: Effect.succeed([instance]),
listUnavailable: Effect.succeed([]),
streamChanges: Stream.empty,
subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), PubSub.subscribe),
});
const scope = yield* Scope.make();
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
return yield* Layer.build(
ProviderRegistry.layer.pipe(
Layer.provideMerge(layerInstanceRegistry),
Layer.provideMerge(
ServerConfig.layerTest(process.cwd(), { prefix: "t3-provider-registry-update-state-" }),
),
Layer.provideMerge(layerBackgroundPolicyAlwaysRun),
Layer.provideMerge(NodeServices.layer),
),
).pipe(Scope.provide(scope));
});

const layerTestNodeServices = Layer.mergeAll(
ProviderLatestVersions.layer,
NodeServices.layer,
Expand Down Expand Up @@ -1928,6 +2009,98 @@ it.layer(
}),
);

describe("update results across refreshes", () => {
const instanceId = ProviderInstanceId.make("codex");

it.effect.each([
{ status: "unchanged", advisory: "current", retired: true },
{ status: "failed", advisory: "current", retired: true },
{ status: "unchanged", advisory: "behind_latest", retired: false },
{ status: "failed", advisory: "behind_latest", retired: false },
// The probe publishes `unknown` until the latest-version check lands.
{ status: "unchanged", advisory: "unknown", retired: false },
{ status: "queued", advisory: "current", retired: false },
{ status: "running", advisory: "current", retired: false },
{ status: "succeeded", advisory: "current", retired: false },
{ status: "failed", advisory: "unknown", retired: false },
] as const)(
"a $status update result is retired: $retired when a refresh reports the provider $advisory",
({ status, advisory, retired }) =>
Effect.gen(function* () {
const snapshot = yield* Ref.make<ServerProvider>(codexUpdateSnapshot("behind_latest"));
const runtimeServices = yield* buildRegistryOverSnapshot(snapshot);

yield* Effect.gen(function* () {
const registry = yield* ProviderRegistry.ProviderRegistry;
const state = makeUpdateState(status);
yield* registry.setProviderMaintenanceActionState({
instanceId,
action: "update",
state,
});
yield* Ref.set(snapshot, codexUpdateSnapshot(advisory));

const [refreshed] = yield* registry.refresh(ProviderDriverKind.make("codex"));

expect(refreshed?.updateState).toEqual(retired ? undefined : state);
}).pipe(Effect.provide(runtimeServices));
}),
);

it.effect.each(["failed", "unchanged"] as const)(
"keeps a %s recommended-version install after a current refresh",
(status) =>
Effect.gen(function* () {
const snapshot = yield* Ref.make<ServerProvider>(codexUpdateSnapshot("current"));
const runtimeServices = yield* buildRegistryOverSnapshot(snapshot);
yield* Effect.gen(function* () {
const registry = yield* ProviderRegistry.ProviderRegistry;
const state = makeUpdateState(status);
const input = {
instanceId,
action: "update" as const,
state,
targetVersion: "0.9.0",
};
yield* registry.setProviderMaintenanceActionState(input);
const [refreshed] = yield* registry.refresh(ProviderDriverKind.make("codex"));
expect(refreshed?.updateState).toEqual(state);
}).pipe(Effect.provide(runtimeServices));
}),
);

it.effect("does not bring a retired result back, and a later update still reports", () =>
Effect.gen(function* () {
const snapshot = yield* Ref.make<ServerProvider>(codexUpdateSnapshot("behind_latest"));
const runtimeServices = yield* buildRegistryOverSnapshot(snapshot);

yield* Effect.gen(function* () {
const registry = yield* ProviderRegistry.ProviderRegistry;
const codex = ProviderDriverKind.make("codex");
yield* registry.setProviderMaintenanceActionState({
instanceId,
action: "update",
state: makeUpdateState("unchanged"),
});
yield* Ref.set(snapshot, codexUpdateSnapshot("current"));
yield* registry.refresh(codex);

yield* Ref.set(snapshot, codexUpdateSnapshot("unknown"));
const [reprobed] = yield* registry.refresh(codex);
expect(reprobed?.updateState).toBeUndefined();

const running = makeUpdateState("running");
const [updating] = yield* registry.setProviderMaintenanceActionState({
instanceId,
action: "update",
state: running,
});
expect(updating?.updateState).toEqual(running);
}).pipe(Effect.provide(runtimeServices));
}),
);
});

it.effect("re-probes when settings change the codex binaryPath", () =>
Effect.gen(function* () {
const firstMissing = `t3code_codex_first_`;
Expand Down
34 changes: 23 additions & 11 deletions apps/server/src/provider/ProviderRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class ProviderRegistry extends Context.Service<
readonly instanceId: ProviderInstanceId;
readonly action: ProviderMaintenanceActionKind;
readonly state: ServerProviderUpdateState | null;
readonly targetVersion?: string | undefined;
}) => Effect.Effect<ReadonlyArray<ServerProvider>>;

readonly streamChanges: Stream.Stream<ReadonlyArray<ServerProvider>>;
Expand Down Expand Up @@ -415,7 +416,10 @@ export const layer = Layer.effect(
ReadonlyMap<ProviderInstance, ReadonlyMap<string, Deferred.Deferred<void>>>
>(new Map());
const maintenanceActionStatesRef = yield* Ref.make<
ReadonlyMap<ProviderInstanceId, { readonly update?: ServerProviderUpdateState | undefined }>
ReadonlyMap<
ProviderInstanceId,
{ readonly update?: ServerProviderUpdateState; readonly targetVersion?: string | undefined }
>
>(new Map());

const liveSubsRef = yield* Ref.make<ReadonlyMap<ProviderInstanceId, ProviderInstance>>(
Expand Down Expand Up @@ -528,6 +532,21 @@ export const layer = Layer.effect(
readonly publish?: boolean;
},
) {
// Latest-version status cannot resolve a failed recommended-version install.
if (provider.versionAdvisory?.status === "current") {
yield* Ref.update(maintenanceActionStatesRef, (previous) => {
const action = previous.get(provider.instanceId);
if (
action?.targetVersion !== undefined ||
(action?.update?.status !== "unchanged" && action?.update?.status !== "failed")
) {
return previous;
}
const next = new Map(previous);
next.delete(provider.instanceId);
return next;
});
}
const providers = yield* upsertProviders([provider], options);
// Reclassify the current model after fetch; republishing the old probe could overwrite newer health results.
if (!(yield* Ref.getAndSet(compatibilityRefreshRunning, true))) {
Expand All @@ -545,21 +564,14 @@ export const layer = Layer.effect(
readonly instanceId: ProviderInstanceId;
readonly action: "update";
readonly state: ServerProviderUpdateState | null;
readonly targetVersion?: string | undefined;
}) {
yield* Ref.update(maintenanceActionStatesRef, (previous) => {
const previousActions = previous.get(input.instanceId);
const nextActions = { ...previousActions };
if (input.state === null || input.state.status === "idle") {
delete nextActions[input.action];
} else {
nextActions[input.action] = input.state;
}

const next = new Map(previous);
if (Object.keys(nextActions).length === 0) {
if (input.state === null || input.state.status === "idle") {
next.delete(input.instanceId);
} else {
next.set(input.instanceId, nextActions);
next.set(input.instanceId, { update: input.state, targetVersion: input.targetVersion });
}
return next;
});
Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/provider/providerMaintenanceRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ it.effect.each([
"verifies the $mode OpenCode update using the refreshed installation",
({ mode, version, installation, status }) => {
const calls: string[] = [];
const targets: Array<string | undefined> = [];
const capabilities = lifecycleFor(OPENCODE_DRIVER);
const pinnedCapabilities = {
...capabilities,
Expand All @@ -722,6 +723,10 @@ it.effect.each([
const updater = yield* makeTestRunner(
{
...registry,
setProviderMaintenanceActionState: (input) => {
targets.push(input.targetVersion);
return registry.setProviderMaintenanceActionState(input);
},
getProviderMaintenanceCapabilitiesForInstance: () => Effect.succeed(pinnedCapabilities),
refreshAfterUpdate: () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -752,6 +757,8 @@ it.effect.each([
assert.deepStrictEqual(calls, [
`npm install -g opencode-ai@${mode === "default" ? "latest" : "1.14.20"}`,
]);
assert.ok(targets.length > 0);
assert.ok(targets.every((target) => target === (mode === "default" ? undefined : "1.14.20")));
const provider = result.providers[0];
assert.ok(provider);
assert.strictEqual(provider.updateState?.status, status);
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/providerMaintenanceRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () {
instanceId,
action: "update",
state,
targetVersion,
});
const setQueuedState = setUpdateState(
makeUpdateState({
Expand Down
Loading