From dcb8ed8eb0a891eea3ec3ba76bd13eefb3b97f30 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 8 May 2026 23:55:47 -0400 Subject: [PATCH] test(server): cover workspace sync fence protocol (#26441) --- .../test/server/httpapi-instance.test.ts | 26 ++++++++ .../server/httpapi-workspace-routing.test.ts | 61 ++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 7a181aac65..da8f8fb56d 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/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 }) diff --git a/packages/opencode/test/server/httpapi-workspace-routing.test.ts b/packages/opencode/test/server/httpapi-workspace-routing.test.ts index 379b71a91e..a62ca1db74 100644 --- a/packages/opencode/test/server/httpapi-workspace-routing.test.ts +++ b/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 } | 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 })