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
19 changes: 13 additions & 6 deletions apps/server/src/provider/Layers/OpenCodeProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,13 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {
: Effect.succeed(runtimeMock.state.inventory as OpenCodeInventory),
loadInventoryFromCli: () =>
runtimeMock.state.inventoryError
? Effect.succeed({
providerList: { all: [], default: {}, connected: [] as string[] },
agents: [],
} as OpenCodeInventory)
? Effect.fail(
new OpenCodeRuntimeError({
operation: "loadInventoryFromCli",
detail: runtimeMock.state.inventoryError.message,
cause: runtimeMock.state.inventoryError,
}),
)
: Effect.succeed(runtimeMock.state.inventory as OpenCodeInventory),
};

Expand Down Expand Up @@ -212,14 +215,18 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {
}),
);

it.effect("degrades gracefully on CLI failure for local installs", () =>
it.effect("reports local model inventory failures without treating them as empty", () =>
Effect.gen(function* () {
runtimeMock.state.inventoryError = new Error("opencode models failed");
const snapshot = yield* checkOpenCodeProviderStatus(makeOpenCodeSettings(), process.cwd());

NodeAssert.equal(snapshot.status, "warning");
NodeAssert.equal(snapshot.status, "error");
NodeAssert.equal(snapshot.installed, true);
NodeAssert.equal(snapshot.models.length, 0);
NodeAssert.equal(
snapshot.message,
"Failed to execute OpenCode CLI health check: opencode models failed",
);
}),
);
});
Expand Down
308 changes: 308 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,176 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
]);
});

it("drops stale OpenCode models missing from a successful refresh", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("opencode"),
driver: ProviderDriverKind.make("opencode"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-07-17T00:00:00.000Z",
version: "1.0.0",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
{
slug: "removed-plugin/model",
name: "Removed Plugin Model",
subProvider: "Removed Plugin",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const refreshedProvider = {
...previousProvider,
checkedAt: "2026-07-17T00:01:00.000Z",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
],
} satisfies ServerProvider;

assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
...refreshedProvider.models,
]);
});

it("retains stale OpenCode models when a refresh fails", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("opencode"),
driver: ProviderDriverKind.make("opencode"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-07-17T00:00:00.000Z",
version: "1.0.0",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const refreshedProvider = {
...previousProvider,
status: "error",
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:01:00.000Z",
models: [],
message: "Failed to refresh OpenCode models.",
} satisfies ServerProvider;

assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, refreshedProvider).models, [
...previousProvider.models,
]);
});

it("classifies pending, logout, uninstall, and reconnect OpenCode inventories", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("opencode"),
driver: ProviderDriverKind.make("opencode"),
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-07-17T00:00:00.000Z",
version: "1.0.0",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
{
slug: "removed-plugin/model",
name: "Removed Plugin Model",
subProvider: "Removed Plugin",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const pendingProvider = {
...previousProvider,
status: "warning",
installed: false,
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:01:00.000Z",
version: null,
models: [],
message: "OpenCode provider status has not been checked in this session yet.",
} satisfies ServerProvider;
const loggedOutProvider = {
...previousProvider,
status: "warning",
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:02:00.000Z",
models: [],
message: "OpenCode is available, but it did not report any connected upstream providers.",
} satisfies ServerProvider;
const missingProvider = {
...previousProvider,
status: "error",
installed: false,
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:03:00.000Z",
version: null,
models: [],
message: "OpenCode CLI (`opencode`) is not installed or not on PATH.",
} satisfies ServerProvider;
const authoritativeProvider = {
...previousProvider,
checkedAt: "2026-07-17T00:04:00.000Z",
models: [previousProvider.models[0]!],
} satisfies ServerProvider;
const failedProvider = {
...authoritativeProvider,
status: "error",
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:05:00.000Z",
models: [],
message: "Failed to refresh OpenCode models.",
} satisfies ServerProvider;

assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, pendingProvider).models, [
...previousProvider.models,
]);
assert.deepStrictEqual(
mergeProviderSnapshot(previousProvider, loggedOutProvider).models,
[],
);
assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, missingProvider).models, []);

const afterRemoval = mergeProviderSnapshot(previousProvider, authoritativeProvider);
const afterFailure = mergeProviderSnapshot(afterRemoval, failedProvider);

assert.deepStrictEqual(afterFailure.models, [authoritativeProvider.models[0]!]);
});

