refactor(flags): route control-plane workspaces through runtime flags (#27337)
This commit is contained in:
@@ -10,8 +10,8 @@ import { GlobalBus } from "@/bus/global"
|
|||||||
import { Auth } from "@/auth"
|
import { Auth } from "@/auth"
|
||||||
import { SyncEvent } from "@/sync"
|
import { SyncEvent } from "@/sync"
|
||||||
import { EventSequenceTable, EventTable } from "@/sync/event.sql"
|
import { EventSequenceTable, EventTable } from "@/sync/event.sql"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { ProjectID } from "@/project/schema"
|
import { ProjectID } from "@/project/schema"
|
||||||
import { Slug } from "@opencode-ai/core/util/slug"
|
import { Slug } from "@opencode-ai/core/util/slug"
|
||||||
@@ -175,6 +175,7 @@ export const layer = Layer.effect(
|
|||||||
const http = yield* HttpClient.HttpClient
|
const http = yield* HttpClient.HttpClient
|
||||||
const sync = yield* SyncEvent.Service
|
const sync = yield* SyncEvent.Service
|
||||||
const vcs = yield* Vcs.Service
|
const vcs = yield* Vcs.Service
|
||||||
|
const flags = yield* RuntimeFlags.Service
|
||||||
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
const connections = new Map<WorkspaceID, ConnectionStatus>()
|
||||||
const syncFibers = yield* FiberMap.make<WorkspaceID, void, SyncLoopError>()
|
const syncFibers = yield* FiberMap.make<WorkspaceID, void, SyncLoopError>()
|
||||||
|
|
||||||
@@ -482,7 +483,7 @@ export const layer = Layer.effect(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
|
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
|
||||||
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
|
if (!flags.experimentalWorkspaces) return
|
||||||
|
|
||||||
const adapter = getAdapter(space.projectID, space.type)
|
const adapter = getAdapter(space.projectID, space.type)
|
||||||
const target = yield* EffectBridge.fromPromise(() => adapter.target(space)).pipe(
|
const target = yield* EffectBridge.fromPromise(() => adapter.target(space)).pipe(
|
||||||
@@ -1040,6 +1041,7 @@ export const defaultLayer = layer.pipe(
|
|||||||
Layer.provide(Project.defaultLayer),
|
Layer.provide(Project.defaultLayer),
|
||||||
Layer.provide(Vcs.defaultLayer),
|
Layer.provide(Vcs.defaultLayer),
|
||||||
Layer.provide(FetchHttpClient.layer),
|
Layer.provide(FetchHttpClient.layer),
|
||||||
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
const TIMEOUT = 5000
|
const TIMEOUT = 5000
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import path from "node:path"
|
|||||||
import { setTimeout as delay } from "node:timers/promises"
|
import { setTimeout as delay } from "node:timers/promises"
|
||||||
import { NodeHttpServer } from "@effect/platform-node"
|
import { NodeHttpServer } from "@effect/platform-node"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Layer, Schema } from "effect"
|
||||||
import { HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import * as Log from "@opencode-ai/core/util/log"
|
import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
@@ -33,24 +33,43 @@ import * as Workspace from "../../src/control-plane/workspace"
|
|||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { InstanceStore } from "@/project/instance-store"
|
import { InstanceStore } from "@/project/instance-store"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
|
import { Auth } from "@/auth"
|
||||||
|
import { SessionPrompt } from "@/session/prompt"
|
||||||
|
import { Project } from "@/project/project"
|
||||||
|
import { Vcs } from "@/project/vcs"
|
||||||
|
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||||
|
|
||||||
void Log.init({ print: false })
|
void Log.init({ print: false })
|
||||||
|
|
||||||
const testServerLayer = Layer.mergeAll(
|
|
||||||
NodeHttpServer.layer(Http.createServer, { host: "127.0.0.1", port: 0 }),
|
|
||||||
Workspace.defaultLayer.pipe(Layer.provide(InstanceStore.defaultLayer), Layer.provide(InstanceBootstrap.defaultLayer)),
|
|
||||||
SessionNs.defaultLayer,
|
|
||||||
)
|
|
||||||
const it = testEffect(testServerLayer)
|
|
||||||
|
|
||||||
const originalWorkspacesFlag = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
const originalWorkspacesFlag = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||||
const originalEnv = {
|
const originalEnv = {
|
||||||
OPENCODE_AUTH_CONTENT: process.env.OPENCODE_AUTH_CONTENT,
|
OPENCODE_AUTH_CONTENT: process.env.OPENCODE_AUTH_CONTENT,
|
||||||
|
OPENCODE_EXPERIMENTAL_WORKSPACES: process.env.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
||||||
OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS,
|
OTEL_EXPORTER_OTLP_HEADERS: process.env.OTEL_EXPORTER_OTLP_HEADERS,
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||||
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workspaceLayer = (experimentalWorkspaces: boolean) =>
|
||||||
|
Workspace.layer.pipe(
|
||||||
|
Layer.provide(Auth.defaultLayer),
|
||||||
|
Layer.provide(SessionNs.defaultLayer),
|
||||||
|
Layer.provide(SyncEvent.defaultLayer),
|
||||||
|
Layer.provide(SessionPrompt.defaultLayer),
|
||||||
|
Layer.provide(Project.defaultLayer),
|
||||||
|
Layer.provide(Vcs.defaultLayer),
|
||||||
|
Layer.provide(FetchHttpClient.layer),
|
||||||
|
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces })),
|
||||||
|
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer))),
|
||||||
|
)
|
||||||
|
|
||||||
|
const testServerLayer = Layer.mergeAll(
|
||||||
|
NodeHttpServer.layer(Http.createServer, { host: "127.0.0.1", port: 0 }),
|
||||||
|
workspaceLayer(true),
|
||||||
|
SessionNs.defaultLayer,
|
||||||
|
)
|
||||||
|
const it = testEffect(testServerLayer)
|
||||||
|
|
||||||
type RecordedCreate = {
|
type RecordedCreate = {
|
||||||
info: WorkspaceInfo
|
info: WorkspaceInfo
|
||||||
env: Record<string, string | undefined>
|
env: Record<string, string | undefined>
|
||||||
@@ -94,6 +113,7 @@ beforeEach(() => {
|
|||||||
Database.close()
|
Database.close()
|
||||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||||
restoreEnv()
|
restoreEnv()
|
||||||
|
process.env.OPENCODE_EXPERIMENTAL_WORKSPACES = "true"
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -141,6 +161,12 @@ const isWorkspaceSyncing = (id: WorkspaceID) =>
|
|||||||
const startWorkspaceSyncing = (projectID: ProjectID) => {
|
const startWorkspaceSyncing = (projectID: ProjectID) => {
|
||||||
void runWorkspace(Workspace.Service.use((workspace) => workspace.startWorkspaceSyncing(projectID)))
|
void runWorkspace(Workspace.Service.use((workspace) => workspace.startWorkspaceSyncing(projectID)))
|
||||||
}
|
}
|
||||||
|
const startWorkspaceSyncingWithFlag = (projectID: ProjectID, experimentalWorkspaces: boolean) =>
|
||||||
|
Effect.runPromise(
|
||||||
|
Workspace.Service.use((workspace) => workspace.startWorkspaceSyncing(projectID)).pipe(
|
||||||
|
Effect.provide(workspaceLayer(experimentalWorkspaces)),
|
||||||
|
),
|
||||||
|
)
|
||||||
const waitForWorkspaceSync = (workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) =>
|
const waitForWorkspaceSync = (workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) =>
|
||||||
runWorkspace(Workspace.Service.use((workspace) => workspace.waitForSync(workspaceID, state, signal)))
|
runWorkspace(Workspace.Service.use((workspace) => workspace.waitForSync(workspaceID, state, signal)))
|
||||||
|
|
||||||
@@ -980,7 +1006,6 @@ describe("workspace CRUD", () => {
|
|||||||
describe("workspace sync state", () => {
|
describe("workspace sync state", () => {
|
||||||
test("startWorkspaceSyncing is disabled by the experimental workspace flag", async () => {
|
test("startWorkspaceSyncing is disabled by the experimental workspace flag", async () => {
|
||||||
await withInstance(async (dir) => {
|
await withInstance(async (dir) => {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = false
|
|
||||||
const type = unique("flag-disabled")
|
const type = unique("flag-disabled")
|
||||||
const info = workspaceInfo(Instance.project.id, type)
|
const info = workspaceInfo(Instance.project.id, type)
|
||||||
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
|
||||||
@@ -988,7 +1013,7 @@ describe("workspace sync state", () => {
|
|||||||
insertWorkspace(info)
|
insertWorkspace(info)
|
||||||
registerAdapter(Instance.project.id, type, localAdapter(path.join(dir, "flag-disabled")).adapter)
|
registerAdapter(Instance.project.id, type, localAdapter(path.join(dir, "flag-disabled")).adapter)
|
||||||
|
|
||||||
startWorkspaceSyncing(Instance.project.id)
|
await startWorkspaceSyncingWithFlag(Instance.project.id, false)
|
||||||
await delay(25)
|
await delay(25)
|
||||||
|
|
||||||
expect((await workspaceStatus()).find((item) => item.workspaceID === info.id)?.status).toBeUndefined()
|
expect((await workspaceStatus()).find((item) => item.workspaceID === info.id)?.status).toBeUndefined()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { afterAll, afterEach, describe, expect } from "bun:test"
|
import { afterAll, afterEach, describe, expect } from "bun:test"
|
||||||
import { Effect, Layer, Option } from "effect"
|
import { Effect, Layer, Option } from "effect"
|
||||||
|
import { FetchHttpClient } from "effect/unstable/http"
|
||||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||||
@@ -17,6 +18,11 @@ import { Plugin } from "../../src/plugin/index"
|
|||||||
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
import { InstanceBootstrap } from "../../src/project/bootstrap-service"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { InstanceStore } from "../../src/project/instance-store"
|
import { InstanceStore } from "../../src/project/instance-store"
|
||||||
|
import { Project } from "../../src/project/project"
|
||||||
|
import { Vcs } from "../../src/project/vcs"
|
||||||
|
import { Session } from "../../src/session/session"
|
||||||
|
import { SessionPrompt } from "../../src/session/prompt"
|
||||||
|
import { SyncEvent } from "../../src/sync"
|
||||||
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { NpmTest } from "../fake/npm"
|
import { NpmTest } from "../fake/npm"
|
||||||
@@ -42,8 +48,16 @@ const pluginLayer = Plugin.layer.pipe(
|
|||||||
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
|
Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
|
||||||
)
|
)
|
||||||
const noopBootstrapLayer = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
const noopBootstrapLayer = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
||||||
const workspaceLayer = Workspace.defaultLayer.pipe(
|
const workspaceLayer = Workspace.layer.pipe(
|
||||||
|
Layer.provide(Auth.defaultLayer),
|
||||||
|
Layer.provide(Session.defaultLayer),
|
||||||
|
Layer.provide(SyncEvent.defaultLayer),
|
||||||
|
Layer.provide(SessionPrompt.defaultLayer),
|
||||||
|
Layer.provide(Project.defaultLayer),
|
||||||
|
Layer.provide(Vcs.defaultLayer),
|
||||||
|
Layer.provide(FetchHttpClient.layer),
|
||||||
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))),
|
Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))),
|
||||||
|
Layer.provide(RuntimeFlags.layer({ experimentalWorkspaces: true })),
|
||||||
)
|
)
|
||||||
const it = testEffect(Layer.mergeAll(pluginLayer, workspaceLayer, CrossSpawnSpawner.defaultLayer))
|
const it = testEffect(Layer.mergeAll(pluginLayer, workspaceLayer, CrossSpawnSpawner.defaultLayer))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user