Skip to content
Merged
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
163 changes: 71 additions & 92 deletions packages/opencode/test/server/httpapi-provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { afterEach, describe, expect } from "bun:test"
import { describe, expect } from "bun:test"
import { Effect, FileSystem, Layer, Path } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance"
import { InstanceRuntime } from "../../src/project/instance-runtime"
import { Server } from "../../src/server/server"
import * as Log from "@opencode-ai/core/util/log"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, provideInstance } from "../fixture/fixture"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"

void Log.init({ print: false })

const it = testEffect(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer))
const testStateLayer = Layer.effectDiscard(
Effect.acquireRelease(
Effect.promise(() => resetDatabase()),
() => Effect.promise(() => resetDatabase()),
),
)

const it = testEffect(Layer.mergeAll(testStateLayer, NodeFileSystem.layer, NodePath.layer))
const projectOptions = { config: { formatter: false, lsp: false } }
const providerID = "test-oauth-parity"
const oauthURL = "https://example.com/oauth"
const oauthInstructions = "Finish OAuth"
Expand Down Expand Up @@ -191,91 +196,69 @@ function writeProviderModelsMutationPlugin(dir: string) {
})
}

function withProviderProject<A, E, R>(self: (dir: string) => Effect.Effect<A, E, R>) {
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })

yield* fs.writeFileString(
path.join(dir, "opencode.json"),
JSON.stringify({ $schema: "https://opencode.ai/config.json", formatter: false, lsp: false }),
)
yield* writeProviderAuthPlugin(dir)
yield* Effect.addFinalizer(() =>
Effect.promise(() =>
WithInstance.provide({ directory: dir, fn: () => InstanceRuntime.disposeInstance(Instance.current) }),
).pipe(Effect.ignore),
)

return yield* self(dir).pipe(provideInstance(dir))
})
function setEnvScoped(key: string, value: string) {
return Effect.acquireRelease(
Effect.sync(() => {
const previous = process.env[key]
process.env[key] = value
return previous
}),
(previous) =>
Effect.sync(() => {
if (previous === undefined) delete process.env[key]
else process.env[key] = previous
}),
)
}

afterEach(async () => {
await disposeAllInstances()
await resetDatabase()
})

describe("provider HttpApi", () => {
it.live(
it.instance(
"serves OAuth authorize response shapes",
withProviderProject((dir) =>
Effect.gen(function* () {
const headers = { "x-opencode-directory": dir, "content-type": "application/json" }
const server = app()

const api = yield* requestAuthorize({
app: server,
providerID,
method: 0,
headers,
})
// method 0 (api-key style) — authorize() resolves with no further
// redirect; #26474 changed the wire format to JSON `null` so clients
// can `.json()` parse uniformly instead of getting an empty body
// that throws.
expect(api).toEqual({ status: 200, body: "null" })

const oauth = yield* requestAuthorize({
app: server,
providerID,
method: 1,
headers,
})
expect(JSON.parse(oauth.body)).toEqual({
url: oauthURL,
method: "code",
instructions: oauthInstructions,
})
}),
),
Effect.gen(function* () {
const instance = yield* TestInstance
yield* writeProviderAuthPlugin(instance.directory)
const headers = { "x-opencode-directory": instance.directory, "content-type": "application/json" }
const server = app()

const api = yield* requestAuthorize({
app: server,
providerID,
method: 0,
headers,
})
// method 0 (api-key style) — authorize() resolves with no further
// redirect; #26474 changed the wire format to JSON `null` so clients
// can `.json()` parse uniformly instead of getting an empty body
// that throws.
expect(api).toEqual({ status: 200, body: "null" })

const oauth = yield* requestAuthorize({
app: server,
providerID,
method: 1,
headers,
})
expect(JSON.parse(oauth.body)).toEqual({
url: oauthURL,
method: "code",
instructions: oauthInstructions,
})
}),
projectOptions,
)

it.live("serves provider lists when auth loaders add runtime fetch options", () =>
it.instance(
"serves provider lists when auth loaders add runtime fetch options",
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })
const previous = process.env.OPENCODE_AUTH_CONTENT

yield* fs.writeFileString(
path.join(dir, "opencode.json"),
JSON.stringify({ $schema: "https://opencode.ai/config.json", formatter: false, lsp: false }),
)
yield* writeFunctionOptionsPlugin(dir)
yield* Effect.sync(() => {
process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({
const instance = yield* TestInstance
yield* writeFunctionOptionsPlugin(instance.directory)
yield* setEnvScoped(
"OPENCODE_AUTH_CONTENT",
JSON.stringify({
google: { type: "oauth", refresh: "dummy", access: "dummy", expires: 9999999999999 },
})
})
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
if (previous === undefined) delete process.env.OPENCODE_AUTH_CONTENT
if (previous !== undefined) process.env.OPENCODE_AUTH_CONTENT = previous
}),
)
const headers = { "x-opencode-directory": dir }
const headers = { "x-opencode-directory": instance.directory }
const providerResponse = yield* Effect.promise(() => Promise.resolve(app().request("/provider", { headers })))
const configResponse = yield* Effect.promise(() =>
Promise.resolve(app().request("/config/providers", { headers })),
Expand All @@ -291,21 +274,16 @@ describe("provider HttpApi", () => {
expect(hasNonZeroModelCost(providerBody, "all", "google")).toBe(true)
expect(hasNonZeroModelCost(configBody, "providers", "google")).toBe(true)
}),
projectOptions,
)

it.live("keeps provider.models hook input mutations out of provider state", () =>
it.instance(
"keeps provider.models hook input mutations out of provider state",
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const path = yield* Path.Path
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })

yield* fs.writeFileString(
path.join(dir, "opencode.json"),
JSON.stringify({ $schema: "https://opencode.ai/config.json", formatter: false, lsp: false }),
)
yield* writeProviderModelsMutationPlugin(dir)
const instance = yield* TestInstance
yield* writeProviderModelsMutationPlugin(instance.directory)

const headers = { "x-opencode-directory": dir }
const headers = { "x-opencode-directory": instance.directory }
const providerResponse = yield* Effect.promise(() => Promise.resolve(app().request("/provider", { headers })))
const configResponse = yield* Effect.promise(() =>
Promise.resolve(app().request("/config/providers", { headers })),
Expand All @@ -320,5 +298,6 @@ describe("provider HttpApi", () => {
expect(hasProviderMutationMarker(configBody, "providers", "google")).toBe(false)
expect(hasNonZeroModelCost(providerBody, "all", "google")).toBe(true)
}),
projectOptions,
)
})
Loading