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) - }), - ), - ) -})