Files
anomalyco_opencode/packages/opencode/test/server/httpapi-public-openapi.test.ts
T
Kit Langton 76d814e747 refactor(server): unify instance httpapi middleware routing
Unify declared instance HTTP API endpoints under typed middleware routing, including event streaming and PTY WebSocket connect handling.\n\nPreserve PTY connect compatibility by checking missing PTYs before parsing optional cursor and ticket query fields, with regression coverage.
2026-05-27 08:45:11 -04:00

226 lines
8.1 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { OpenApi } from "effect/unstable/httpapi"
import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
type Method = "get" | "post" | "put" | "delete" | "patch"
type OpenApiSchema = { readonly $ref?: string }
type OpenApiResponse = {
readonly description?: string
readonly content?: Record<string, { readonly schema?: OpenApiSchema }>
}
type OpenApiOperation = {
readonly parameters?: ReadonlyArray<{ readonly name: string; readonly in: string }>
readonly responses?: Record<string, OpenApiResponse>
readonly security?: unknown
}
type OpenApiPathItem = Partial<Record<Method, OpenApiOperation>>
type OpenApiSpec = { readonly paths: Record<string, OpenApiPathItem> }
const methods = ["get", "post", "put", "delete", "patch"] as const
const allowedV2BuiltInEndpointErrors: string[] = []
function v2Operations(spec: OpenApiSpec) {
return Object.entries(spec.paths).flatMap(([path, item]) =>
path.startsWith("/api/")
? methods.flatMap((method) => {
const operation = item[method]
return operation ? [{ method, path, operation }] : []
})
: [],
)
}
function responseRef(response: OpenApiResponse | undefined) {
return response?.content?.["application/json"]?.schema?.$ref
}
function componentName(ref: string) {
return ref.replace("#/components/schemas/", "")
}
function isBuiltInEndpointError(name: string) {
return name.startsWith("EffectHttpApiError") || name.startsWith("effect_HttpApiError_")
}
describe("PublicApi OpenAPI v2 errors", () => {
test("preserves /api auth responses", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of v2Operations(spec)) {
expect(route.operation.responses?.["401"], `${route.method.toUpperCase()} ${route.path}`).toBeDefined()
expect(route.operation.security, `${route.method.toUpperCase()} ${route.path}`).toEqual([])
}
})
test("does not rewrite /api endpoint errors to legacy error components", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
const refs = v2Operations(spec)
.flatMap((route) =>
Object.entries(route.operation.responses ?? {}).flatMap(([status, response]) => {
const ref = responseRef(response)
return ref ? [`${route.method.toUpperCase()} ${route.path} ${status} ${componentName(ref)}`] : []
}),
)
.filter((entry) => entry.endsWith(" BadRequestError") || entry.endsWith(" NotFoundError"))
expect(refs).toEqual([])
})
test("new /api endpoint errors cannot use built-in components without an explicit allowlist", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
const builtInEndpointErrors = v2Operations(spec)
.flatMap((route) =>
Object.entries(route.operation.responses ?? {}).flatMap(([status, response]) => {
if (status === "401") return []
const ref = responseRef(response)
if (!ref) return []
const name = componentName(ref)
return isBuiltInEndpointError(name) ? [`${route.method.toUpperCase()} ${route.path} ${status} ${name}`] : []
}),
)
.sort()
expect(builtInEndpointErrors).toEqual(allowedV2BuiltInEndpointErrors)
})
test("documents v2 provider and model catalog errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
expect(componentName(responseRef(spec.paths["/api/provider"]?.get?.responses?.["503"]) ?? "")).toBe(
"ServiceUnavailableError",
)
expect(componentName(responseRef(spec.paths["/api/model"]?.get?.responses?.["503"]) ?? "")).toBe(
"ServiceUnavailableError",
)
expect(componentName(responseRef(spec.paths["/api/provider/{providerID}"]?.get?.responses?.["404"]) ?? "")).toBe(
"ProviderNotFoundError",
)
expect(componentName(responseRef(spec.paths["/api/provider/{providerID}"]?.get?.responses?.["503"]) ?? "")).toBe(
"ServiceUnavailableError",
)
})
test("documents v2 session not-found errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of [
["post", "/api/session/{sessionID}/prompt"],
["post", "/api/session/{sessionID}/compact"],
["post", "/api/session/{sessionID}/wait"],
["get", "/api/session/{sessionID}/context"],
["get", "/api/session/{sessionID}/message"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["404"]) ?? "")).toBe(
"SessionNotFoundError",
)
}
})
test("documents v2 unfinished session mutation errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of [
["post", "/api/session/{sessionID}/prompt"],
["post", "/api/session/{sessionID}/compact"],
["post", "/api/session/{sessionID}/wait"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["503"]) ?? "")).toBe(
"ServiceUnavailableError",
)
}
})
test("documents v2 session read data errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of [
["get", "/api/session/{sessionID}/context"],
["get", "/api/session/{sessionID}/message"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["500"]) ?? "")).toMatch(
/^UnknownError\d*$/,
)
}
})
test("documents session busy errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of [
["post", "/session/{sessionID}/shell"],
["post", "/session/{sessionID}/revert"],
["post", "/session/{sessionID}/unrevert"],
["delete", "/session/{sessionID}/message/{messageID}"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["409"]) ?? "")).toBe(
"SessionBusyError",
)
}
})
test("documents permission and question not-found errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
expect(
componentName(responseRef(spec.paths["/permission/{requestID}/reply"]?.post?.responses?.["404"]) ?? ""),
).toBe("PermissionNotFoundError")
for (const route of [
["post", "/question/{requestID}/reply"],
["post", "/question/{requestID}/reject"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["404"]) ?? "")).toBe(
"QuestionNotFoundError",
)
}
})
test("documents MCP server not-found errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of [
["post", "/mcp/{name}/auth"],
["post", "/mcp/{name}/auth/authenticate"],
["post", "/mcp/{name}/auth/callback"],
["delete", "/mcp/{name}/auth"],
["post", "/mcp/{name}/connect"],
["post", "/mcp/{name}/disconnect"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["404"]) ?? "")).toBe(
"McpServerNotFoundError",
)
}
})
test("documents PTY resource and ticket errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
for (const route of [
["get", "/pty/{ptyID}"],
["put", "/pty/{ptyID}"],
["delete", "/pty/{ptyID}"],
["post", "/pty/{ptyID}/connect-token"],
] as const) {
expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["404"]) ?? "")).toBe(
"PtyNotFoundError",
)
}
expect(componentName(responseRef(spec.paths["/pty/{ptyID}/connect-token"]?.post?.responses?.["403"]) ?? "")).toBe(
"PtyForbiddenError",
)
expect(
spec.paths["/pty/{ptyID}/connect"]?.get?.parameters
?.filter((parameter) => parameter.in === "query")
.map((parameter) => parameter.name),
).toEqual(["directory", "workspace", "cursor", "ticket"])
})
test("documents project not-found errors", () => {
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
expect(componentName(responseRef(spec.paths["/project/{projectID}"]?.patch?.responses?.["404"]) ?? "")).toBe(
"ProjectNotFoundError",
)
})
})