Skip to content
259 changes: 243 additions & 16 deletions apps/server/src/provider/providerMaintenanceRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ function layerMockSpawner(

function makeRegistry(
initialProviders: ServerProvider | ReadonlyArray<ServerProvider> = baseProvider,
options?: {
readonly installedVersionAfterUpdate?: string;
},
) {
return Effect.gen(function* () {
const providersRef = yield* Ref.make<ReadonlyArray<ServerProvider>>(
Expand Down Expand Up @@ -193,7 +196,18 @@ function makeRegistry(
refresh: () => Ref.get(providersRef),
refreshInstance: () => Ref.get(providersRef),
reloadSecretsAndRefresh: () => Ref.get(providersRef),
refreshAfterUpdate: () => Ref.get(providersRef),
refreshAfterUpdate: () => {
const version = options?.installedVersionAfterUpdate;
if (version === undefined) {
return Ref.get(providersRef);
}
return Ref.updateAndGet(providersRef, (providers) =>
providers.map((provider) => ({
...provider,
version,
})),
);
},
refreshWorkspaceSnapshot: () => Ref.get(providersRef),
getProviderMaintenanceCapabilitiesForInstance: (_instanceId, provider) =>
Effect.succeed(lifecycleFor(provider)),
Expand All @@ -220,6 +234,7 @@ const makeTestRunner = (
ranges: [],
})),
},
latestVersions: Layer.Layer<ProviderLatestVersions.ProviderLatestVersions> = ProviderLatestVersions.layer,
) =>
Effect.service(ProviderMaintenanceRunner.ProviderMaintenanceRunner).pipe(
Effect.provide(
Expand All @@ -233,13 +248,28 @@ const makeTestRunner = (
forceRefresh: Effect.succeed(manifest),
refreshInBackground: Effect.void,
}),
ProviderLatestVersions.layer,
latestVersions,
),
),
),
),
);

function layerFailingLatestLookupAfterFirst() {
let lookups = 0;
return Layer.succeed(ProviderLatestVersions.ProviderLatestVersions, {
cached: (_key, lookup) =>
Effect.gen(function* () {
lookups += 1;
if (lookups > 1) {
return yield* Effect.die("latest lookup failed");
}
return yield* lookup;
}),
invalidate: () => Effect.void,
});
}

describe("providerMaintenanceRunner", () => {
it.effect("runs the allowlisted provider update command and records success", () => {
const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];
Expand Down Expand Up @@ -269,7 +299,190 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner((command, args) => {
calls.push({ command, args });
return { stdout: "updated" };
}),
),
),
);
});

