From 11030c627b4fd7f3a95fe36df840868851b1b194 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 10 May 2026 11:57:52 -0400 Subject: [PATCH] Scope boolean query overrides --- .../routes/instance/httpapi/groups/query.ts | 4 ++ .../server/routes/instance/httpapi/public.ts | 12 ++--- .../server/httpapi-query-schema-drift.test.ts | 47 ++++++++++--------- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/query.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/query.ts index d5b10d1800..c780f5222c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/query.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/query.ts @@ -6,3 +6,7 @@ export const QueryBoolean = Schema.Literals(["true", "false"]).pipe( encode: SchemaGetter.transform((value) => (value ? "true" : "false")), }), ) + +export const QueryBooleanOpenApi = { + anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }], +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index 612ff83aeb..4286e6f6cc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -1,5 +1,6 @@ import { OpenApi } from "effect/unstable/httpapi" import { OpenCodeHttpApi } from "./api" +import { QueryBooleanOpenApi } from "./groups/query" type OpenApiParameter = { name: string @@ -54,17 +55,20 @@ type OpenApiResponse = { // Query schemas describe decoded Effect values, but the generated SDK needs the // public call shape. These keep SDK callers passing numbers/booleans while the // server still decodes string query params at runtime. -const QueryBooleanParameters = new Set(["roots", "archived"]) const QueryParameterSchemas: Record = { "GET /experimental/session start": { type: "number" }, + "GET /experimental/session roots": QueryBooleanOpenApi, + "GET /experimental/session archived": QueryBooleanOpenApi, "GET /find/file limit": { type: "integer", minimum: 1, maximum: 200 }, "GET /experimental/session cursor": { type: "number" }, "GET /experimental/session limit": { type: "number" }, "GET /session start": { type: "number" }, + "GET /session roots": QueryBooleanOpenApi, "GET /session limit": { type: "number" }, "GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER }, "GET /api/session limit": { type: "number" }, "GET /api/session start": { type: "number" }, + "GET /api/session roots": QueryBooleanOpenApi, "GET /api/session/{sessionID}/message limit": { type: "number" }, } @@ -486,12 +490,6 @@ function normalizeParameter(param: OpenApiParameter, route: string) { param.schema = override return } - if (QueryBooleanParameters.has(param.name)) { - param.schema = { - anyOf: [{ type: "boolean" }, { type: "string", enum: ["true", "false"] }], - } - return - } } param.schema = stripOptionalNull(param.schema) } diff --git a/packages/opencode/test/server/httpapi-query-schema-drift.test.ts b/packages/opencode/test/server/httpapi-query-schema-drift.test.ts index d8eb780646..d9f2b56cb0 100644 --- a/packages/opencode/test/server/httpapi-query-schema-drift.test.ts +++ b/packages/opencode/test/server/httpapi-query-schema-drift.test.ts @@ -26,7 +26,7 @@ import { import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty" 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 { QueryBoolean } from "../../src/server/routes/instance/httpapi/groups/query" +import { QueryBoolean, QueryBooleanOpenApi } from "../../src/server/routes/instance/httpapi/groups/query" import { resetDatabase } from "../fixture/db" import { disposeAllInstances, tmpdir } from "../fixture/fixture" import { it } from "../lib/effect" @@ -36,6 +36,8 @@ const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES type Method = "get" | "post" | "put" | "delete" | "patch" type QuerySchema = { readonly fields: Record } type OpenApiSchema = { + readonly anyOf?: readonly OpenApiSchema[] + readonly enum?: readonly string[] readonly maximum?: number readonly minimum?: number readonly pattern?: string @@ -75,6 +77,13 @@ const numericSdkQueryParams = [ { method: "get", path: "/api/session/:sessionID/message", name: "limit", schema: { type: "number" } }, ] satisfies Array<{ method: Method; path: string; name: string; schema: OpenApiSchema }> +const booleanSdkQueryParams = [ + { method: "get", path: ExperimentalPaths.session, name: "roots" }, + { method: "get", path: ExperimentalPaths.session, name: "archived" }, + { method: "get", path: SessionPaths.list, name: "roots" }, + { method: "get", path: "/api/session", name: "roots" }, +] satisfies Array<{ method: Method; path: string; name: string }> + const queryParamPatterns = [ { method: "get", path: SessionPaths.diff, name: "messageID", pattern: "^msg" }, ] satisfies Array<{ method: Method; path: string; name: string; pattern: string }> @@ -174,20 +183,7 @@ describe("httpapi query schema drift", () => { ) it.effect( - "OpenAPI query parameter patterns come from runtime schemas", - Effect.sync(() => { - const spec = OpenApi.fromApi(PublicApi) - for (const expected of queryParamPatterns) { - expect( - queryParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema, - `${expected.method.toUpperCase()} ${expected.path} ${expected.name}`, - ).toEqual({ type: "string", pattern: expected.pattern }) - } - }), - ) - - it.effect( - "OpenAPI workspace query params are declared by runtime query schemas", + "OpenAPI query params are declared by runtime query schemas", Effect.sync(() => { const spec = OpenApi.fromApi(PublicApi) for (const route of openApiDriftRoutes) { @@ -200,7 +196,7 @@ describe("httpapi query schema drift", () => { ) it.effect( - "OpenAPI numeric query params preserve generated SDK call shapes", + "OpenAPI query and path schemas preserve compatibility metadata", Effect.sync(() => { const spec = OpenApi.fromApi(PublicApi) for (const expected of numericSdkQueryParams) { @@ -209,13 +205,18 @@ describe("httpapi query schema drift", () => { `${expected.method.toUpperCase()} ${expected.path} ${expected.name}`, ).toEqual(expected.schema) } - }), - ) - - it.effect( - "OpenAPI path parameter patterns come from runtime schemas", - Effect.sync(() => { - const spec = OpenApi.fromApi(PublicApi) + for (const expected of booleanSdkQueryParams) { + expect( + queryParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema, + `${expected.method.toUpperCase()} ${expected.path} ${expected.name}`, + ).toEqual(QueryBooleanOpenApi) + } + for (const expected of queryParamPatterns) { + expect( + queryParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema, + `${expected.method.toUpperCase()} ${expected.path} ${expected.name}`, + ).toEqual({ type: "string", pattern: expected.pattern }) + } for (const expected of pathParamPatterns) { expect( pathParameter(spec.paths[openApiPath(expected.path)]?.[expected.method], expected.name)?.schema,