Просмотр исходного кода

fix(httpapi): return session busy error bodies (#28684)

Shoubhit Dash 2 месяцев назад
Родитель
Сommit
82b796ce31

+ 5 - 5
packages/opencode/src/server/routes/instance/httpapi/groups/session.ts

@@ -19,7 +19,7 @@ import {
   WorkspaceRoutingQuery,
   WorkspaceRoutingQueryFields,
 } from "../middleware/workspace-routing"
-import { ApiNotFoundError } from "../errors"
+import { ApiNotFoundError, SessionBusyError } from "../errors"
 import { described } from "./metadata"
 import { QueryBoolean } from "./query"
 
@@ -354,7 +354,7 @@ export const SessionApi = HttpApi.make("session")
           query: WorkspaceRoutingQuery,
           payload: ShellPayload,
           success: described(MessageV2.WithParts, "Created message"),
-          error: [HttpApiError.BadRequest, ApiNotFoundError],
+          error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
         }).annotateMerge(
           OpenApi.annotations({
             identifier: "session.shell",
@@ -367,7 +367,7 @@ export const SessionApi = HttpApi.make("session")
           query: WorkspaceRoutingQuery,
           payload: RevertPayload,
           success: described(Session.Info, "Updated session"),
-          error: [HttpApiError.BadRequest, ApiNotFoundError],
+          error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
         }).annotateMerge(
           OpenApi.annotations({
             identifier: "session.revert",
@@ -380,7 +380,7 @@ export const SessionApi = HttpApi.make("session")
           params: { sessionID: SessionID },
           query: WorkspaceRoutingQuery,
           success: described(Session.Info, "Updated session"),
-          error: [HttpApiError.BadRequest, ApiNotFoundError],
+          error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
         }).annotateMerge(
           OpenApi.annotations({
             identifier: "session.unrevert",
@@ -406,7 +406,7 @@ export const SessionApi = HttpApi.make("session")
           params: { sessionID: SessionID, messageID: MessageID },
           query: WorkspaceRoutingQuery,
           success: described(Schema.Boolean, "Successfully deleted message"),
-          error: [HttpApiError.BadRequest, ApiNotFoundError],
+          error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
         }).annotateMerge(
           OpenApi.annotations({
             identifier: "session.deleteMessage",

+ 10 - 2
packages/opencode/src/server/routes/instance/httpapi/handlers/session-errors.ts

@@ -1,7 +1,6 @@
 import type { NotFoundError as StorageNotFoundError } from "@/storage/storage"
 import type { Session } from "@/session/session"
 import { Effect } from "effect"
-import { HttpApiError } from "effect/unstable/httpapi"
 import * as ApiError from "../errors"
 
 export function mapStorageNotFound<A, R>(self: Effect.Effect<A, StorageNotFoundError, R>) {
@@ -9,5 +8,14 @@ export function mapStorageNotFound<A, R>(self: Effect.Effect<A, StorageNotFoundE
 }
 
 export function mapBusy<A, R>(self: Effect.Effect<A, Session.BusyError, R>) {
-  return self.pipe(Effect.catchTag("SessionBusyError", () => Effect.fail(new HttpApiError.BadRequest({}))))
+  return self.pipe(
+    Effect.catchTag("SessionBusyError", (error) =>
+      Effect.fail(
+        new ApiError.SessionBusyError({
+          sessionID: error.sessionID,
+          message: `Session is busy: ${error.sessionID}`,
+        }),
+      ),
+    ),
+  )
 }

+ 15 - 0
packages/opencode/test/server/httpapi-public-openapi.test.ts

@@ -142,4 +142,19 @@ describe("PublicApi OpenAPI v2 errors", () => {
       )
     }
   })
+
+  test("documents session busy errors", () => {
+    const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
+
+    for (const route of [
+      ["post", "/session/{sessionID}/shell"],
+      ["post", "/session/{sessionID}/revert"],
+      ["post", "/session/{sessionID}/unrevert"],
+      ["delete", "/session/{sessionID}/message/{messageID}"],
+    ] as const) {
+      expect(componentName(responseRef(spec.paths[route[1]]?.[route[0]]?.responses?.["409"]) ?? "")).toBe(
+        "SessionBusyError",
+      )
+    }
+  })
 })

+ 18 - 1
packages/opencode/test/server/httpapi-session.test.ts

@@ -1,7 +1,7 @@
 import { afterEach, describe, expect } from "bun:test"
 import { mkdir } from "node:fs/promises"
 import path from "node:path"
-import { Effect, Layer } from "effect"
+import { Cause, Effect, Exit, Layer } from "effect"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { registerAdapter } from "../../src/control-plane/adapters"
 import type { WorkspaceAdapter } from "../../src/control-plane/types"
@@ -13,6 +13,7 @@ import { InstanceBootstrap as InstanceBootstrapService } from "../../src/project
 import { InstanceStore } from "../../src/project/instance-store"
 import { Project } from "../../src/project/project"
 import { Server } from "../../src/server/server"
+import * as HttpSessionError from "../../src/server/routes/instance/httpapi/handlers/session-errors"
 import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
 import { Session } from "@/session/session"
 import { MessageID, PartID, SessionID, type SessionID as SessionIDType } from "../../src/session/schema"
@@ -215,6 +216,22 @@ afterEach(async () => {
 })
 
 describe("session HttpApi", () => {
+  it.effect("maps busy sessions to public session busy errors", () =>
+    Effect.gen(function* () {
+      const sessionID = SessionID.descending()
+      const exit = yield* HttpSessionError.mapBusy(Effect.fail(new Session.BusyError({ sessionID }))).pipe(Effect.exit)
+
+      expect(Exit.isFailure(exit)).toBe(true)
+      if (Exit.isFailure(exit)) {
+        expect(Cause.squash(exit.cause)).toMatchObject({
+          _tag: "SessionBusyError",
+          sessionID,
+          message: `Session is busy: ${sessionID}`,
+        })
+      }
+    }),
+  )
+
   it.instance(
     "returns declared not found errors for read routes",
     () =>