it.effect.each([
{ name: "current", probe: {}, skip: true },
{ name: "failed", probe: { status: "error" }, skip: false },
{ name: "behind", probe: { version: "2.1.309" }, skip: false },
{ name: "unknown", probe: { version: null }, skip: false },
{
name: "installed current",
probe: { version: "2.1.309", installedVersion: "2.1.310" },
skip: true,
},
{ name: "installed behind", probe: { installedVersion: "2.1.309" }, skip: false },
{ name: "installed unknown", probe: { installedVersion: null }, skip: false },
] as const)("only skips a successful fresh current probe: $name", ({ probe, skip }) => {
const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];
const refreshedInstanceIds: Array<ProviderInstanceId> = [];
return Effect.gen(function* () {
const { registry, updateStatesRef, providersRef } = yield* makeRegistry({
...baseProvider,
version: "2.1.309",
versionAdvisory: {
status: "behind_latest",
currentVersion: "2.1.309",
latestVersion: "2.1.310",
updateCommand: "claude update",
canUpdate: true,
checkedAt: "2026-04-10T00:00:00.000Z",
message: "Update available.",
},
});
const updater = yield* makeTestRunner({
...registry,
refreshInstance: (instanceId) => {
refreshedInstanceIds.push(instanceId);
return Ref.updateAndGet(providersRef, (providers) =>
providers.map((provider) =>
provider.instanceId === instanceId
? {
...provider,
version: "2.1.310",
...probe,
checkedAt: "2026-04-10T00:00:01.000Z",
}
: provider,
),
);
},
getProviderMaintenanceCapabilitiesForInstance: () =>
Effect.succeed(
makeProviderMaintenanceCapabilities({
provider: CODEX_DRIVER,
packageName: "@anthropic-ai/claude-code",
updateExecutable: "claude",
updateArgs: ["update"],
updateLockKey: "claudeAgent-native",
}),
),
});

const result = yield* updater.updateProvider(CODEX_DRIVER);
assert.deepStrictEqual(refreshedInstanceIds, [CODEX_INSTANCE_ID]);
assert.deepStrictEqual(calls, skip ? [] : [{ command: "claude", args: ["update"] }]);
if (!skip) return;
assert.strictEqual(result.providers[0]?.updateState?.status, "succeeded");
assert.strictEqual(result.providers[0]?.updateState?.message, "Already up to date.");
assert.deepStrictEqual(
(yield* Ref.get(updateStatesRef)).map((state) => [state.status, state.message]),
[
["queued", "Waiting for another provider update to finish."],
["running", "Checking for the latest version"],
["succeeded", "Already up to date."],
],
);
}).pipe(
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("2.1.310"),
layerMockSpawner((command, args) => {
calls.push({ command, args });
return { stdout: "updated" };
}),
),
),
);
});

it.effect("still runs the installer when refreshInstance returns the cached snapshot", () => {
const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];
return Effect.gen(function* () {
const { registry } = yield* makeRegistry({
...baseProvider,
version: "2.1.310",
});
const updater = yield* makeTestRunner({
...registry,
getProviderMaintenanceCapabilitiesForInstance: () =>
Effect.succeed(
makeProviderMaintenanceCapabilities({
provider: CODEX_DRIVER,
packageName: "@anthropic-ai/claude-code",
updateExecutable: "claude",
updateArgs: ["update"],
updateLockKey: "claudeAgent-native",
}),
),
});

const result = yield* updater.updateProvider(CODEX_DRIVER);
assert.deepStrictEqual(calls, [{ command: "claude", args: ["update"] }]);
assert.strictEqual(result.providers[0]?.updateState?.status, "succeeded");
}).pipe(
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("2.1.310"),
layerMockSpawner((command, args) => {
calls.push({ command, args });
return { stdout: "updated" };
}),
),
),
);
});

