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

fix(httpapi): align sync seq validation

Reject negative and fractional sync sequence values in Effect HttpApi schemas so replay/history validation matches the legacy Hono routes.
Kit Langton 4 месяцев назад
Родитель
Сommit
2a4f2bf527

+ 6 - 3
packages/opencode/src/server/routes/instance/httpapi/sync.ts

@@ -9,15 +9,16 @@ import { not } from "drizzle-orm"
 import { or } from "drizzle-orm"
 import { SyncEvent } from "@/sync"
 import { EventTable } from "@/sync/event.sql"
+import { NonNegativeInt } from "@/util/schema"
 import { Effect, Layer, Schema } from "effect"
-import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 import { Authorization } from "./auth"
 
 const root = "/sync"
 const ReplayEvent = Schema.Struct({
   id: Schema.String,
   aggregateID: Schema.String,
-  seq: Schema.Number,
+  seq: NonNegativeInt,
   type: Schema.String,
   data: Schema.Record(Schema.String, Schema.Unknown),
 }).annotate({ identifier: "SyncReplayEvent" })
@@ -28,7 +29,7 @@ const ReplayPayload = Schema.Struct({
 const ReplayResponse = Schema.Struct({
   sessionID: Schema.String,
 }).annotate({ identifier: "SyncReplayResponse" })
-const HistoryPayload = Schema.Record(Schema.String, Schema.Number)
+const HistoryPayload = Schema.Record(Schema.String, NonNegativeInt)
 const HistoryEvent = Schema.Struct({
   id: Schema.String,
   aggregate_id: Schema.String,
@@ -59,6 +60,7 @@ export const SyncApi = HttpApi.make("sync")
         HttpApiEndpoint.post("replay", SyncPaths.replay, {
           payload: ReplayPayload,
           success: ReplayResponse,
+          error: HttpApiError.BadRequest,
         }).annotateMerge(
           OpenApi.annotations({
             identifier: "sync.replay",
@@ -69,6 +71,7 @@ export const SyncApi = HttpApi.make("sync")
         HttpApiEndpoint.post("history", SyncPaths.history, {
           payload: HistoryPayload,
           success: Schema.Array(HistoryEvent),
+          error: HttpApiError.BadRequest,
         }).annotateMerge(
           OpenApi.annotations({
             identifier: "sync.history.list",

+ 46 - 2
packages/opencode/test/server/httpapi-sync.test.ts

@@ -16,8 +16,8 @@ const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
 const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
 const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket
 
-function app() {
-  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
+function app(httpapi = true) {
+  Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = httpapi
   return InstanceRoutes(websocket)
 }
 
@@ -81,4 +81,48 @@ describe("sync HttpApi", () => {
     expect(replayed.status).toBe(200)
     expect(await replayed.json()).toEqual({ sessionID: session.id })
   })
+
+  test("matches legacy seq validation", async () => {
+    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
+    const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" }
+    const cases = [
+      {
+        path: SyncPaths.history,
+        body: { aggregate: -1 },
+      },
+      {
+        path: SyncPaths.history,
+        body: { aggregate: 1.5 },
+      },
+      {
+        path: SyncPaths.replay,
+        body: {
+          directory: tmp.path,
+          events: [{ id: "event", aggregateID: "session", seq: -1, type: "session.created", data: {} }],
+        },
+      },
+      {
+        path: SyncPaths.replay,
+        body: {
+          directory: tmp.path,
+          events: [{ id: "event", aggregateID: "session", seq: 1.5, type: "session.created", data: {} }],
+        },
+      },
+    ]
+
+    for (const item of cases) {
+      const legacy = await app(false).request(item.path, {
+        method: "POST",
+        headers,
+        body: JSON.stringify(item.body),
+      })
+      const httpapi = await app(true).request(item.path, {
+        method: "POST",
+        headers,
+        body: JSON.stringify(item.body),
+      })
+      expect(httpapi.status).toBe(legacy.status)
+      expect(httpapi.status).toBe(400)
+    }
+  })
 })