diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index e7e65f8901..4a21e2e65e 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -10,8 +10,8 @@ import { GlobalBus } from "@/bus/global" import { Auth } from "@/auth" import { SyncEvent } from "@/sync" 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 { RuntimeFlags } from "@/effect/runtime-flags" import { Filesystem } from "@/util/filesystem" import { ProjectID } from "@/project/schema" import { Slug } from "@opencode-ai/core/util/slug" @@ -175,6 +175,7 @@ export const layer = Layer.effect( const http = yield* HttpClient.HttpClient const sync = yield* SyncEvent.Service const vcs = yield* Vcs.Service + const flags = yield* RuntimeFlags.Service const connections = new Map() const syncFibers = yield* FiberMap.make() @@ -482,7 +483,7 @@ export const layer = Layer.effect( }) 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 target = yield* EffectBridge.fromPromise(() => adapter.target(space)).pipe( @@ -1040,6 +1041,7 @@ export const defaultLayer = layer.pipe( Layer.provide(Project.defaultLayer), Layer.provide(Vcs.defaultLayer), Layer.provide(FetchHttpClient.layer), + Layer.provide(RuntimeFlags.defaultLayer), ) const TIMEOUT = 5000 diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index adac51fe52..01304e8050 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -6,7 +6,7 @@ import path from "node:path" import { setTimeout as delay } from "node:timers/promises" import { NodeHttpServer } from "@effect/platform-node" 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 * as Log from "@opencode-ai/core/util/log" 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 { InstanceStore } from "@/project/instance-store" 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 }) -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 originalEnv = { 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_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, 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 = { info: WorkspaceInfo env: Record @@ -94,6 +113,7 @@ beforeEach(() => { Database.close() Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true restoreEnv() + process.env.OPENCODE_EXPERIMENTAL_WORKSPACES = "true" }) afterEach(async () => { @@ -141,6 +161,12 @@ const isWorkspaceSyncing = (id: WorkspaceID) => const startWorkspaceSyncing = (projectID: 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, signal?: AbortSignal) => runWorkspace(Workspace.Service.use((workspace) => workspace.waitForSync(workspaceID, state, signal))) @@ -980,7 +1006,6 @@ describe("workspace CRUD", () => { describe("workspace sync state", () => { test("startWorkspaceSyncing is disabled by the experimental workspace flag", async () => { await withInstance(async (dir) => { - Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = false const type = unique("flag-disabled") const info = workspaceInfo(Instance.project.id, type) const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({}))) @@ -988,7 +1013,7 @@ describe("workspace sync state", () => { insertWorkspace(info) 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) expect((await workspaceStatus()).find((item) => item.workspaceID === info.id)?.status).toBeUndefined() diff --git a/packages/opencode/test/plugin/workspace-adapter.test.ts b/packages/opencode/test/plugin/workspace-adapter.test.ts index bef8604324..0cf603fa3b 100644 --- a/packages/opencode/test/plugin/workspace-adapter.test.ts +++ b/packages/opencode/test/plugin/workspace-adapter.test.ts @@ -1,5 +1,6 @@ import { afterAll, afterEach, describe, expect } from "bun:test" import { Effect, Layer, Option } from "effect" +import { FetchHttpClient } from "effect/unstable/http" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { AppFileSystem } from "@opencode-ai/core/filesystem" 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 { Instance } from "../../src/project/instance" 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 { testEffect } from "../lib/effect" import { NpmTest } from "../fake/npm" @@ -42,8 +48,16 @@ const pluginLayer = Plugin.layer.pipe( Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), ) 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(RuntimeFlags.layer({ experimentalWorkspaces: true })), ) const it = testEffect(Layer.mergeAll(pluginLayer, workspaceLayer, CrossSpawnSpawner.defaultLayer))