Fix OpenAPI workspace query drift (#26609)
This commit is contained in:
@@ -1,14 +1,53 @@
|
||||
import { afterEach, describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Server } from "../../src/server/server"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
|
||||
import {
|
||||
FilePaths,
|
||||
FileQuery,
|
||||
FindFileQuery,
|
||||
FindTextQuery,
|
||||
} from "../../src/server/routes/instance/httpapi/groups/file"
|
||||
import {
|
||||
ExperimentalPaths,
|
||||
SessionListQuery as ExperimentalSessionListQuery,
|
||||
ToolListQuery,
|
||||
} from "../../src/server/routes/instance/httpapi/groups/experimental"
|
||||
import { InstancePaths, VcsDiffQuery } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||
import {
|
||||
ListQuery as SessionListQuery,
|
||||
MessagesQuery,
|
||||
SessionPaths,
|
||||
} from "../../src/server/routes/instance/httpapi/groups/session"
|
||||
import { MessagesQuery as V2MessagesQuery } from "../../src/server/routes/instance/httpapi/groups/v2/message"
|
||||
import { SessionsQuery as V2SessionsQuery } from "../../src/server/routes/instance/httpapi/groups/v2/session"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||
import { it } from "../lib/effect"
|
||||
|
||||
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||
|
||||
type Method = "get" | "post" | "put" | "delete" | "patch"
|
||||
type QuerySchema = { readonly fields: Record<string, unknown> }
|
||||
type OpenApiParameter = { readonly name: string; readonly in: string }
|
||||
type OpenApiOperation = { readonly parameters?: readonly OpenApiParameter[] }
|
||||
|
||||
const openApiDriftRoutes = [
|
||||
{ method: "get", path: SessionPaths.list, query: SessionListQuery },
|
||||
{ method: "get", path: SessionPaths.messages, query: MessagesQuery },
|
||||
{ method: "get", path: FilePaths.findFile, query: FindFileQuery },
|
||||
{ method: "get", path: FilePaths.findText, query: FindTextQuery },
|
||||
{ method: "get", path: FilePaths.list, query: FileQuery },
|
||||
{ method: "get", path: ExperimentalPaths.session, query: ExperimentalSessionListQuery },
|
||||
{ method: "get", path: ExperimentalPaths.tool, query: ToolListQuery },
|
||||
{ method: "get", path: InstancePaths.vcsDiff, query: VcsDiffQuery },
|
||||
{ method: "get", path: "/api/session", query: V2SessionsQuery },
|
||||
{ method: "get", path: "/api/session/:sessionID/message", query: V2MessagesQuery },
|
||||
] satisfies Array<{ method: Method; path: string; query: QuerySchema }>
|
||||
|
||||
function app() {
|
||||
return Server.Default().app
|
||||
}
|
||||
@@ -27,6 +66,29 @@ function withTmp<A, E, R>(
|
||||
).pipe(Effect.flatMap(fn))
|
||||
}
|
||||
|
||||
function openApiPath(path: string) {
|
||||
return path.replace(/:([A-Za-z0-9_]+)/g, "{$1}")
|
||||
}
|
||||
|
||||
function queryParameters(operation: OpenApiOperation | undefined) {
|
||||
return (operation?.parameters ?? []).filter((param) => param.in === "query").map((param) => param.name)
|
||||
}
|
||||
|
||||
function assertAdvertisedQueryParamsAreRuntimeFields(input: {
|
||||
readonly method: Method
|
||||
readonly operation: OpenApiOperation | undefined
|
||||
readonly path: string
|
||||
readonly query: QuerySchema
|
||||
}) {
|
||||
const runtimeFields = new Set(Object.keys(input.query.fields))
|
||||
const advertisedOnly = queryParameters(input.operation).filter((name) => !runtimeFields.has(name))
|
||||
|
||||
expect(
|
||||
advertisedOnly,
|
||||
`${input.method.toUpperCase()} ${input.path} advertises query params not accepted by runtime schema`,
|
||||
).toEqual([])
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
|
||||
await disposeAllInstances()
|
||||
@@ -44,6 +106,38 @@ describe("httpapi query schema drift", () => {
|
||||
expect(status, `route ${url} 400'd, query schema is missing routing fields`).not.toBe(400)
|
||||
}
|
||||
|
||||
it.effect(
|
||||
"OpenAPI workspace query params are declared by runtime query schemas",
|
||||
Effect.sync(() => {
|
||||
const spec = OpenApi.fromApi(PublicApi)
|
||||
for (const route of openApiDriftRoutes) {
|
||||
assertAdvertisedQueryParamsAreRuntimeFields({
|
||||
...route,
|
||||
operation: spec.paths[openApiPath(route.path)]?.[route.method],
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect(
|
||||
"drift assertion catches spec-only workspace query params",
|
||||
Effect.sync(() => {
|
||||
expect(() =>
|
||||
assertAdvertisedQueryParamsAreRuntimeFields({
|
||||
method: "get",
|
||||
operation: {
|
||||
parameters: [
|
||||
{ name: "directory", in: "query" },
|
||||
{ name: "workspace", in: "query" },
|
||||
],
|
||||
},
|
||||
path: "/fixture",
|
||||
query: { fields: {} },
|
||||
}),
|
||||
).toThrow("advertises query params not accepted by runtime schema")
|
||||
}),
|
||||
)
|
||||
|
||||
it.live(
|
||||
"session list accepts directory and workspace",
|
||||
withTmp({ config: { formatter: false, lsp: false } }, (tmp) =>
|
||||
|
||||
Reference in New Issue
Block a user