Prechádzať zdrojové kódy

test(server): cover workspace sync fence protocol (#26441)

Kit Langton 3 mesiacov pred
rodič
commit
dcb8ed8eb0

+ 26 - 0
packages/opencode/test/server/httpapi-instance.test.ts

@@ -5,6 +5,7 @@ import { Config, Effect, FileSystem, Layer, Path } from "effect"
 import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
 import * as Socket from "effect/unstable/socket/Socket"
 import { WorkspaceID } from "../../src/control-plane/schema"
+import { ControlPaths } from "../../src/server/routes/instance/httpapi/groups/control"
 import { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
 import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
 import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
@@ -95,6 +96,31 @@ describe("instance HttpApi", () => {
     }),
   )
 
+  it.live("does not emit sync fence headers for fixed-workspace reads or no-op mutations", () =>
+    Effect.gen(function* () {
+      const originalWorkspaceID = Flag.OPENCODE_WORKSPACE_ID
+      Flag.OPENCODE_WORKSPACE_ID = WorkspaceID.ascending()
+      yield* Effect.addFinalizer(() =>
+        Effect.sync(() => {
+          Flag.OPENCODE_WORKSPACE_ID = originalWorkspaceID
+        }),
+      )
+
+      const dir = yield* tmpdirScoped({ git: true })
+      const read = yield* HttpClientRequest.get(InstancePaths.path).pipe(directoryHeader(dir), HttpClient.execute)
+      const log = yield* HttpClientRequest.post(ControlPaths.log).pipe(
+        directoryHeader(dir),
+        HttpClientRequest.bodyJson({ service: "fence-test", level: "info", message: "noop" }),
+        Effect.flatMap(HttpClient.execute),
+      )
+
+      expect(read.status).toBe(200)
+      expect(read.headers[FenceHeader]).toBeUndefined()
+      expect(log.status).toBe(200)
+      expect(log.headers[FenceHeader]).toBeUndefined()
+    }),
+  )
+
   it.live("serves path and VCS read endpoints", () =>
     Effect.gen(function* () {
       const dir = yield* tmpdirScoped({ git: true })

+ 60 - 1
packages/opencode/test/server/httpapi-workspace-routing.test.ts

@@ -1,7 +1,7 @@
 import { NodeHttpServer, NodeServices } from "@effect/platform-node"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { describe, expect } from "bun:test"
-import { Context, Effect, Layer, Queue } from "effect"
+import { Context, Effect, Layer, Queue, Ref } from "effect"
 import {
   FetchHttpClient,
   HttpClient,
@@ -28,6 +28,7 @@ import {
   WorkspaceRouteContext,
   workspaceRouterMiddleware,
 } from "../../src/server/routes/instance/httpapi/middleware/workspace-routing"
+import { HEADER as FenceHeader } from "../../src/server/shared/fence"
 import { Database } from "../../src/storage/db"
 import { resetDatabase } from "../fixture/db"
 import { tmpdirScoped } from "../fixture/fixture"
@@ -289,6 +290,64 @@ describe("HttpApi workspace routing middleware", () => {
     }),
   )
 
+  it.live("waits for sync fence headers from remote workspace HTTP responses", () =>
+    Effect.gen(function* () {
+      const dir = yield* tmpdirScoped({ git: true })
+      const project = yield* Project.use.fromDirectory(dir)
+      const workspaceID = WorkspaceID.ascending()
+      const type = "remote-http-fence-target"
+      const waited = yield* Ref.make<{ workspaceID: WorkspaceID; state: Record<string, number> } | undefined>(undefined)
+
+      const remoteUrl = yield* startRemoteWorkspaceHttpServer(() =>
+        HttpServerResponse.json(
+          { proxied: true },
+          { status: 202, headers: { [FenceHeader]: JSON.stringify({ aggregate: 3 }) } },
+        ),
+      )
+      registerAdapter(project.project.id, type, remoteAdapter(path.join(dir, `.${type}`), `${remoteUrl}/base`))
+
+      const workspace = Workspace.Service.of({
+        create: () => Effect.die("unused"),
+        sessionWarp: () => Effect.die("unused"),
+        list: () => Effect.die("unused"),
+        syncList: () => Effect.die("unused"),
+        get: (id) =>
+          Effect.succeed(
+            id === workspaceID
+              ? {
+                  id: workspaceID,
+                  type,
+                  branch: null,
+                  name: "remote-http-fence-target",
+                  directory: null,
+                  extra: null,
+                  projectID: project.project.id,
+                  timeUsed: Date.now(),
+                }
+              : undefined,
+          ),
+        remove: () => Effect.die("unused"),
+        status: () => Effect.die("unused"),
+        isSyncing: () => Effect.succeed(true),
+        waitForSync: (id, state) => Ref.set(waited, { workspaceID: id, state }),
+        startWorkspaceSyncing: () => Effect.die("unused"),
+      })
+
+      yield* HttpRouter.add("PATCH", "/probe", HttpServerResponse.text("route called")).pipe(
+        Layer.provide(workspaceRoutingTestLayer),
+        Layer.provide(Layer.succeed(Workspace.Service, workspace)),
+        HttpRouter.serve,
+        Layer.build,
+      )
+
+      const response = yield* HttpClientRequest.patch(`/probe?workspace=${workspaceID}`).pipe(HttpClient.execute)
+
+      expect(response.status).toBe(202)
+      expect(yield* response.json).toEqual({ proxied: true })
+      expect(yield* Ref.get(waited)).toEqual({ workspaceID, state: { aggregate: 3 } })
+    }),
+  )
+
   it.live("returns 503 when a remote workspace is not actively syncing", () =>
     Effect.gen(function* () {
       const dir = yield* tmpdirScoped({ git: true })