Преглед изворни кода

fix(httpapi): enforce instance route parity (#24660)

Kit Langton пре 4 месеци
родитељ
комит
51fc10e407

+ 21 - 0
packages/opencode/src/server/routes/instance/httpapi/pty.ts

@@ -1,6 +1,7 @@
 import { EffectBridge } from "@/effect/bridge"
 import { Pty } from "@/pty"
 import { PtyID } from "@/pty/schema"
+import { Shell } from "@/shell/shell"
 import { Effect, Layer, Schema } from "effect"
 import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
 import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -14,8 +15,14 @@ const Params = Schema.Struct({
 const CursorQuery = Schema.Struct({
   cursor: Schema.optional(Schema.String),
 })
+const ShellItem = Schema.Struct({
+  path: Schema.String,
+  name: Schema.String,
+  acceptable: Schema.Boolean,
+})
 
 export const PtyPaths = {
+  shells: `${root}/shells`,
   list: root,
   create: root,
   get: `${root}/:ptyID`,
@@ -28,6 +35,15 @@ export const PtyApi = HttpApi.make("pty")
   .add(
     HttpApiGroup.make("pty")
       .add(
+        HttpApiEndpoint.get("shells", PtyPaths.shells, {
+          success: Schema.Array(ShellItem),
+        }).annotateMerge(
+          OpenApi.annotations({
+            identifier: "pty.shells",
+            summary: "List available shells",
+            description: "Get a list of available shells on the system.",
+          }),
+        ),
         HttpApiEndpoint.get("list", PtyPaths.list, {
           success: Schema.Array(Pty.Info),
         }).annotateMerge(
@@ -101,6 +117,10 @@ export const ptyHandlers = Layer.unwrap(
   Effect.gen(function* () {
     const pty = yield* Pty.Service
 
+    const shells = Effect.fn("PtyHttpApi.shells")(function* () {
+      return yield* Effect.promise(() => Shell.list())
+    })
+
     const list = Effect.fn("PtyHttpApi.list")(function* () {
       return yield* pty.list()
     })
@@ -143,6 +163,7 @@ export const ptyHandlers = Layer.unwrap(
 
     return HttpApiBuilder.group(PtyApi, "pty", (handlers) =>
       handlers
+        .handle("shells", shells)
         .handle("list", list)
         .handle("create", create)
         .handle("get", get)

+ 1 - 0
packages/opencode/src/server/routes/instance/index.ts

@@ -99,6 +99,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
     app.post(SyncPaths.start, (c) => handler(c.req.raw, context))
     app.post(SyncPaths.replay, (c) => handler(c.req.raw, context))
     app.post(SyncPaths.history, (c) => handler(c.req.raw, context))
+    app.get(PtyPaths.shells, (c) => handler(c.req.raw, context))
     app.get(PtyPaths.list, (c) => handler(c.req.raw, context))
     app.post(PtyPaths.create, (c) => handler(c.req.raw, context))
     app.get(PtyPaths.get, (c) => handler(c.req.raw, context))

+ 23 - 0
packages/opencode/test/server/httpapi-bridge.test.ts

@@ -3,6 +3,7 @@ import type { UpgradeWebSocket } from "hono/ws"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { Instance } from "../../src/project/instance"
 import { InstanceRoutes } from "../../src/server/routes/instance"
+import { WorkspaceRoutes } from "../../src/server/routes/control/workspace"
 import { FilePaths } from "../../src/server/routes/instance/httpapi/file"
 import * as Log from "@opencode-ai/core/util/log"
 import { resetDatabase } from "../fixture/db"
@@ -25,6 +26,10 @@ function app(input?: { password?: string; username?: string }) {
   return InstanceRoutes(websocket)
 }
 
+function routeKey(route: ReturnType<typeof InstanceRoutes>["routes"][number]) {
+  return `${route.method} ${route.path}`
+}
+
 function authorization(username: string, password: string) {
   return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
 }
@@ -46,6 +51,24 @@ afterEach(async () => {
 })
 
 describe("HttpApi Hono bridge", () => {
+  test("mounts experimental handlers for every legacy instance route", () => {
+    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
+    const legacy = InstanceRoutes(websocket)
+    Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
+    const experimental = InstanceRoutes(websocket)
+
+    const bridge = experimental.routes.slice(0, experimental.routes.length - legacy.routes.length)
+    const workspaceRoutes = WorkspaceRoutes().routes.map((route) => ({
+      ...route,
+      path: `/experimental/workspace${route.path === "/" ? "" : route.path}`,
+    }))
+    const legacyRoutes = [...new Set([...legacy.routes, ...workspaceRoutes].map(routeKey))]
+    const bridgeRoutes = new Set(bridge.map(routeKey))
+
+    expect(legacyRoutes.filter((route) => !bridgeRoutes.has(route))).toEqual([])
+    expect([...bridgeRoutes].filter((route) => !legacyRoutes.includes(route)).sort()).toEqual([])
+  })
+
   test("allows requests when auth is disabled", async () => {
     await using tmp = await tmpdir({ git: true })
     await Bun.write(`${tmp.path}/hello.txt`, "hello")

+ 16 - 0
packages/opencode/test/server/httpapi-pty.test.ts

@@ -27,6 +27,22 @@ afterEach(async () => {
 })
 
 describe("pty HttpApi bridge", () => {
+  test("serves available shell list through experimental Effect routes", async () => {
+    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
+    const response = await app().request(PtyPaths.shells, { headers: { "x-opencode-directory": tmp.path } })
+
+    expect(response.status).toBe(200)
+    expect(await response.json()).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({
+          path: expect.any(String),
+          name: expect.any(String),
+          acceptable: expect.any(Boolean),
+        }),
+      ]),
+    )
+  })
+
   testPty("serves PTY JSON routes through experimental Effect routes", async () => {
     await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
     const headers = { "x-opencode-directory": tmp.path }