mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-30 13:51:48 +08:00
test(httpapi): cover full OpenAPI route inventory (#24667)
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { Auth } from "@/auth"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
const AuthParams = Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
})
|
||||
|
||||
const LogQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
workspace: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
const LogInput = Schema.Struct({
|
||||
service: Schema.String.annotate({ description: "Service name for the log entry" }),
|
||||
level: Schema.Union([
|
||||
Schema.Literal("debug"),
|
||||
Schema.Literal("info"),
|
||||
Schema.Literal("error"),
|
||||
Schema.Literal("warn"),
|
||||
]).annotate({ description: "Log level" }),
|
||||
message: Schema.String.annotate({ description: "Log message" }),
|
||||
extra: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)).annotate({
|
||||
description: "Additional metadata for the log entry",
|
||||
}),
|
||||
}).annotate({ identifier: "AppLogInput" })
|
||||
|
||||
export const ControlPaths = {
|
||||
auth: "/auth/:providerID",
|
||||
log: "/log",
|
||||
} as const
|
||||
|
||||
export const ControlApi = HttpApi.make("control")
|
||||
.add(
|
||||
HttpApiGroup.make("control")
|
||||
.add(
|
||||
HttpApiEndpoint.put("authSet", ControlPaths.auth, {
|
||||
params: AuthParams,
|
||||
payload: Auth.Info,
|
||||
success: Schema.Boolean,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "auth.set",
|
||||
summary: "Set auth credentials",
|
||||
description: "Set authentication credentials",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("authRemove", ControlPaths.auth, {
|
||||
params: AuthParams,
|
||||
success: Schema.Boolean,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "auth.remove",
|
||||
summary: "Remove auth credentials",
|
||||
description: "Remove authentication credentials",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("log", ControlPaths.log, {
|
||||
query: LogQuery,
|
||||
payload: LogInput,
|
||||
success: Schema.Boolean,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "app.log",
|
||||
summary: "Write log",
|
||||
description: "Write a log entry to the server logs with specified level and metadata.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "control", description: "Control plane routes." })),
|
||||
)
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Bus } from "@/bus"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as Stream from "effect/Stream"
|
||||
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
const log = Log.create({ service: "server" })
|
||||
|
||||
@@ -10,6 +11,23 @@ export const EventPaths = {
|
||||
event: "/event",
|
||||
} as const
|
||||
|
||||
export const EventApi = HttpApi.make("event")
|
||||
.add(
|
||||
HttpApiGroup.make("event")
|
||||
.add(
|
||||
HttpApiEndpoint.get("subscribe", EventPaths.event, {
|
||||
success: Schema.Unknown,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "event.subscribe",
|
||||
summary: "Subscribe to events",
|
||||
description: "Get events",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "event", description: "Instance event stream route." })),
|
||||
)
|
||||
|
||||
function eventData(data: unknown) {
|
||||
return `data: ${JSON.stringify(data)}\n\n`
|
||||
}
|
||||
|
||||
102
packages/opencode/src/server/routes/instance/httpapi/global.ts
Normal file
102
packages/opencode/src/server/routes/instance/httpapi/global.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
const GlobalHealth = Schema.Struct({
|
||||
healthy: Schema.Literal(true),
|
||||
version: Schema.String,
|
||||
}).annotate({ identifier: "GlobalHealth" })
|
||||
|
||||
const GlobalEvent = Schema.Struct({
|
||||
directory: Schema.String,
|
||||
project: Schema.optional(Schema.String),
|
||||
workspace: Schema.optional(Schema.String),
|
||||
payload: Schema.Unknown,
|
||||
}).annotate({ identifier: "GlobalEvent" })
|
||||
|
||||
const GlobalUpgradeInput = Schema.Struct({
|
||||
target: Schema.optional(Schema.String),
|
||||
}).annotate({ identifier: "GlobalUpgradeInput" })
|
||||
|
||||
const GlobalUpgradeResult = Schema.Union([
|
||||
Schema.Struct({
|
||||
success: Schema.Literal(true),
|
||||
version: Schema.String,
|
||||
}),
|
||||
Schema.Struct({
|
||||
success: Schema.Literal(false),
|
||||
error: Schema.String,
|
||||
}),
|
||||
]).annotate({ identifier: "GlobalUpgradeResult" })
|
||||
|
||||
export const GlobalPaths = {
|
||||
health: "/global/health",
|
||||
event: "/global/event",
|
||||
config: "/global/config",
|
||||
dispose: "/global/dispose",
|
||||
upgrade: "/global/upgrade",
|
||||
} as const
|
||||
|
||||
export const GlobalApi = HttpApi.make("global")
|
||||
.add(
|
||||
HttpApiGroup.make("global")
|
||||
.add(
|
||||
HttpApiEndpoint.get("health", GlobalPaths.health, {
|
||||
success: GlobalHealth,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.health",
|
||||
summary: "Get health",
|
||||
description: "Get health information about the OpenCode server.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("event", GlobalPaths.event, {
|
||||
success: GlobalEvent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.event",
|
||||
summary: "Get global events",
|
||||
description: "Subscribe to global events from the OpenCode system using server-sent events.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("configGet", GlobalPaths.config, {
|
||||
success: Config.Info,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.config.get",
|
||||
summary: "Get global configuration",
|
||||
description: "Retrieve the current global OpenCode configuration settings and preferences.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("configUpdate", GlobalPaths.config, {
|
||||
payload: Config.Info,
|
||||
success: Config.Info,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.config.update",
|
||||
summary: "Update global configuration",
|
||||
description: "Update global OpenCode configuration settings and preferences.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("dispose", GlobalPaths.dispose, {
|
||||
success: Schema.Boolean,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.dispose",
|
||||
summary: "Dispose instance",
|
||||
description: "Clean up and dispose all OpenCode instances, releasing all resources.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("upgrade", GlobalPaths.upgrade, {
|
||||
payload: GlobalUpgradeInput,
|
||||
success: GlobalUpgradeResult,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.upgrade",
|
||||
summary: "Upgrade opencode",
|
||||
description: "Upgrade opencode to the specified version or latest if not specified.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "global", description: "Global server routes." })),
|
||||
)
|
||||
@@ -113,6 +113,25 @@ export const PtyApi = HttpApi.make("pty")
|
||||
}),
|
||||
)
|
||||
|
||||
export const PtyConnectApi = HttpApi.make("pty-connect")
|
||||
.add(
|
||||
HttpApiGroup.make("pty-connect")
|
||||
.add(
|
||||
HttpApiEndpoint.get("connect", PtyPaths.connect, {
|
||||
params: Params,
|
||||
query: CursorQuery,
|
||||
success: Schema.Boolean,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.connect",
|
||||
summary: "Connect to PTY session",
|
||||
description: "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "pty", description: "PTY websocket route." })),
|
||||
)
|
||||
|
||||
export const ptyHandlers = Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const pty = yield* Pty.Service
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { HttpApi, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ConfigApi } from "./config"
|
||||
import { ControlApi } from "./control"
|
||||
import { EventApi } from "./event"
|
||||
import { ExperimentalApi } from "./experimental"
|
||||
import { FileApi } from "./file"
|
||||
import { GlobalApi } from "./global"
|
||||
import { InstanceApi } from "./instance"
|
||||
import { McpApi } from "./mcp"
|
||||
import { PermissionApi } from "./permission"
|
||||
import { ProjectApi } from "./project"
|
||||
import { ProviderApi } from "./provider"
|
||||
import { PtyApi, PtyConnectApi } from "./pty"
|
||||
import { QuestionApi } from "./question"
|
||||
import { SessionApi } from "./session"
|
||||
import { SyncApi } from "./sync"
|
||||
import { TuiApi } from "./tui"
|
||||
import { WorkspaceApi } from "./workspace"
|
||||
|
||||
export const PublicApi = HttpApi.make("opencode")
|
||||
.addHttpApi(ControlApi)
|
||||
.addHttpApi(GlobalApi)
|
||||
.addHttpApi(EventApi)
|
||||
.addHttpApi(ConfigApi)
|
||||
.addHttpApi(ExperimentalApi)
|
||||
.addHttpApi(FileApi)
|
||||
.addHttpApi(InstanceApi)
|
||||
.addHttpApi(McpApi)
|
||||
.addHttpApi(PermissionApi)
|
||||
.addHttpApi(ProjectApi)
|
||||
.addHttpApi(ProviderApi)
|
||||
.addHttpApi(PtyApi)
|
||||
.addHttpApi(PtyConnectApi)
|
||||
.addHttpApi(QuestionApi)
|
||||
.addHttpApi(SessionApi)
|
||||
.addHttpApi(SyncApi)
|
||||
.addHttpApi(TuiApi)
|
||||
.addHttpApi(WorkspaceApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode",
|
||||
version: "1.0.0",
|
||||
description: "opencode api",
|
||||
}),
|
||||
)
|
||||
@@ -19,8 +19,10 @@ import { SessionApi } from "../../src/server/routes/instance/httpapi/session"
|
||||
import { SyncApi } from "../../src/server/routes/instance/httpapi/sync"
|
||||
import { TuiApi } from "../../src/server/routes/instance/httpapi/tui"
|
||||
import { WorkspaceApi } from "../../src/server/routes/instance/httpapi/workspace"
|
||||
import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
|
||||
import { Server } from "../../src/server/server"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { HttpApi, HttpApiGroup } from "effect/unstable/httpapi"
|
||||
import { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
@@ -33,6 +35,7 @@ const original = {
|
||||
}
|
||||
|
||||
const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
|
||||
const methods = ["get", "post", "put", "delete", "patch"] as const
|
||||
|
||||
function app(input?: { password?: string; username?: string }) {
|
||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||
@@ -75,6 +78,12 @@ function reflectedHttpApiRoutes() {
|
||||
return [...new Set(routes)]
|
||||
}
|
||||
|
||||
function openApiRouteKeys(spec: { paths: Record<string, Partial<Record<(typeof methods)[number], unknown>>> }) {
|
||||
return Object.entries(spec.paths)
|
||||
.flatMap(([path, item]) => methods.filter((method) => item[method]).map((method) => `${method.toUpperCase()} ${path}`))
|
||||
.sort()
|
||||
}
|
||||
|
||||
function authorization(username: string, password: string) {
|
||||
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
||||
}
|
||||
@@ -129,6 +138,14 @@ describe("HttpApi Hono bridge", () => {
|
||||
expect([...bridgeRoutes].filter((route) => !httpApiRoutes.includes(route)).sort()).toEqual([])
|
||||
})
|
||||
|
||||
test("covers every generated OpenAPI route with Effect HttpApi contracts", async () => {
|
||||
const honoRoutes = openApiRouteKeys(await Server.openapi())
|
||||
const effectRoutes = openApiRouteKeys(OpenApi.fromApi(PublicApi))
|
||||
|
||||
expect(honoRoutes.filter((route) => !effectRoutes.includes(route))).toEqual([])
|
||||
expect(effectRoutes.filter((route) => !honoRoutes.includes(route))).toEqual([])
|
||||
})
|
||||
|
||||
test("allows requests when auth is disabled", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await Bun.write(`${tmp.path}/hello.txt`, "hello")
|
||||
|
||||
Reference in New Issue
Block a user