diff --git a/packages/opencode/test/fixture/flag.ts b/packages/opencode/test/fixture/flag.ts new file mode 100644 index 0000000000..224c5ef1f4 --- /dev/null +++ b/packages/opencode/test/fixture/flag.ts @@ -0,0 +1,20 @@ +import type { WorkspaceID } from "@/control-plane/schema" +import { Flag } from "@opencode-ai/core/flag/flag" +import { Effect, Scope } from "effect" + +/** + * Scoped override for `Flag.OPENCODE_WORKSPACE_ID`. Saves the previous value + * on entry and restores it via finalizer when the surrounding scope closes — + * preserves the original try/finally semantics regardless of test outcome. + */ +export function withFixedWorkspaceID(id: WorkspaceID): Effect.Effect { + return Effect.gen(function* () { + const previous = Flag.OPENCODE_WORKSPACE_ID + Flag.OPENCODE_WORKSPACE_ID = id + yield* Effect.addFinalizer(() => + Effect.sync(() => { + Flag.OPENCODE_WORKSPACE_ID = previous + }), + ) + }) +} diff --git a/packages/opencode/test/server/httpapi-instance-context.test.ts b/packages/opencode/test/server/httpapi-instance-context.test.ts index 0e5c752371..1b72f34775 100644 --- a/packages/opencode/test/server/httpapi-instance-context.test.ts +++ b/packages/opencode/test/server/httpapi-instance-context.test.ts @@ -21,6 +21,7 @@ import { instanceRouterMiddleware } from "../../src/server/routes/instance/httpa import { workspaceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/workspace-routing" import { resetDatabase } from "../fixture/db" import { disposeAllInstances, tmpdirScoped } from "../fixture/fixture" +import { withFixedWorkspaceID } from "../fixture/flag" import { waitGlobalBusEvent } from "./global-bus" import { testEffect } from "../lib/effect" @@ -204,16 +205,10 @@ describe("HttpApi instance context middleware", () => { }), ) - it.live("uses configured workspace id instead of routing to requested workspaces", () => + it.live("uses configured workspace id instead of routing to the requested workspace", () => Effect.gen(function* () { - const originalWorkspaceID = Flag.OPENCODE_WORKSPACE_ID const fixedWorkspaceID = WorkspaceID.ascending() - Flag.OPENCODE_WORKSPACE_ID = fixedWorkspaceID - yield* Effect.addFinalizer(() => - Effect.sync(() => { - Flag.OPENCODE_WORKSPACE_ID = originalWorkspaceID - }), - ) + yield* withFixedWorkspaceID(fixedWorkspaceID) const dir = yield* tmpdirScoped({ git: true }) const project = yield* Project.use.fromDirectory(dir) @@ -238,6 +233,66 @@ describe("HttpApi instance context middleware", () => { }), ) + it.live("falls through to local instead of MissingWorkspace when configured workspace id is set", () => + Effect.gen(function* () { + const fixedWorkspaceID = WorkspaceID.ascending() + yield* withFixedWorkspaceID(fixedWorkspaceID) + + const dir = yield* tmpdirScoped({ git: true }) + yield* Project.use.fromDirectory(dir) + yield* serveProbe() + + // Reference a workspace id that is not registered locally. Without the + // configured env override, this would short-circuit to a 500 + // MissingWorkspace response. With the env set, planRequest must skip the + // MissingWorkspace branch and fall through to Local with the configured + // workspace id. + const unknownWorkspaceID = WorkspaceID.ascending() + const response = yield* HttpClientRequest.get(`/probe?workspace=${unknownWorkspaceID}`).pipe( + HttpClientRequest.setHeader("x-opencode-directory", dir), + HttpClient.execute, + ) + + expect(response.status).toBe(200) + expect(yield* response.json).toMatchObject({ + directory: dir, + workspaceID: fixedWorkspaceID, + }) + }), + ) + + it.live("keeps configured workspace id on control-plane routes without remote routing", () => + Effect.gen(function* () { + const fixedWorkspaceID = WorkspaceID.ascending() + yield* withFixedWorkspaceID(fixedWorkspaceID) + + const dir = yield* tmpdirScoped({ git: true }) + const project = yield* Project.use.fromDirectory(dir) + const workspaceDir = path.join(dir, ".workspace-local") + const workspace = yield* createLocalWorkspace({ + projectID: project.project.id, + type: "instance-context-fixed-workspace-control-plane", + directory: workspaceDir, + }) + // /session is matched by isLocalWorkspaceRoute, so shouldStayOnControlPlane + // is true. Combined with the env override, the route must stay Local with + // the configured workspace id (not divert to the requested workspace's + // local directory). + yield* serveProbe("/session") + + const response = yield* HttpClientRequest.get(`/session?workspace=${workspace.id}`).pipe( + HttpClientRequest.setHeader("x-opencode-directory", dir), + HttpClient.execute, + ) + + expect(response.status).toBe(200) + expect(yield* response.json).toMatchObject({ + directory: dir, + workspaceID: fixedWorkspaceID, + }) + }), + ) + it.live("preserves selected workspace id on instance disposal events", () => Effect.gen(function* () { const dir = yield* tmpdirScoped({ git: true })