it.effect("still runs the installer when precheck advisory enrichment fails", () => {
const calls: Array<{ command: string; args: ReadonlyArray<string> }> = [];
return Effect.gen(function* () {
const { registry, providersRef } = yield* makeRegistry({
...baseProvider,
version: "2.1.310",
versionAdvisory: {
status: "current",
currentVersion: "2.1.310",
latestVersion: "2.1.310",
updateCommand: "claude update",
canUpdate: true,
checkedAt: "2026-04-10T00:00:00.000Z",
message: null,
},
});
const updater = yield* makeTestRunner(
{
...registry,
refreshInstance: (instanceId) =>
Ref.updateAndGet(providersRef, (providers) =>
providers.map((provider) =>
provider.instanceId === instanceId
? { ...provider, checkedAt: "2026-04-10T00:00:01.000Z" }
: provider,
),
),
getProviderMaintenanceCapabilitiesForInstance: () =>
Effect.succeed(
makeProviderMaintenanceCapabilities({
provider: CODEX_DRIVER,
packageName: "@anthropic-ai/claude-code",
updateExecutable: "claude",
updateArgs: ["update"],
updateLockKey: "claudeAgent-native",
}),
),
},
undefined,
layerFailingLatestLookupAfterFirst(),
);

const result = yield* updater.updateProvider(CODEX_DRIVER);
assert.deepStrictEqual(calls, [{ command: "claude", args: ["update"] }]);
assert.strictEqual(result.providers[0]?.updateState?.status, "succeeded");
}).pipe(
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("2.1.310"),
layerMockSpawner((command, args) => {
calls.push({ command, args });
return { stdout: "updated" };
Expand Down Expand Up @@ -298,7 +511,7 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner(() => ({ stdout: "updated" })),
),
),
Expand Down Expand Up @@ -331,7 +544,7 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner((_command, _args, options) => {
seen.push(options.env);
return { stdout: "updated" };
Expand Down Expand Up @@ -371,7 +584,7 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner((command, args) => {
calls.push({ command, args });
return { stdout: "updated" };
Expand Down Expand Up @@ -433,7 +646,7 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner((command) => {
calls.push(command);
return { stdout: "updated" };
Expand Down Expand Up @@ -461,7 +674,7 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner(() => ({
stdout: "checking install\n",
stderr: "Error: Could not detect the Codex installation method.\n\n",
Expand All @@ -487,7 +700,7 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner(() => ({ code: 1 })),
),
),
Expand Down Expand Up @@ -552,7 +765,7 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner(() => {
startedLatch.resolve();
return {
Expand All @@ -571,7 +784,9 @@ describe("providerMaintenanceRunner", () => {
"releases the running-provider marker when interrupted after queuing but before the lock run starts",
() =>
Effect.gen(function* () {
const { registry } = yield* makeRegistry(baseProvider);
const { registry } = yield* makeRegistry(baseProvider, {
installedVersionAfterUpdate: "1.0.0",
});
let blockQueuedState = true;
const queuedStateWrittenLatch: { resolve: () => void } = { resolve: () => {} };
const releaseQueuedStateLatch: { resolve: () => void } = { resolve: () => {} };
Expand Down Expand Up @@ -612,7 +827,7 @@ describe("providerMaintenanceRunner", () => {
Effect.provide(
Layer.mergeAll(
layerNonWindowsPlatform,
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
layerMockSpawner(() => ({ stdout: "updated" })),
),
),
Expand All @@ -626,7 +841,9 @@ describe("providerMaintenanceRunner", () => {
readonly shell: boolean | string | undefined;
}> = [];
return Effect.gen(function* () {
const { registry } = yield* makeRegistry(baseProvider);
const { registry } = yield* makeRegistry(baseProvider, {
installedVersionAfterUpdate: "1.0.0",
});
const runner = yield* makeTestRunner(registry);

const result = yield* runner.updateProvider(CODEX_DRIVER);
Expand All @@ -652,7 +869,7 @@ describe("providerMaintenanceRunner", () => {
Layer.succeed(SpawnExecutableResolution, (command) =>
command === "npm" ? "C:\\fake\\npm\\npm.cmd" : undefined,
),
layerLatestVersionHttpClient("0.0.0"),
layerLatestVersionHttpClient("1.0.0"),
Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) => {
Expand Down Expand Up @@ -689,8 +906,14 @@ it.effect.each([
status: "succeeded",
},
{
mode: "unknown",
mode: "targeted-current",
version: "1.14.20",
installation: { installedVersion: "1.14.20" },
status: "succeeded",
},
{
mode: "unknown",
version: "1.14.19",
installation: { installedVersion: null },
status: "unchanged",
},
Expand Down Expand Up @@ -723,6 +946,10 @@ it.effect.each([
const updater = yield* makeTestRunner(
{
...registry,
refreshInstance: () =>
Ref.updateAndGet(providersRef, (providers) =>
providers.map((provider) => ({ ...provider, checkedAt: "2026-04-10T00:00:01.000Z" })),
),
getProviderMaintenanceCapabilitiesForInstance: () => Effect.succeed(pinnedCapabilities),
refreshAfterUpdate: () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -786,7 +1013,7 @@ it.effect(
"uses fresh installer versions for post-update verification and compatibility checks",
() => {
const calls: string[] = [];
let latest = "1.0.0";
let latest = "2.0.0";
const manifest: ModelManifest.ModelManifestData = {
version: 1,
currentModels: {},
Expand Down
Loading
Loading