From 2ca34a7e632fcfad0ab367cce82c8c67a8209fca Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 9 Jul 2026 13:30:11 -0400 Subject: [PATCH] test(server): reproduce partial startup catalog reads --- .../server/test/catalog-readiness.test.ts | 56 +++++++++++++++++++ .../test/fixtures/delayed-catalog-plugin.ts | 18 ++++++ 2 files changed, 74 insertions(+) create mode 100644 packages/server/test/catalog-readiness.test.ts create mode 100644 packages/server/test/fixtures/delayed-catalog-plugin.ts diff --git a/packages/server/test/catalog-readiness.test.ts b/packages/server/test/catalog-readiness.test.ts new file mode 100644 index 00000000000..18fec93a6b4 --- /dev/null +++ b/packages/server/test/catalog-readiness.test.ts @@ -0,0 +1,56 @@ +import fs from "fs/promises" +import os from "os" +import path from "path" +import { afterEach, describe, expect, test } from "bun:test" +import { Layer } from "effect" +import { HttpRouter, HttpServer } from "effect/unstable/http" +import { createEmbeddedRoutes } from "../src/routes" + +const directories: string[] = [] + +afterEach(async () => { + await Promise.all(directories.splice(0).map((directory) => fs.rm(directory, { recursive: true, force: true }))) +}) + +describe("catalog readiness", () => { + test("catalog reads wait for initial plugin activation", async () => { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-server-catalog-")) + directories.push(directory) + await fs.writeFile( + path.join(directory, "opencode.json"), + JSON.stringify({ + plugins: ["-*", path.join(import.meta.dir, "fixtures/delayed-catalog-plugin.ts")], + }), + ) + + const server = HttpRouter.toWebHandler(createEmbeddedRoutes().pipe(Layer.provide(HttpServer.layerServices))) + const request = (pathname: string) => + server.handler( + new Request(`http://localhost${pathname}`, { + headers: { "x-opencode-directory": directory }, + }), + ) + + try { + const [providers, models] = await Promise.all([request("/api/provider"), request("/api/model")]) + expect(providers.status).toBe(200) + expect(models.status).toBe(200) + const initialProviders = (await providers.json()).data + const initialModels = (await models.json()).data + + expect((await request("/api/model/default")).status).toBe(200) + const [readyProviders, readyModels] = await Promise.all([request("/api/provider"), request("/api/model")]) + expect((await readyProviders.json()).data).toContainEqual(expect.objectContaining({ id: "delayed-provider" })) + expect((await readyModels.json()).data).toContainEqual( + expect.objectContaining({ providerID: "delayed-provider", id: "delayed-model" }), + ) + + expect(initialProviders).toContainEqual(expect.objectContaining({ id: "delayed-provider" })) + expect(initialModels).toContainEqual( + expect.objectContaining({ providerID: "delayed-provider", id: "delayed-model" }), + ) + } finally { + await server.dispose() + } + }) +}) diff --git a/packages/server/test/fixtures/delayed-catalog-plugin.ts b/packages/server/test/fixtures/delayed-catalog-plugin.ts new file mode 100644 index 00000000000..f584803fbf1 --- /dev/null +++ b/packages/server/test/fixtures/delayed-catalog-plugin.ts @@ -0,0 +1,18 @@ +import { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Effect } from "effect" + +export default Plugin.define({ + id: "delayed-catalog", + effect: Effect.fn(function* (ctx) { + yield* Effect.sleep("300 millis") + yield* ctx.catalog.transform((catalog) => { + catalog.provider.update("delayed-provider", (provider) => { + provider.name = "Delayed Provider" + }) + catalog.model.update("delayed-provider", "delayed-model", (model) => { + model.name = "Delayed Model" + model.enabled = true + }) + }) + }), +})