mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-30 04:50:45 +00:00
Co-authored-by: rekram1-node <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: mradwankhalil <293462981+mradwankhalil@users.noreply.github.com>
246 lines
11 KiB
TypeScript
246 lines
11 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { Effect, Schedule } from "effect"
|
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"
|
|
import { Credential } from "@opencode/core/credential"
|
|
import { Integration } from "@opencode/core/integration"
|
|
import { Model } from "@opencode/core/model"
|
|
import { Plugin } from "@opencode/core/plugin"
|
|
import { PluginHost } from "@opencode/core/plugin/host"
|
|
import { Provider } from "@opencode/core/provider"
|
|
import { ProviderPlugins } from "@opencode/core/plugin/provider"
|
|
import { DigitalOceanPlugin } from "@opencode/core/plugin/provider/digitalocean"
|
|
import { drain } from "../lib/clock"
|
|
import { testEffect } from "../lib/effect"
|
|
import { PluginTestLayer } from "./fixture"
|
|
|
|
const it = testEffect(PluginTestLayer)
|
|
const providerID = Provider.ID.make("digitalocean")
|
|
const integrationID = Integration.ID.make("digitalocean")
|
|
const methodID = Integration.MethodID.make("browser")
|
|
|
|
const addPlugin = Effect.fn(function* () {
|
|
const plugin = yield* Plugin.Service
|
|
const host = yield* PluginHost.make(plugin)
|
|
yield* DigitalOceanPlugin.effect(host)
|
|
})
|
|
|
|
const oauthCredential = Effect.fn(function* (access = "do-token", expires = 0) {
|
|
const credentials = yield* Credential.Service
|
|
return yield* credentials.create({
|
|
integrationID,
|
|
value: Credential.OAuth.make({ type: "oauth", methodID, access, refresh: "", expires }),
|
|
})
|
|
})
|
|
|
|
const discovery = Effect.gen(function* () {
|
|
const providers = yield* Provider.Service
|
|
const models = yield* Model.Service
|
|
yield* providers.transform((draft) => {
|
|
draft.update(providerID, (provider) => {
|
|
provider.package = "@opencode/ai/providers/openai-compatible"
|
|
provider.settings = { baseURL: "https://inference.do-ai.run/v1" }
|
|
})
|
|
draft.models.update(providerID, Model.ID.make("snapshot-model"), () => {})
|
|
draft.models.update(providerID, Model.ID.make("router:configured"), (model) => {
|
|
model.name = "Configured router"
|
|
model.limit.context = 256_000
|
|
})
|
|
draft.models.update(Provider.ID.openai, Model.ID.make("router:alpha"), () => {})
|
|
})
|
|
const remote: { status: number; body: unknown; requests: HttpClientRequest.HttpClientRequest[] } = {
|
|
status: 200,
|
|
body: { model_routers: [{ name: "alpha" }, { name: "configured" }] },
|
|
requests: [],
|
|
}
|
|
const http = HttpClient.make((request) =>
|
|
Effect.sync(() => {
|
|
remote.requests.push(request)
|
|
return HttpClientResponse.fromWeb(request, Response.json(remote.body, { status: remote.status }))
|
|
}),
|
|
)
|
|
return { models, remote, install: addPlugin().pipe(Effect.provideService(HttpClient.HttpClient, http)) }
|
|
})
|
|
|
|
const status = Effect.fn(function* (attempt: Integration.Attempt) {
|
|
const integrations = yield* Integration.Service
|
|
return yield* integrations.oauth
|
|
.status({ integrationID, attemptID: attempt.attemptID })
|
|
.pipe(
|
|
Effect.repeat({ until: (value) => value.status !== "pending", schedule: Schedule.spaced("10 millis") }),
|
|
Effect.timeout("3 seconds"),
|
|
)
|
|
})
|
|
|
|
describe("DigitalOceanPlugin", () => {
|
|
test("is registered alongside the other provider plugins", () => {
|
|
expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.digitalocean")
|
|
})
|
|
|
|
it.effect("registers browser OAuth alongside generic key and environment methods", () =>
|
|
Effect.gen(function* () {
|
|
const integrations = yield* Integration.Service
|
|
yield* integrations.transform((draft) => {
|
|
draft.method.update({ integrationID, method: { type: "key" } })
|
|
draft.method.update({ integrationID, method: { type: "env", names: ["DIGITALOCEAN_ACCESS_TOKEN"] } })
|
|
})
|
|
yield* addPlugin()
|
|
expect((yield* integrations.get(integrationID))?.methods).toEqual([
|
|
{ type: "key" },
|
|
{ type: "env", names: ["DIGITALOCEAN_ACCESS_TOKEN"] },
|
|
{ id: methodID, type: "oauth", label: "Login with DigitalOcean" },
|
|
])
|
|
}),
|
|
)
|
|
|
|
it.effect("loads routers for the signed-in account", () =>
|
|
Effect.gen(function* () {
|
|
const fixture = yield* discovery
|
|
yield* oauthCredential()
|
|
yield* fixture.install
|
|
yield* drain
|
|
|
|
expect(fixture.remote.requests).toHaveLength(1)
|
|
expect(fixture.remote.requests[0]).toMatchObject({
|
|
method: "GET",
|
|
url: "https://api.digitalocean.com/v2/gen-ai/models/routers",
|
|
headers: { authorization: "Bearer do-token", accept: "application/json", "user-agent": expect.any(String) },
|
|
})
|
|
expect(yield* fixture.models.get(providerID, Model.ID.make("router:alpha"))).toMatchObject({
|
|
name: "alpha",
|
|
family: "digitalocean-inference-routers",
|
|
package: "@opencode/ai/providers/openai-compatible",
|
|
settings: { baseURL: "https://inference.do-ai.run/v1" },
|
|
capabilities: { tools: true, input: ["text"], output: ["text"] },
|
|
limit: { context: 128_000, output: 8_192 },
|
|
})
|
|
expect(yield* fixture.models.get(providerID, Model.ID.make("router:configured"))).toMatchObject({
|
|
name: "Configured router",
|
|
limit: { context: 256_000 },
|
|
})
|
|
|
|
}),
|
|
)
|
|
|
|
it.effect("loads on credential changes and clears the previous account's routers", () =>
|
|
Effect.gen(function* () {
|
|
const fixture = yield* discovery
|
|
const credentials = yield* Credential.Service
|
|
yield* fixture.install
|
|
yield* drain
|
|
expect(fixture.remote.requests).toHaveLength(0)
|
|
|
|
yield* oauthCredential()
|
|
yield* drain
|
|
expect(yield* fixture.models.get(providerID, Model.ID.make("router:alpha"))).toBeDefined()
|
|
|
|
fixture.remote.status = 503
|
|
const second = yield* oauthCredential("other-account")
|
|
yield* drain
|
|
expect(fixture.remote.requests.at(-1)?.headers.authorization).toBe("Bearer other-account")
|
|
expect(yield* fixture.models.get(providerID, Model.ID.make("router:alpha"))).toBeUndefined()
|
|
|
|
fixture.remote.status = 200
|
|
fixture.remote.body = { model_routers: [{ name: "beta" }] }
|
|
|
|
const count = fixture.remote.requests.length
|
|
yield* credentials.create({ integrationID, value: Credential.Key.make({ type: "key", key: "do-key" }) })
|
|
yield* drain
|
|
expect(fixture.remote.requests).toHaveLength(count)
|
|
expect(yield* fixture.models.get(providerID, Model.ID.make("router:beta"))).toBeUndefined()
|
|
|
|
yield* credentials.activate(second.id)
|
|
yield* drain
|
|
expect(yield* fixture.models.get(providerID, Model.ID.make("router:beta"))).toBeDefined()
|
|
yield* credentials.remove(second.id)
|
|
yield* drain
|
|
expect(yield* fixture.models.get(providerID, Model.ID.make("router:beta"))).toBeUndefined()
|
|
}),
|
|
)
|
|
|
|
it.live("completes browser OAuth independently of router discovery and releases the listener", () =>
|
|
Effect.gen(function* () {
|
|
const fixture = yield* discovery
|
|
fixture.remote.status = 503
|
|
yield* fixture.install
|
|
const integrations = yield* Integration.Service
|
|
const credentials = yield* Credential.Service
|
|
const attempt = yield* integrations.oauth.connect({ integrationID, methodID })
|
|
const url = new URL(attempt.url)
|
|
expect(attempt.mode).toBe("auto")
|
|
expect(url.origin + url.pathname).toBe("https://cloud.digitalocean.com/v1/oauth/authorize")
|
|
expect(url.searchParams.get("response_type")).toBe("token")
|
|
expect(url.searchParams.get("client_id")).toBe("b1a6c5158156caac821fd1b30253ca8acb52454a48fa744420e41889cb589f82")
|
|
expect(url.searchParams.get("scope")).toBe("genai:read inference:query")
|
|
expect(url.searchParams.get("redirect_uri")).toBe("http://localhost:1456/auth/callback")
|
|
expect(url.searchParams.get("state")).toBeTruthy()
|
|
const page = yield* Effect.promise(() =>
|
|
fetch("http://localhost:1456/auth/callback", { headers: { Connection: "close" } }).then((res) => res.text()),
|
|
)
|
|
expect(page).toContain("/auth/token")
|
|
const now = Date.now()
|
|
const response = yield* Effect.promise(() =>
|
|
fetch("http://localhost:1456/auth/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
body: JSON.stringify({ access_token: "do-token", expires_in: "3600", state: url.searchParams.get("state") }),
|
|
}),
|
|
)
|
|
expect(response.status).toBe(200)
|
|
expect(yield* status(attempt)).toMatchObject({ status: "complete" })
|
|
const saved = (yield* credentials.list(integrationID))[0]?.value
|
|
expect(saved).toMatchObject({ type: "oauth", methodID, access: "do-token", refresh: "" })
|
|
expect(saved?.metadata).toBeUndefined()
|
|
if (saved?.type !== "oauth") throw new Error("Expected OAuth credential")
|
|
expect(saved.expires).toBeGreaterThanOrEqual(now + 3_600_000)
|
|
expect(saved.expires).toBeLessThanOrEqual(Date.now() + 3_600_000)
|
|
|
|
const next = yield* integrations.oauth.connect({ integrationID, methodID })
|
|
expect(new URL(next.url).searchParams.get("state")).not.toBe(url.searchParams.get("state"))
|
|
yield* integrations.oauth.cancel({ integrationID, attemptID: next.attemptID })
|
|
const retry = yield* integrations.oauth.connect({ integrationID, methodID })
|
|
yield* integrations.oauth.cancel({ integrationID, attemptID: retry.attemptID })
|
|
}),
|
|
)
|
|
|
|
const invalid = [
|
|
{
|
|
name: "mismatched state",
|
|
body: JSON.stringify({ access_token: "token", state: "wrong" }),
|
|
message: "Invalid OAuth state",
|
|
},
|
|
{ name: "missing token", body: JSON.stringify({}), message: "Missing access token" },
|
|
{ name: "malformed JSON", body: "not-json", message: "Missing access token" },
|
|
{
|
|
name: "provider denial",
|
|
body: JSON.stringify({ error: "access_denied", error_description: "Denied" }),
|
|
message: "Denied",
|
|
},
|
|
{
|
|
name: "provider denial without a description",
|
|
body: JSON.stringify({ error: "access_denied", error_description: "" }),
|
|
message: "access_denied",
|
|
},
|
|
]
|
|
invalid.forEach((fixture) => {
|
|
it.live(`rejects ${fixture.name} and releases the listener`, () =>
|
|
Effect.gen(function* () {
|
|
yield* addPlugin()
|
|
const integrations = yield* Integration.Service
|
|
const credentials = yield* Credential.Service
|
|
const attempt = yield* integrations.oauth.connect({ integrationID, methodID })
|
|
const response = yield* Effect.promise(() =>
|
|
fetch("http://localhost:1456/auth/token", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
body: fixture.body,
|
|
}),
|
|
)
|
|
expect(response.status).toBe(400)
|
|
expect(yield* status(attempt)).toMatchObject({ status: "failed", message: fixture.message })
|
|
expect(yield* credentials.list(integrationID)).toEqual([])
|
|
const next = yield* integrations.oauth.connect({ integrationID, methodID })
|
|
yield* integrations.oauth.cancel({ integrationID, attemptID: next.attemptID })
|
|
}),
|
|
)
|
|
})
|
|
})
|