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
55 changes: 55 additions & 0 deletions apps/server/src/provider/makeManagedServerProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as PubSub from "effect/PubSub";
import * as Queue from "effect/Queue";
import * as Ref from "effect/Ref";
import * as Stream from "effect/Stream";
import { TestClock } from "effect/testing";

import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
import { ServerSettingsService } from "../serverSettings.ts";
import { makeManagedServerProvider } from "./makeManagedServerProvider.ts";
import {
createProviderVersionAdvisory,
makeCachedProviderMaintenanceResolution,
} from "./providerMaintenance.ts";

const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] });
const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");
Expand Down Expand Up @@ -150,6 +155,56 @@ const enrichedSnapshotSecond: ServerProvider = {
};

describe("makeManagedServerProvider", () => {
it.effect(
"rereads installer metadata on explicit refresh while background checks stay cached",
() =>
Effect.scoped(
Effect.gen(function* () {
const latestVersion = yield* Ref.make("1.0.0");
const enriched = yield* Queue.unbounded<ServerProvider>();
const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
Ref.get(latestVersion).pipe(
Effect.map((version) => ({ ...maintenanceCapabilities, latestVersion: version })),
),
);
const provider = yield* makeManagedServerProvider<TestSettings>({
resolveMaintenance,
getSettings: Effect.succeed({ enabled: true }),
streamSettings: Stream.empty,
haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
initialSnapshot: () => Effect.succeed(initialSnapshot),
checkProvider: Effect.succeed(refreshedSnapshot),
enrichSnapshot: ({ snapshot, publishSnapshot }) =>
Effect.gen(function* () {
const capabilities = yield* resolveMaintenance();
const next = {
...snapshot,
versionAdvisory: createProviderVersionAdvisory({
driver: snapshot.driver,
currentVersion: snapshot.version,
latestVersion: capabilities.latestVersion ?? null,
maintenanceCapabilities: capabilities,
}),
};
yield* publishSnapshot(next);
yield* Queue.offer(enriched, next);
}),
refreshInterval: "1 minute",
});
assert.strictEqual((yield* Queue.take(enriched)).versionAdvisory?.status, "current");

yield* Ref.set(latestVersion, "2.0.0");
yield* TestClock.adjust("1 minute");
assert.strictEqual((yield* Queue.take(enriched)).versionAdvisory?.latestVersion, "1.0.0");

yield* provider.refresh;
const updated = yield* Queue.take(enriched);
assert.strictEqual(updated.versionAdvisory?.latestVersion, "2.0.0");
assert.strictEqual(updated.versionAdvisory?.status, "behind_latest");
}),
).pipe(Effect.provide(AlwaysRunTestLayer)),
);

it.effect(
"runs the initial provider check in the background and streams the refreshed snapshot",
() =>
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/provider/makeManagedServerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,12 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(
return {
resolveMaintenance: input.resolveMaintenance,
getSnapshot: Ref.get(snapshotStateRef).pipe(Effect.map((state) => state.snapshot)),
refresh: refreshSnapshot().pipe(Effect.tapError(Effect.logError), Effect.orDie),
// Explicit refreshes must observe installer changes such as `brew update` immediately.
// The interval loop keeps using refreshSnapshot directly so background probes stay cached.
refresh: Effect.gen(function* () {
yield* input.resolveMaintenance({ fresh: true });
return yield* refreshSnapshot();
}).pipe(Effect.tapError(Effect.logError), Effect.orDie),
applyUsageLimits,
get streamChanges() {
return Stream.fromPubSub(changesPubSub);
Expand Down
89 changes: 88 additions & 1 deletion apps/server/src/provider/providerCompatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import { ProviderRegistryLive } from "./Layers/ProviderRegistry.ts";
import { ProviderRegistry } from "./Services/ProviderRegistry.ts";
import { ProviderInstanceRegistry } from "./Services/ProviderInstanceRegistry.ts";
import type { ProviderInstance } from "./ProviderDriver.ts";
import { makeManualOnlyProviderMaintenanceCapabilities } from "./providerMaintenance.ts";
import {
createProviderVersionAdvisory,
makeManualOnlyProviderMaintenanceCapabilities,
makeProviderMaintenanceCapabilities,
} from "./providerMaintenance.ts";
import { BUILT_IN_DRIVERS } from "./builtInDrivers.ts";
import * as Schema from "effect/Schema";
import {
Expand Down Expand Up @@ -51,6 +55,89 @@ const provider: ServerProvider = {
slashCommands: [],
};

describe("Homebrew compatibility recovery", () => {
const recoveryPolicy: ProviderCompatibilityPolicy = {
driver,
t3CodeRange: ">=0.0.42 <0.1.0",
recommendedRange: ">=0.156.0",
ranges: [
{ range: "<0.149.0", status: "broken" },
{ range: ">=0.149.0 <0.156.0", status: "unsupported" },
{ range: ">=0.156.0", status: "supported" },
],
};
const snapshot = (
currentVersion: string,
latestVersion: string | null,
updateCommand = "brew upgrade --cask codex",
): ServerProvider => ({
...provider,
version: currentVersion,
versionAdvisory: createProviderVersionAdvisory({
driver,
currentVersion,
latestVersion,
maintenanceCapabilities: makeProviderMaintenanceCapabilities({
provider: driver,
packageName: "@openai/codex",
updateExecutable: "/opt/homebrew/bin/brew",
updateArgs: ["upgrade", "--cask", "codex"],
updateLockKey: "homebrew",
updateCommand,
}),
}),
});

it.each(["brew upgrade --cask codex", "brew upgrade codex"])(
"explains how to refresh stale metadata for %s and clears guidance after refresh",
(command) => {
const stale = snapshot("0.155.1", "0.155.1", command);
const result = applyProviderCompatibility(stale, [recoveryPolicy], []);
assert.strictEqual(result.versionAdvisory?.status, "current");
assert.strictEqual(result.compatibilityAdvisory?.latestVersionStatus, "unsupported");
assert.include(result.compatibilityAdvisory?.message ?? "", "Use >=0.156.0.");
assert.include(result.compatibilityAdvisory?.message ?? "", "brew update");
assert.include(result.compatibilityAdvisory?.message ?? "", "this environment's host");
assert.include(result.compatibilityAdvisory?.message ?? "", "refresh provider status");
assert.strictEqual(result.message, stale.message);
assert.deepStrictEqual(result.versionAdvisory, stale.versionAdvisory);

const refreshed = applyProviderCompatibility(
{ ...result, versionAdvisory: snapshot("0.155.1", "0.157.1", command).versionAdvisory! },
[recoveryPolicy],
[],
);
assert.strictEqual(refreshed.compatibilityAdvisory?.latestVersionStatus, "supported");
assert.notInclude(refreshed.compatibilityAdvisory?.message ?? "", "brew update");
},
);

it.each([
["0.148.0", "0.148.0"],
["0.155.0", "0.155.1"],
])("offers metadata recovery for incompatible %s with latest %s", (current, latest) => {
const result = applyProviderCompatibility(snapshot(current, latest), [recoveryPolicy], []);
assert.include(result.compatibilityAdvisory?.message ?? "", "brew update");
});

it.each([
["0.156.0", "0.157.1", "brew upgrade --cask codex"],
["0.155.1", "0.157.1", "brew upgrade --cask codex"],
["0.155.1", null, "brew upgrade --cask codex"],
["0.155.1", "0.155.1", "npm install -g @openai/codex@latest"],
] as const)(
"does not add irrelevant recovery guidance for %s, %s, %s",
(current, latest, command) => {
const result = applyProviderCompatibility(
snapshot(current, latest, command),
[recoveryPolicy],
[],
);
assert.notInclude(result.compatibilityAdvisory?.message ?? "", "brew update");
},
);
});

describe("provider compatibility", () => {
it("bundles a compatibility policy for every built-in harness", () => {
for (const builtIn of BUILT_IN_DRIVERS) {
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/provider/providerCompatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,22 @@ export function applyProviderCompatibility(
? (resolveProviderCompatibility(policies, snapshot.driver, latestVersion) ??
resolveProviderCompatibility(fallback, snapshot.driver, latestVersion))
: undefined;
// Homebrew's resolver emits this canonical command only after verifying ownership.
// `brew info` can keep reporting an incompatible release until metadata is refreshed.
const needsHomebrewRefresh =
snapshot.versionAdvisory?.updateCommand?.startsWith("brew upgrade ") &&
(advisory?.status === "unsupported" || advisory?.status === "broken") &&
(latestAdvisory?.status === "unsupported" || latestAdvisory?.status === "broken");
return advisory
? {
...base,
compatibilityAdvisory: {
...advisory,
...(needsHomebrewRefresh
? {
message: `${advisory.message} Homebrew's local package metadata may be stale. Run brew update on this environment's host, then refresh provider status to check for updates.`,
}
: {}),
...(latestAdvisory ? { latestVersionStatus: latestAdvisory.status } : {}),
},
}
Expand Down
Loading