it("fills missing capabilities from the previous provider snapshot", () => {
const previousProvider = {
instanceId: ProviderInstanceId.make("cursor"),
Expand Down Expand Up @@ -866,6 +1036,144 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
}),
);

it.effect(
"persists authoritative OpenCode removals without resurrecting them on a failed live refresh",
() =>
Effect.gen(function* () {
const openCodeDriver = ProviderDriverKind.make("opencode");
const openCodeInstanceId = ProviderInstanceId.make("opencode");
const initialProvider = {
instanceId: openCodeInstanceId,
driver: openCodeDriver,
status: "ready",
enabled: true,
installed: true,
auth: { status: "authenticated" },
checkedAt: "2026-07-17T00:00:00.000Z",
version: "1.0.0",
models: [
{
slug: "github/gpt-5",
name: "GPT-5",
subProvider: "GitHub",
isCustom: false,
capabilities: null,
},
{
slug: "removed-plugin/model",
name: "Removed Plugin Model",
subProvider: "Removed Plugin",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
} as const satisfies ServerProvider;
const authoritativeProvider = {
...initialProvider,
checkedAt: "2026-07-17T00:01:00.000Z",
models: [initialProvider.models[0]!],
} satisfies ServerProvider;
const failedProvider = {
...authoritativeProvider,
status: "error",
auth: { status: "unknown" },
checkedAt: "2026-07-17T00:02:00.000Z",
models: [],
message: "Failed to refresh OpenCode models.",
} satisfies ServerProvider;
const changes = yield* PubSub.unbounded<ServerProvider>();
const instance = {
instanceId: openCodeInstanceId,
driverKind: openCodeDriver,
continuationIdentity: {
driverKind: openCodeDriver,
continuationKey: "opencode:instance:opencode",
},
displayName: undefined,
enabled: true,
snapshot: {
maintenanceCapabilities: makeManualOnlyProviderMaintenanceCapabilities({
provider: openCodeDriver,
packageName: null,
}),
getSnapshot: Effect.succeed(initialProvider),
refresh: Effect.succeed(authoritativeProvider),
streamChanges: Stream.fromPubSub(changes),
},
adapter: {} as ProviderInstance["adapter"],
textGeneration: {} as ProviderInstance["textGeneration"],
} satisfies ProviderInstance;
const instanceRegistryLayer = Layer.succeed(
ProviderInstanceRegistry.ProviderInstanceRegistry,
{
getInstance: (instanceId) =>
Effect.succeed(instanceId === openCodeInstanceId ? instance : undefined),
listInstances: Effect.succeed([instance]),
listUnavailable: Effect.succeed([]),
streamChanges: Stream.empty,
subscribeChanges: Effect.flatMap(PubSub.unbounded<void>(), (pubsub) =>
PubSub.subscribe(pubsub),
),
},
);
const scope = yield* Scope.make();
yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
const runtimeServices = yield* Layer.build(
ProviderRegistryLive.pipe(
Layer.provideMerge(instanceRegistryLayer),
Layer.provideMerge(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-provider-registry-opencode-authoritative-persist-",
}),
),
Layer.provideMerge(NodeServices.layer),
),
).pipe(Scope.provide(scope));

yield* Effect.gen(function* () {
const registry = yield* ProviderRegistry.ProviderRegistry;
const config = yield* ServerConfig.ServerConfig;
const filePath = yield* resolveProviderStatusCachePath({
cacheDir: config.providerStatusCacheDir,
instanceId: openCodeInstanceId,
});

yield* PubSub.publish(changes, authoritativeProvider);

let cachedProvider = yield* readProviderStatusCache(filePath);
for (
let attempt = 0;
attempt < 50 && cachedProvider?.checkedAt !== authoritativeProvider.checkedAt;
attempt += 1
) {
yield* TestClock.adjust("10 millis");
yield* Effect.yieldNow;
cachedProvider = yield* readProviderStatusCache(filePath);
}

assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);

yield* PubSub.publish(changes, failedProvider);
for (
let attempt = 0;
attempt < 50 && cachedProvider?.checkedAt !== failedProvider.checkedAt;
attempt += 1
) {
yield* TestClock.adjust("10 millis");
yield* Effect.yieldNow;
cachedProvider = yield* readProviderStatusCache(filePath);
}

assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
authoritativeProvider.models[0]!,
]);
}).pipe(Effect.provide(runtimeServices));
}),
);

it.effect("returns the cached provider list when a manual refresh fails", () =>
Effect.gen(function* () {
const codexDriver = ProviderDriverKind.make("codex");
Expand Down
Loading
Loading