From f4cb3170d4bac622358bbcf7e00d696fc8c2c84b Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 9 May 2026 20:27:14 -0400 Subject: [PATCH] refactor(httpapi-exercise): migrate query-schema-drift scenarios into harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the 8 standalone scenarios from packages/opencode/test/server/httpapi-query-schema-drift.test.ts into the route-coverage exerciser as `.viaSdk(...)` scenarios. Each one exercises a routing-aware GET through the real SDK client wired to the in-process router, so the SDK's auto-injected `?directory=...` / `?workspace=...` query params hit the same typed schemas as in production. Routes now covered by `.viaSdk(...)` scenarios in the exerciser: /session (session.list.via_sdk) /session/{sessionID}/message (session.messages.via_sdk) /find/file (find.files.via_sdk) /find (find.text.via_sdk) /file/content (file.read.via_sdk) /experimental/session (experimental.session.list.via_sdk) /experimental/tool (experimental.tool.list.via_sdk) /vcs/diff (vcs.diff.via_sdk) Standalone httpapi-query-schema-drift.test.ts deleted — its coverage now lives in the harness alongside every other route test, single source of truth. 149/149 scenarios pass after migration (139 existing + 2 from proof-of-concept #26604 + 8 migrated here). --- packages/opencode/config.json | 3 + .../test/server/httpapi-exercise/index.ts | 41 ++++++ .../server/httpapi-query-schema-drift.test.ts | 134 ------------------ 3 files changed, 44 insertions(+), 134 deletions(-) create mode 100644 packages/opencode/config.json delete mode 100644 packages/opencode/test/server/httpapi-query-schema-drift.test.ts diff --git a/packages/opencode/config.json b/packages/opencode/config.json new file mode 100644 index 0000000000..c3eb6a56fa --- /dev/null +++ b/packages/opencode/config.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://opencode.ai/config.json" +} \ No newline at end of file diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index 8cacb47132..2911521b2d 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -1260,6 +1260,47 @@ const scenarios: Scenario[] = [ .probe({ path: "/global/upgrade", body: { target: 1 } }) .at(() => ({ path: "/global/upgrade", body: { target: 1 } })) .status(400), + + // ─── SDK routing-params drift coverage (replaces httpapi-query-schema-drift.test.ts) ─── + // Each routing-aware GET endpoint runs through the real SDK client, which + // auto-injects `?directory=...&workspace=...`. Pre-#26581 these all 4xx'd + // because the typed query schemas didn't accept the SDK's injected fields. + // Any future endpoint added under WorkspaceRoutingMiddleware that forgets + // WorkspaceRoutingQueryFields will fail its `.viaSdk(...)` scenario on + // first run. + http.protected + .get("/session", "session.list.via_sdk") + .viaSdk((sdk) => sdk.session.list({ roots: true }, { throwOnError: true })) + .json(200, array, "status"), + http.protected + .get("/session/{sessionID}/message", "session.messages.via_sdk") + .at((ctx) => ({ path: route("/session/{sessionID}/message", { sessionID: "ses_via_sdk_drift" }), headers: ctx.headers() })) + .viaSdk((sdk) => sdk.session.messages({ sessionID: "ses_via_sdk_drift", limit: 80 })) + .status(404, undefined, "status"), + http.protected + .get("/find/file", "find.files.via_sdk") + .viaSdk((sdk) => sdk.find.files({ query: "foo" }, { throwOnError: true })) + .json(200, array, "status"), + http.protected + .get("/find", "find.text.via_sdk") + .viaSdk((sdk) => sdk.find.text({ pattern: "foo" }, { throwOnError: true })) + .json(200, array, "status"), + http.protected + .get("/file/content", "file.read.via_sdk") + .viaSdk((sdk) => sdk.file.read({ path: "foo" }, { throwOnError: true })) + .json(200, undefined, "status"), + http.protected + .get("/experimental/session", "experimental.session.list.via_sdk") + .viaSdk((sdk) => sdk.experimental.session.list({}, { throwOnError: true })) + .json(200, array, "status"), + http.protected + .get("/experimental/tool", "experimental.tool.list.via_sdk") + .viaSdk((sdk) => sdk.tool.list({ provider: "anthropic", model: "claude" }, { throwOnError: true })) + .json(200, array, "status"), + http.protected + .get("/vcs/diff", "vcs.diff.via_sdk") + .viaSdk((sdk) => sdk.vcs.diff({ mode: "git" }, { throwOnError: true })) + .json(200, array, "status"), ] const llmScenarios = new Set([ diff --git a/packages/opencode/test/server/httpapi-query-schema-drift.test.ts b/packages/opencode/test/server/httpapi-query-schema-drift.test.ts deleted file mode 100644 index 68daeca1e9..0000000000 --- a/packages/opencode/test/server/httpapi-query-schema-drift.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { afterEach, describe, expect } from "bun:test" -import { Effect } from "effect" -import { Flag } from "@opencode-ai/core/flag/flag" -import { Server } from "../../src/server/server" -import { SessionID } from "../../src/session/schema" -import { resetDatabase } from "../fixture/db" -import { disposeAllInstances, tmpdir } from "../fixture/fixture" -import { it } from "../lib/effect" - -const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES - -function app() { - return Server.Default().app -} - -function request(url: string, init?: RequestInit) { - return Effect.promise(async () => app().request(url, init)) -} - -function withTmp( - options: Parameters[0], - fn: (tmp: Awaited>) => Effect.Effect, -) { - return Effect.acquireRelease( - Effect.promise(() => tmpdir(options)), - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ).pipe(Effect.flatMap(fn)) -} - -afterEach(async () => { - Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces - await disposeAllInstances() - await resetDatabase() -}) - -// Regression for the "OpenAPI advertises ?directory&workspace, runtime -// rejects them" drift class. Each affected route must accept both params -// without 400. -describe("httpapi query schema drift", () => { - const routingParams = (dir: string) => - `directory=${encodeURIComponent(dir)}&workspace=${encodeURIComponent("ws_test")}` - - const expectNotSchemaRejection = (status: number, url: string) => { - expect(status, `route ${url} 400'd, query schema is missing routing fields`).not.toBe(400) - } - - it.live( - "session list accepts directory and workspace", - withTmp({ config: { formatter: false, lsp: false } }, (tmp) => - Effect.gen(function* () { - const url = `/session?${routingParams(tmp.path)}` - const response = yield* request(url) - expectNotSchemaRejection(response.status, url) - }), - ), - ) - - it.live( - "session messages accepts directory and workspace", - withTmp({ config: { formatter: false, lsp: false } }, (tmp) => - Effect.gen(function* () { - const url = `/session/${SessionID.descending()}/message?limit=80&${routingParams(tmp.path)}` - const response = yield* request(url) - expectNotSchemaRejection(response.status, url) - }), - ), - ) - - it.live( - "file find/file accepts directory and workspace", - withTmp({ config: { formatter: false, lsp: false } }, (tmp) => - Effect.gen(function* () { - const url = `/find/file?query=foo&${routingParams(tmp.path)}` - const response = yield* request(url) - expectNotSchemaRejection(response.status, url) - }), - ), - ) - - it.live( - "file find/text accepts directory and workspace", - withTmp({ config: { formatter: false, lsp: false } }, (tmp) => - Effect.gen(function* () { - const url = `/find?pattern=foo&${routingParams(tmp.path)}` - const response = yield* request(url) - expectNotSchemaRejection(response.status, url) - }), - ), - ) - - it.live( - "file read accepts directory and workspace", - withTmp({ config: { formatter: false, lsp: false } }, (tmp) => - Effect.gen(function* () { - const url = `/file?path=foo&${routingParams(tmp.path)}` - const response = yield* request(url) - expectNotSchemaRejection(response.status, url) - }), - ), - ) - - it.live( - "experimental session list accepts directory and workspace", - withTmp({ config: { formatter: false, lsp: false } }, (tmp) => - Effect.gen(function* () { - const url = `/experimental/session?${routingParams(tmp.path)}` - const response = yield* request(url) - expectNotSchemaRejection(response.status, url) - }), - ), - ) - - it.live( - "experimental tool list accepts directory and workspace", - withTmp({ config: { formatter: false, lsp: false } }, (tmp) => - Effect.gen(function* () { - const url = `/experimental/tool?provider=anthropic&model=claude&${routingParams(tmp.path)}` - const response = yield* request(url) - expectNotSchemaRejection(response.status, url) - }), - ), - ) - - it.live( - "vcs diff accepts directory and workspace", - withTmp({ config: { formatter: false, lsp: false } }, (tmp) => - Effect.gen(function* () { - const url = `/vcs/diff?mode=working&${routingParams(tmp.path)}` - const response = yield* request(url) - expectNotSchemaRejection(response.status, url) - }), - ), - ) -})