feat(core): rework workspace integration and adaptor interface (#15895)

This commit is contained in:
James Long
2026-03-03 21:35:38 -05:00
committed by GitHub
parent e79d41c70e
commit 7f37acdaaa
23 changed files with 1720 additions and 580 deletions

View File

@@ -5,8 +5,11 @@ import { tmpdir } from "../fixture/fixture"
import { Project } from "../../src/project/project"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { Instance } from "../../src/project/instance"
import { WorkspaceContext } from "../../src/control-plane/workspace-context"
import { Database } from "../../src/storage/db"
import { resetDatabase } from "../fixture/db"
import * as adaptors from "../../src/control-plane/adaptors"
import type { Adaptor } from "../../src/control-plane/types"
afterEach(async () => {
mock.restore()
@@ -18,18 +21,35 @@ type State = {
calls: Array<{ method: string; url: string; body?: string }>
}
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
async function setup(state: State) {
mock.module("../../src/control-plane/adaptors", () => ({
getAdaptor: () => ({
request: async (_config: unknown, method: string, url: string, data?: BodyInit) => {
const body = data ? await new Response(data).text() : undefined
state.calls.push({ method, url, body })
return new Response("proxied", { status: 202 })
},
}),
}))
const TestAdaptor: Adaptor = {
configure(config) {
return config
},
async create() {
throw new Error("not used")
},
async remove() {},
async fetch(_config: unknown, input: RequestInfo | URL, init?: RequestInit) {
const url =
input instanceof Request || input instanceof URL
? input.toString()
: new URL(input, "http://workspace.test").toString()
const request = new Request(url, init)
const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.text()
state.calls.push({
method: request.method,
url: `${new URL(request.url).pathname}${new URL(request.url).search}`,
body,
})
return new Response("proxied", { status: 202 })
},
}
adaptors.installAdaptor("testing", TestAdaptor)
await using tmp = await tmpdir({ git: true })
const { project } = await Project.fromDirectory(tmp.path)
@@ -45,20 +65,23 @@ async function setup(state: State) {
id: id1,
branch: "main",
project_id: project.id,
config: remote,
type: remote.type,
name: remote.name,
},
{
id: id2,
branch: "main",
project_id: project.id,
config: { type: "worktree", directory: tmp.path },
type: "worktree",
directory: tmp.path,
name: "local",
},
])
.run(),
)
const { SessionProxyMiddleware } = await import("../../src/control-plane/session-proxy-middleware")
const app = new Hono().use(SessionProxyMiddleware)
const { WorkspaceRouterMiddleware } = await import("../../src/control-plane/workspace-router-middleware")
const app = new Hono().use(WorkspaceRouterMiddleware)
return {
id1,
@@ -66,15 +89,19 @@ async function setup(state: State) {
app,
async request(input: RequestInfo | URL, init?: RequestInit) {
return Instance.provide({
directory: state.workspace === "first" ? id1 : id2,
fn: async () => app.request(input, init),
directory: tmp.path,
fn: async () =>
WorkspaceContext.provide({
workspaceID: state.workspace === "first" ? id1 : id2,
fn: () => app.request(input, init),
}),
})
},
}
}
describe("control-plane/session-proxy-middleware", () => {
test("forwards non-GET session requests for remote workspaces", async () => {
test("forwards non-GET session requests for workspaces", async () => {
const state: State = {
workspace: "first",
calls: [],
@@ -102,46 +129,21 @@ describe("control-plane/session-proxy-middleware", () => {
])
})
test("does not forward GET requests", async () => {
const state: State = {
workspace: "first",
calls: [],
}
// It will behave this way when we have syncing
//
// test("does not forward GET requests", async () => {
// const state: State = {
// workspace: "first",
// calls: [],
// }
const ctx = await setup(state)
// const ctx = await setup(state)
ctx.app.get("/session/foo", (c) => c.text("local", 200))
const response = await ctx.request("http://workspace.test/session/foo?x=1")
// ctx.app.get("/session/foo", (c) => c.text("local", 200))
// const response = await ctx.request("http://workspace.test/session/foo?x=1")
expect(response.status).toBe(200)
expect(await response.text()).toBe("local")
expect(state.calls).toEqual([])
})
test("does not forward GET or POST requests for worktree workspaces", async () => {
const state: State = {
workspace: "second",
calls: [],
}
const ctx = await setup(state)
ctx.app.get("/session/foo", (c) => c.text("local-get", 200))
ctx.app.post("/session/foo", (c) => c.text("local-post", 200))
const getResponse = await ctx.request("http://workspace.test/session/foo?x=1")
const postResponse = await ctx.request("http://workspace.test/session/foo?x=1", {
method: "POST",
body: JSON.stringify({ hello: "world" }),
headers: {
"content-type": "application/json",
},
})
expect(getResponse.status).toBe(200)
expect(await getResponse.text()).toBe("local-get")
expect(postResponse.status).toBe(200)
expect(await postResponse.text()).toBe("local-post")
expect(state.calls).toEqual([])
})
// expect(response.status).toBe(200)
// expect(await response.text()).toBe("local")
// expect(state.calls).toEqual([])
// })
})

View File

@@ -4,6 +4,7 @@ import { WorkspaceServer } from "../../src/control-plane/workspace-server/server
import { parseSSE } from "../../src/control-plane/sse"
import { GlobalBus } from "../../src/bus/global"
import { resetDatabase } from "../fixture/db"
import { tmpdir } from "../fixture/fixture"
afterEach(async () => {
await resetDatabase()
@@ -13,13 +14,17 @@ Log.init({ print: false })
describe("control-plane/workspace-server SSE", () => {
test("streams GlobalBus events and parseSSE reads them", async () => {
await using tmp = await tmpdir({ git: true })
const app = WorkspaceServer.App()
const stop = new AbortController()
const seen: unknown[] = []
try {
const response = await app.request("/event", {
signal: stop.signal,
headers: {
"x-opencode-workspace": "wrk_test_workspace",
"x-opencode-directory": tmp.path,
},
})
expect(response.status).toBe(200)

View File

@@ -7,6 +7,8 @@ import { Database } from "../../src/storage/db"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { GlobalBus } from "../../src/bus/global"
import { resetDatabase } from "../fixture/db"
import * as adaptors from "../../src/control-plane/adaptors"
import type { Adaptor } from "../../src/control-plane/types"
afterEach(async () => {
mock.restore()
@@ -15,35 +17,34 @@ afterEach(async () => {
Log.init({ print: false })
const seen: string[] = []
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert.config
const remote = { type: "testing", name: "remote-a" } as unknown as typeof WorkspaceTable.$inferInsert
mock.module("../../src/control-plane/adaptors", () => ({
getAdaptor: (config: { type: string }) => {
seen.push(config.type)
return {
async create() {
throw new Error("not used")
},
async remove() {},
async request() {
const body = new ReadableStream<Uint8Array>({
start(controller) {
const encoder = new TextEncoder()
controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
controller.close()
},
})
return new Response(body, {
status: 200,
headers: {
"content-type": "text/event-stream",
},
})
},
}
const TestAdaptor: Adaptor = {
configure(config) {
return config
},
}))
async create() {
throw new Error("not used")
},
async remove() {},
async fetch(_config: unknown, _input: RequestInfo | URL, _init?: RequestInit) {
const body = new ReadableStream<Uint8Array>({
start(controller) {
const encoder = new TextEncoder()
controller.enqueue(encoder.encode('data: {"type":"remote.ready","properties":{}}\n\n'))
controller.close()
},
})
return new Response(body, {
status: 200,
headers: {
"content-type": "text/event-stream",
},
})
},
}
adaptors.installAdaptor("testing", TestAdaptor)
describe("control-plane/workspace.startSyncing", () => {
test("syncs only remote workspaces and emits remote SSE events", async () => {
@@ -62,13 +63,16 @@ describe("control-plane/workspace.startSyncing", () => {
id: id1,
branch: "main",
project_id: project.id,
config: remote,
type: remote.type,
name: remote.name,
},
{
id: id2,
branch: "main",
project_id: project.id,
config: { type: "worktree", directory: tmp.path },
type: "worktree",
directory: tmp.path,
name: "local",
},
])
.run(),
@@ -91,7 +95,5 @@ describe("control-plane/workspace.startSyncing", () => {
])
await sync.stop()
expect(seen).toContain("testing")
expect(seen).not.toContain("worktree")
})
})