test(server): cover workspace sync fence protocol (#26441)
This commit is contained in:
@@ -5,6 +5,7 @@ import { Config, Effect, FileSystem, Layer, Path } from "effect"
|
|||||||
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import * as Socket from "effect/unstable/socket/Socket"
|
import * as Socket from "effect/unstable/socket/Socket"
|
||||||
import { WorkspaceID } from "../../src/control-plane/schema"
|
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 { InstancePaths } from "../../src/server/routes/instance/httpapi/groups/instance"
|
||||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||||
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
|
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", () =>
|
it.live("serves path and VCS read endpoints", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = yield* tmpdirScoped({ git: true })
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
import { NodeHttpServer, NodeServices } from "@effect/platform-node"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Context, Effect, Layer, Queue } from "effect"
|
import { Context, Effect, Layer, Queue, Ref } from "effect"
|
||||||
import {
|
import {
|
||||||
FetchHttpClient,
|
FetchHttpClient,
|
||||||
HttpClient,
|
HttpClient,
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
WorkspaceRouteContext,
|
WorkspaceRouteContext,
|
||||||
workspaceRouterMiddleware,
|
workspaceRouterMiddleware,
|
||||||
} from "../../src/server/routes/instance/httpapi/middleware/workspace-routing"
|
} 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 { Database } from "../../src/storage/db"
|
||||||
import { resetDatabase } from "../fixture/db"
|
import { resetDatabase } from "../fixture/db"
|
||||||
import { tmpdirScoped } from "../fixture/fixture"
|
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", () =>
|
it.live("returns 503 when a remote workspace is not actively syncing", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const dir = yield* tmpdirScoped({ git: true })
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
|||||||
Reference in New Issue
Block a user