From fcb4534a8244f78d71ca5bbd2e062787ebc39098 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 22 Jul 2026 00:47:24 +0000 Subject: [PATCH] fix(core): await initial Copilot model sync --- .../src/plugin/provider/github-copilot.ts | 2 +- .../plugin/provider-github-copilot.test.ts | 79 ++++++++++++++++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts index 94414eda3d..d07bc714bc 100644 --- a/packages/core/src/plugin/provider/github-copilot.ts +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -196,6 +196,7 @@ export const GithubCopilotPlugin = define({ yield* ctx.integration.transform((draft) => { draft.method.update(oauth(ctx.app)) }) + yield* load() yield* ctx.catalog.transform((evt) => { const item = evt.provider.get(ProviderV2.ID.githubCopilot) if (!item) return @@ -227,7 +228,6 @@ export const GithubCopilotPlugin = define({ Stream.runForEach(refresh), Effect.forkScoped({ startImmediately: true }), ) - yield* refresh().pipe(Effect.forkScoped) yield* ctx.aisdk.hook( "sdk", Effect.fn(function* (evt) { diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index bb2443ae41..d009151de2 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -1,8 +1,9 @@ import { AISDK } from "@opencode-ai/core/aisdk" import { App } from "@opencode-ai/core/app" import { describe, expect, test } from "bun:test" -import { Effect } from "effect" +import { Effect, Fiber } from "effect" import { Catalog } from "@opencode-ai/core/catalog" +import { Credential } from "@opencode-ai/core/credential" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" @@ -15,11 +16,20 @@ import { PluginTestLayer } from "./fixture" const it = testEffect(PluginTestLayer) -const addPlugin = Effect.fn(function* () { +const addPlugin = Effect.fn(function* (credential?: Credential.OAuth) { const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service const host = yield* PluginHost.make(plugin) - yield* GithubCopilotPlugin.effect(host) + yield* GithubCopilotPlugin.effect({ + ...host, + integration: { + ...host.integration, + connection: { + active: () => Effect.succeed(credential ? ({ type: "env", name: "TEST" } as const) : undefined), + resolve: () => Effect.succeed(credential), + }, + }, + }) }) function required(value: T | undefined): T { @@ -62,6 +72,69 @@ describe("GithubCopilotPlugin", () => { }), ) + it.live("waits for initial Copilot models before exposing the connected catalog", () => + Effect.acquireUseRelease( + Effect.sync(() => { + const requested = Promise.withResolvers() + const release = Promise.withResolvers() + const server = Bun.serve({ + port: 0, + fetch: async () => { + requested.resolve() + await release.promise + return Response.json({ + data: [ + { + model_picker_enabled: true, + id: "gpt-5", + name: "GPT-5", + version: "gpt-5-2026-06-01", + supported_endpoints: ["/responses"], + capabilities: { + family: "gpt", + limits: { max_output_tokens: 16_384, max_prompt_tokens: 180_000 }, + supports: { tool_calls: true }, + }, + }, + ], + }) + }, + }) + return { release, requested, server } + }), + ({ release, requested, server }) => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((draft) => { + draft.provider.update(ProviderV2.ID.githubCopilot, () => {}) + draft.model.update(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale"), () => {}) + }) + const credential = Credential.OAuth.make({ + type: "oauth", + methodID: Integration.MethodID.make("device"), + access: "access", + refresh: "refresh", + expires: 0, + metadata: { apiEndpoint: server.url.origin }, + }) + + const plugin = yield* addPlugin(credential).pipe(Effect.forkScoped({ startImmediately: true })) + yield* Effect.promise(() => requested.promise) + const pending = plugin.pollUnsafe() === undefined + release.resolve() + yield* Fiber.join(plugin) + + expect(pending).toBe(true) + expect(yield* catalog.model.get(ProviderV2.ID.githubCopilot, ModelV2.ID.make("stale"))).toBeUndefined() + expect(yield* catalog.model.get(ProviderV2.ID.githubCopilot, ModelV2.ID.make("gpt-5"))).toBeDefined() + }), + ({ release, server }) => { + release.resolve() + return Effect.promise(() => server.stop(true)) + }, + ), + ) + it.live("adds Copilot authentication and request metadata headers", () => Effect.gen(function* () { const requests: Headers[] = []