From 39ea816a72e17185bb9437a8c93fd590a15cc7d8 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 20 May 2026 23:36:27 -0400 Subject: [PATCH] refactor(opencode): roll out serviceUse proxy across 14 services + tests (#28576) --- packages/opencode/src/account/account.ts | 3 + packages/opencode/src/account/repo.ts | 3 + packages/opencode/src/agent/agent.ts | 3 + packages/opencode/src/config/config.ts | 3 + packages/opencode/src/env/index.ts | 3 + packages/opencode/src/file/index.ts | 3 + packages/opencode/src/file/ripgrep.ts | 3 + packages/opencode/src/format/index.ts | 3 + packages/opencode/src/installation/index.ts | 3 + packages/opencode/src/mcp/auth.ts | 3 + packages/opencode/src/mcp/index.ts | 3 + .../opencode/src/project/instance-store.ts | 3 + packages/opencode/src/provider/auth.ts | 3 + packages/opencode/src/share/share-next.ts | 3 + packages/opencode/test/account/repo.test.ts | 40 +- .../opencode/test/account/service.test.ts | 18 +- .../agent/plan-mode-subagent-bypass.test.ts | 12 +- .../agent/plugin-agent-regression.test.ts | 2 +- .../opencode/test/config/agent-color.test.ts | 6 +- packages/opencode/test/config/config.test.ts | 86 +- packages/opencode/test/config/tui.test.ts | 2 +- .../test/control-plane/workspace.test.ts | 914 +++++++++--------- packages/opencode/test/file/fsmonitor.test.ts | 4 +- .../opencode/test/file/path-traversal.test.ts | 4 +- packages/opencode/test/file/ripgrep.test.ts | 12 +- packages/opencode/test/format/format.test.ts | 6 +- .../test/installation/installation.test.ts | 18 +- .../test/mcp/oauth-auto-connect.test.ts | 6 +- .../opencode/test/permission-task.test.ts | 2 +- .../test/plugin/auth-override.test.ts | 4 +- .../test/provider/amazon-bedrock.test.ts | 4 +- .../opencode/test/provider/provider.test.ts | 14 +- .../test/server/global-session-list.test.ts | 6 +- .../test/server/httpapi-experimental.test.ts | 2 +- .../server/httpapi-instance-context.test.ts | 2 +- .../test/server/httpapi-session.test.ts | 4 +- .../opencode/test/server/httpapi-sync.test.ts | 2 +- .../server/httpapi-workspace-routing.test.ts | 2 +- .../test/server/httpapi-workspace.test.ts | 4 +- .../test/server/project-init-git.test.ts | 2 +- .../test/server/session-actions.test.ts | 4 +- .../server/session-diff-missing-patch.test.ts | 4 +- .../opencode/test/server/session-list.test.ts | 10 +- .../test/server/session-messages.test.ts | 4 +- .../test/server/session-select.test.ts | 2 +- .../opencode/test/session/compaction.test.ts | 2 +- .../opencode/test/session/session.test.ts | 2 +- .../opencode/test/share/share-next.test.ts | 14 +- 48 files changed, 640 insertions(+), 622 deletions(-) diff --git a/packages/opencode/src/account/account.ts b/packages/opencode/src/account/account.ts index a0aed88cba..dcff82664b 100644 --- a/packages/opencode/src/account/account.ts +++ b/packages/opencode/src/account/account.ts @@ -1,4 +1,5 @@ import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect" +import { serviceUse } from "@/effect/service-use" import { FetchHttpClient, HttpClient, @@ -181,6 +182,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Account") {} +export const use = serviceUse(Service) + export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/account/repo.ts b/packages/opencode/src/account/repo.ts index 04380137c8..a5291e8283 100644 --- a/packages/opencode/src/account/repo.ts +++ b/packages/opencode/src/account/repo.ts @@ -1,4 +1,5 @@ import { eq } from "drizzle-orm" +import { serviceUse } from "@/effect/service-use" import { Effect, Layer, Option, Schema, Context } from "effect" import { Database } from "@/storage/db" @@ -38,6 +39,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/AccountRepo") {} +export const use = serviceUse(Service) + export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index ce6cf30b6d..e78304e4e7 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,4 +1,5 @@ import { Config } from "@/config/config" +import { serviceUse } from "@/effect/service-use" import { Provider } from "@/provider/provider" import { ModelID, ProviderID } from "../provider/schema" import { generateObject, streamObject, type ModelMessage } from "ai" @@ -76,6 +77,8 @@ type State = Omit export class Service extends Context.Service()("@opencode/Agent") {} +export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index cdc32b971d..e3931aa368 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1,4 +1,5 @@ import * as Log from "@opencode-ai/core/util/log" +import { serviceUse } from "@/effect/service-use" import path from "path" import { pathToFileURL } from "url" import os from "os" @@ -319,6 +320,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Config") {} +export const use = serviceUse(Service) + function globalConfigFile() { const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) => path.join(Global.Path.config, file), diff --git a/packages/opencode/src/env/index.ts b/packages/opencode/src/env/index.ts index e7af61422b..efdfd0ac01 100644 --- a/packages/opencode/src/env/index.ts +++ b/packages/opencode/src/env/index.ts @@ -1,4 +1,5 @@ import { Context, Effect, Layer } from "effect" +import { serviceUse } from "@/effect/service-use" import { InstanceState } from "@/effect/instance-state" type State = Record @@ -12,6 +13,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Env") {} +export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index b951a4d7a5..0f17ed2792 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,4 +1,5 @@ import { BusEvent } from "@/bus/bus-event" +import { serviceUse } from "@/effect/service-use" import { InstanceState } from "@/effect/instance-state" import { AppFileSystem } from "@opencode-ai/core/filesystem" @@ -326,6 +327,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/File") {} +export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index aae794c1a1..c1a636782b 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -1,4 +1,5 @@ import path from "path" +import { serviceUse } from "@/effect/service-use" import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Cause, Context, Effect, Fiber, Layer, Queue, Schema, Stream } from "effect" import type { PlatformError } from "effect/PlatformError" @@ -141,6 +142,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Ripgrep") {} +export const use = serviceUse(Service) + function env() { const env = sanitizedProcessEnv() delete env.RIPGREP_CONFIG_PATH diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 50abba0ff9..eb55b0713b 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,4 +1,5 @@ import { Effect, Layer, Context, Schema } from "effect" +import { serviceUse } from "@/effect/service-use" import { ChildProcess } from "effect/unstable/process" import { AppProcess } from "@opencode-ai/core/process" import { InstanceState } from "@/effect/instance-state" @@ -27,6 +28,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Format") {} +export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index 9b0e06c4af..079bc30ffa 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -1,4 +1,5 @@ import { Effect, Layer, Schema, Context, Stream } from "effect" +import { serviceUse } from "@/effect/service-use" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { withTransientReadRetry } from "@/util/effect-http-client" import { errorMessage } from "@/util/error" @@ -89,6 +90,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Installation") {} +export const use = serviceUse(Service) + export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index be19be0af0..d7406e5192 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -1,4 +1,5 @@ import path from "path" +import { serviceUse } from "@/effect/service-use" import { Global } from "@opencode-ai/core/global" import { Effect, Layer, Context, Option, Schema } from "effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" @@ -51,6 +52,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/McpAuth") {} +export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 832811b281..d6e8420d5a 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -1,4 +1,5 @@ import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai" +import { serviceUse } from "@/effect/service-use" import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" @@ -264,6 +265,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/MCP") {} +export const use = serviceUse(Service) + export const layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/project/instance-store.ts b/packages/opencode/src/project/instance-store.ts index faa56668a7..8c847fd993 100644 --- a/packages/opencode/src/project/instance-store.ts +++ b/packages/opencode/src/project/instance-store.ts @@ -1,4 +1,5 @@ import { GlobalBus } from "@/bus/global" +import { serviceUse } from "@/effect/service-use" import { WorkspaceContext } from "@/control-plane/workspace-context" import { InstanceRef } from "@/effect/instance-ref" import { disposeInstance as runDisposers } from "@/effect/instance-registry" @@ -24,6 +25,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/InstanceStore") {} +export const use = serviceUse(Service) + interface Entry { readonly deferred: Deferred.Deferred } diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 6f857ce522..214ab3bd99 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -1,4 +1,5 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin" +import { serviceUse } from "@/effect/service-use" import { Auth } from "@/auth" import { InstanceState } from "@/effect/instance-state" import { optionalOmitUndefined } from "@opencode-ai/core/schema" @@ -102,6 +103,8 @@ interface State { export class Service extends Context.Service()("@opencode/ProviderAuth") {} +export const use = serviceUse(Service) + export const layer: Layer.Layer = Layer.effect( Service, Effect.gen(function* () { diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index f5e0e654a9..98d5c82255 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -1,4 +1,5 @@ import type * as SDK from "@opencode-ai/sdk/v2" +import { serviceUse } from "@/effect/service-use" import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { Account } from "@/account/account" @@ -76,6 +77,8 @@ export interface Interface { export class Service extends Context.Service()("@opencode/ShareNext") {} +export const use = serviceUse(Service) + const db = (fn: (d: Parameters[0] extends (trx: infer D) => any ? D : never) => T) => Effect.sync(() => Database.use(fn)) diff --git a/packages/opencode/test/account/repo.test.ts b/packages/opencode/test/account/repo.test.ts index 9a1e72ae21..1376651543 100644 --- a/packages/opencode/test/account/repo.test.ts +++ b/packages/opencode/test/account/repo.test.ts @@ -18,14 +18,14 @@ const it = testEffect(Layer.merge(AccountRepo.layer, truncate)) it.live("list returns empty when no accounts exist", () => Effect.gen(function* () { - const accounts = yield* AccountRepo.Service.use((r) => r.list()) + const accounts = yield* AccountRepo.use.list() expect(accounts).toEqual([]) }), ) it.live("active returns none when no accounts exist", () => Effect.gen(function* () { - const active = yield* AccountRepo.Service.use((r) => r.active()) + const active = yield* AccountRepo.use.active() expect(Option.isNone(active)).toBe(true) }), ) @@ -45,13 +45,13 @@ it.live("persistAccount inserts and getRow retrieves", () => }), ) - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const row = yield* AccountRepo.use.getRow(id) expect(Option.isSome(row)).toBe(true) const value = Option.getOrThrow(row) expect(value.id).toBe(AccountID.make("user-1")) expect(value.email).toBe("test@example.com") - const active = yield* AccountRepo.Service.use((r) => r.active()) + const active = yield* AccountRepo.use.active() expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-1")) }), ) @@ -72,9 +72,9 @@ it.live("persistAccount normalizes trailing slashes in stored server URLs", () = }), ) - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) - const active = yield* AccountRepo.Service.use((r) => r.active()) - const list = yield* AccountRepo.Service.use((r) => r.list()) + const row = yield* AccountRepo.use.getRow(id) + const active = yield* AccountRepo.use.active() + const list = yield* AccountRepo.use.list() expect(Option.getOrThrow(row).url).toBe("https://control.example.com") expect(Option.getOrThrow(active).url).toBe("https://control.example.com") @@ -112,7 +112,7 @@ it.live("persistAccount sets the active account and org", () => ) // Last persisted account is active with its org - const active = yield* AccountRepo.Service.use((r) => r.active()) + const active = yield* AccountRepo.use.active() expect(Option.isSome(active)).toBe(true) expect(Option.getOrThrow(active).id).toBe(AccountID.make("user-2")) expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2")) @@ -148,7 +148,7 @@ it.live("list returns all accounts", () => }), ) - const accounts = yield* AccountRepo.Service.use((r) => r.list()) + const accounts = yield* AccountRepo.use.list() expect(accounts.length).toBe(2) expect(accounts.map((a) => a.email).sort()).toEqual(["a@example.com", "b@example.com"]) }), @@ -170,9 +170,9 @@ it.live("remove deletes an account", () => }), ) - yield* AccountRepo.Service.use((r) => r.remove(id)) + yield* AccountRepo.use.remove(id) - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const row = yield* AccountRepo.use.getRow(id) expect(Option.isNone(row)).toBe(true) }), ) @@ -207,12 +207,12 @@ it.live("use stores the selected org and marks the account active", () => ) yield* AccountRepo.Service.use((r) => r.use(id1, Option.some(OrgID.make("org-99")))) - const active1 = yield* AccountRepo.Service.use((r) => r.active()) + const active1 = yield* AccountRepo.use.active() expect(Option.getOrThrow(active1).id).toBe(id1) expect(Option.getOrThrow(active1).active_org_id).toBe(OrgID.make("org-99")) yield* AccountRepo.Service.use((r) => r.use(id1, Option.none())) - const active2 = yield* AccountRepo.Service.use((r) => r.active()) + const active2 = yield* AccountRepo.use.active() expect(Option.getOrThrow(active2).active_org_id).toBeNull() }), ) @@ -243,7 +243,7 @@ it.live("persistToken updates token fields", () => }), ) - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const row = yield* AccountRepo.use.getRow(id) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("new_token")) expect(value.refresh_token).toBe(RefreshToken.make("new_refresh")) @@ -276,7 +276,7 @@ it.live("persistToken with no expiry sets token_expiry to null", () => }), ) - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const row = yield* AccountRepo.use.getRow(id) expect(Option.getOrThrow(row).token_expiry).toBeNull() }), ) @@ -309,14 +309,14 @@ it.live("persistAccount upserts on conflict", () => }), ) - const accounts = yield* AccountRepo.Service.use((r) => r.list()) + const accounts = yield* AccountRepo.use.list() expect(accounts.length).toBe(1) - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const row = yield* AccountRepo.use.getRow(id) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("at_v2")) - const active = yield* AccountRepo.Service.use((r) => r.active()) + const active = yield* AccountRepo.use.active() expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2")) }), ) @@ -337,9 +337,9 @@ it.live("remove clears active state when deleting the active account", () => }), ) - yield* AccountRepo.Service.use((r) => r.remove(id)) + yield* AccountRepo.use.remove(id) - const active = yield* AccountRepo.Service.use((r) => r.active()) + const active = yield* AccountRepo.use.active() expect(Option.isNone(active)).toBe(true) }), ) diff --git a/packages/opencode/test/account/service.test.ts b/packages/opencode/test/account/service.test.ts index b7f28b85d1..c154561768 100644 --- a/packages/opencode/test/account/service.test.ts +++ b/packages/opencode/test/account/service.test.ts @@ -88,7 +88,7 @@ it.live("login normalizes trailing slashes in the provided server URL", () => }), ) - const result = yield* Account.Service.use((s) => s.login("https://one.example.com/")).pipe( + const result = yield* Account.use.login("https://one.example.com/").pipe( Effect.provide(live(client)), ) @@ -109,7 +109,7 @@ it.live("login maps transport failures to account transport errors", () => ) const error = yield* Effect.flip( - Account.Service.use((s) => s.login("https://one.example.com")).pipe(Effect.provide(live(client))), + Account.use.login("https://one.example.com").pipe(Effect.provide(live(client))), ) expect(error).toBeInstanceOf(AccountTransportError) @@ -163,7 +163,7 @@ it.live("orgsByAccount groups orgs per account", () => }), ) - const rows = yield* Account.Service.use((s) => s.orgsByAccount()).pipe(Effect.provide(live(client))) + const rows = yield* Account.use.orgsByAccount().pipe(Effect.provide(live(client))) expect(rows.map((row) => [row.account.id, row.orgs.map((org) => org.id)]).map(([id, orgs]) => [id, orgs])).toEqual([ [AccountID.make("user-1"), [OrgID.make("org-1")]], @@ -201,12 +201,12 @@ it.live("token refresh persists the new token", () => ), ) - const token = yield* Account.Service.use((s) => s.token(id)).pipe(Effect.provide(live(client))) + const token = yield* Account.use.token(id).pipe(Effect.provide(live(client))) expect(Option.getOrThrow(token)).toBeDefined() expect(String(Option.getOrThrow(token))).toBe("at_new") - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const row = yield* AccountRepo.use.getRow(id) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) @@ -246,12 +246,12 @@ it.live("token refreshes before expiry when inside the eager refresh window", () }), ) - const token = yield* Account.Service.use((s) => s.token(id)).pipe(Effect.provide(live(client))) + const token = yield* Account.use.token(id).pipe(Effect.provide(live(client))) expect(String(Option.getOrThrow(token))).toBe("at_new") expect(refreshCalls).toBe(1) - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const row = yield* AccountRepo.use.getRow(id) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) @@ -315,7 +315,7 @@ it.live("concurrent config and token requests coalesce token refresh", () => expect(String(Option.getOrThrow(token))).toBe("at_new") expect(refreshCalls).toBe(1) - const row = yield* AccountRepo.Service.use((r) => r.getRow(id)) + const row = yield* AccountRepo.use.getRow(id) const value = Option.getOrThrow(row) expect(value.access_token).toBe(AccessToken.make("at_new")) expect(value.refresh_token).toBe(RefreshToken.make("rt_new")) @@ -388,7 +388,7 @@ it.live("poll stores the account and first org on success", () => expect(res.email).toBe("user@example.com") } - const active = yield* AccountRepo.Service.use((r) => r.active()) + const active = yield* AccountRepo.use.active() expect(Option.getOrThrow(active)).toEqual( expect.objectContaining({ id: "user-1", diff --git a/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts b/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts index 641a929aeb..07fb9a64d5 100644 --- a/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts +++ b/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts @@ -46,8 +46,8 @@ function testAgent(input: { it.instance("[#26514] subagent spawned from plan mode inherits read-only restriction (edit denied)", () => Effect.gen(function* () { - const planAgent = yield* Agent.Service.use((svc) => svc.get("plan")) - const generalAgent = yield* Agent.Service.use((svc) => svc.get("general")) + const planAgent = yield* Agent.use.get("plan") + const generalAgent = yield* Agent.use.get("general") expect(planAgent).toBeDefined() expect(generalAgent).toBeDefined() @@ -83,8 +83,8 @@ it.instance("[#26514] explore subagent launched from plan mode also stays read-o // should propagate the parent **agent** permissions, not just deny edit // when the subagent happens to already deny it. Effect.gen(function* () { - const planAgent = yield* Agent.Service.use((svc) => svc.get("plan")) - const explore = yield* Agent.Service.use((svc) => svc.get("explore")) + const planAgent = yield* Agent.use.get("plan") + const explore = yield* Agent.use.get("explore") expect(planAgent).toBeDefined() expect(explore).toBeDefined() @@ -108,8 +108,8 @@ it.instance( // be able to edit when the parent agent is `plan`. () => Effect.gen(function* () { - const planAgent = yield* Agent.Service.use((svc) => svc.get("plan")) - const my = yield* Agent.Service.use((svc) => svc.get("my_subagent")) + const planAgent = yield* Agent.use.get("plan") + const my = yield* Agent.use.get("my_subagent") expect(planAgent).toBeDefined() expect(my).toBeDefined() diff --git a/packages/opencode/test/agent/plugin-agent-regression.test.ts b/packages/opencode/test/agent/plugin-agent-regression.test.ts index c437281cc6..02c73b4109 100644 --- a/packages/opencode/test/agent/plugin-agent-regression.test.ts +++ b/packages/opencode/test/agent/plugin-agent-regression.test.ts @@ -51,7 +51,7 @@ it.instance( () => Effect.gen(function* () { yield* Plugin.Service.use((p) => p.init()) - const agents = yield* Agent.Service.use((svc) => svc.list()) + const agents = yield* Agent.use.list() const added = agents.find((agent) => agent.name === PLUGIN_AGENT.name) expect(added?.description).toBe(PLUGIN_AGENT.description) expect(added?.mode).toBe(PLUGIN_AGENT.mode) diff --git a/packages/opencode/test/config/agent-color.test.ts b/packages/opencode/test/config/agent-color.test.ts index d198080591..664170696e 100644 --- a/packages/opencode/test/config/agent-color.test.ts +++ b/packages/opencode/test/config/agent-color.test.ts @@ -11,7 +11,7 @@ it.instance( "agent color parsed from project config", () => Effect.gen(function* () { - const cfg = yield* Config.Service.use((svc) => svc.get()) + const cfg = yield* Config.use.get() expect(cfg.agent?.["build"]?.color).toBe("#FFA500") expect(cfg.agent?.["plan"]?.color).toBe("primary") }), @@ -30,9 +30,9 @@ it.instance( "Agent.get includes color from config", () => Effect.gen(function* () { - const plan = yield* AgentSvc.Service.use((svc) => svc.get("plan")) + const plan = yield* AgentSvc.use.get("plan") expect(plan?.color).toBe("#A855F7") - const build = yield* AgentSvc.Service.use((svc) => svc.get("build")) + const build = yield* AgentSvc.use.get("build") expect(build?.color).toBe("accent") }), { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 12fb96dc67..4924eff250 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -70,14 +70,14 @@ const load = (ctx: InstanceContext) => ) const saveGlobal = (config: Config.Info) => Effect.runPromise( - Config.Service.use((svc) => svc.updateGlobal(config)).pipe( + Config.use.updateGlobal(config).pipe( Effect.map((result) => result.info), Effect.scoped, Effect.provide(layer), ), ) const clear = async (wait = false) => { - await Effect.runPromise(Config.Service.use((svc) => svc.invalidate()).pipe(Effect.scoped, Effect.provide(layer))) + await Effect.runPromise(Config.use.invalidate().pipe(Effect.scoped, Effect.provide(layer))) if (wait) await InstanceRuntime.disposeAllInstances() } const listDirs = (ctx: InstanceContext) => @@ -162,7 +162,7 @@ async function check(map: (dir: string) => string) { it.instance("loads config with defaults when no files exist", () => Effect.gen(function* () { - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.username).toBeDefined() }), ) @@ -218,7 +218,7 @@ test("does not create global config when OPENCODE_CONFIG_DIR is set", async () = it.instance( "loads JSON config file", Effect.gen(function* () { - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.model).toBe("test/model") expect(config.username).toBe("testuser") }), @@ -228,7 +228,7 @@ it.instance( it.instance( "loads shell config field", Effect.gen(function* () { - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.shell).toBe("bash") }), { config: { shell: "bash" } }, @@ -313,7 +313,7 @@ test("updates global config and omits empty shell key in jsonc", async () => { it.instance( "loads formatter boolean config", Effect.gen(function* () { - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.formatter).toBe(true) }), { config: { formatter: true } }, @@ -322,7 +322,7 @@ it.instance( it.instance( "loads lsp boolean config", Effect.gen(function* () { - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.lsp).toBe(true) }), { config: { lsp: true } }, @@ -355,7 +355,7 @@ it.instance("ignores legacy tui keys in opencode config", () => tui: { scroll_speed: 4 }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.model).toBe("test/model") expect((config as Record).theme).toBeUndefined() expect((config as Record).tui).toBeUndefined() @@ -376,7 +376,7 @@ it.instance("loads JSONC config file", () => }`, ), ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.model).toBe("test/model") expect(config.username).toBe("testuser") }), @@ -398,7 +398,7 @@ it.instance("jsonc overrides json in the same directory", () => $schema: "https://opencode.ai/config.json", model: "override", }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.model).toBe("base") expect(config.username).toBe("base") }), @@ -414,7 +414,7 @@ it.instance("handles environment variable substitution", () => $schema: "https://opencode.ai/config.json", username: "{env:TEST_VAR}", }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.username).toBe("test-user") }), ), @@ -435,7 +435,7 @@ it.instance("preserves env variables when adding $schema to config", () => }), ), ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.username).toBe("secret_value") // Read the file to verify the env variable was preserved @@ -455,7 +455,7 @@ it.instance("handles file inclusion substitution", () => $schema: "https://opencode.ai/config.json", username: "{file:included.txt}", }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.username).toBe("test-user") }), ) @@ -470,7 +470,7 @@ it.instance("handles file inclusion with replacement tokens", () => $schema: "https://opencode.ai/config.json", username: "{file:included.md}", }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.username).toBe("const out = await Bun.$`echo hi`") }), ) @@ -547,7 +547,7 @@ it.instance("validates config schema and throws on invalid fields", () => $schema: "https://opencode.ai/config.json", invalid_field: "should cause error", }) - const exit = yield* Config.Service.use((svc) => svc.get()).pipe(Effect.exit) + const exit = yield* Config.use.get().pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) }), ) @@ -556,7 +556,7 @@ it.instance("throws error for invalid JSON", () => Effect.gen(function* () { const test = yield* TestInstance yield* Effect.promise(() => Filesystem.write(path.join(test.directory, "opencode.json"), "{ invalid json }")) - const exit = yield* Config.Service.use((svc) => svc.get()).pipe(Effect.exit) + const exit = yield* Config.use.get().pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) }), ) @@ -574,7 +574,7 @@ it.instance("handles agent configuration", () => }, }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test_agent"]).toEqual( expect.objectContaining({ model: "test/model", @@ -598,7 +598,7 @@ it.instance("treats agent variant as model-scoped setting (not provider option)" }, }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() const agent = config.agent?.["test_agent"] expect(agent?.variant).toBe("xhigh") @@ -622,7 +622,7 @@ it.instance("handles command configuration", () => }, }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.command?.["test_command"]).toEqual({ template: "test template", description: "test command", @@ -638,7 +638,7 @@ it.instance("migrates autoshare to share field", () => $schema: "https://opencode.ai/config.json", autoshare: true, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.share).toBe("auto") expect(config.autoshare).toBe(true) }), @@ -656,7 +656,7 @@ it.instance("migrates mode field to agent field", () => }, }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test_mode"]).toEqual({ model: "test/model", temperature: 0.5, @@ -679,7 +679,7 @@ model: test/model Test agent prompt`, ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test"]).toEqual( expect.objectContaining({ name: "test", @@ -705,7 +705,7 @@ permission: Ordered permissions`, ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"]) }), ) @@ -732,7 +732,7 @@ mode: subagent Nested agent prompt`, ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["helper"]).toMatchObject({ name: "helper", @@ -770,7 +770,7 @@ description: Nested command Nested command template`, ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.command?.["hello"]).toEqual({ description: "Test command", @@ -804,7 +804,7 @@ description: Nested command Nested command template`, ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.command?.["hello"]).toEqual({ description: "Test command", @@ -834,7 +834,7 @@ it.instance("updates config and writes to file", () => it.instance("gets config directories", () => Effect.gen(function* () { - const dirs = yield* Config.Service.use((svc) => svc.directories()) + const dirs = yield* Config.use.directories() expect(dirs.length).toBeGreaterThanOrEqual(1) }), ) @@ -950,7 +950,7 @@ it.instance("resolves scoped npm plugins in config", () => yield* writeTextEffect(path.join(pluginDir, "index.js"), "export default {}\n") yield* writeConfigEffect(test.directory, { plugin: ["@scope/plugin"] }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.plugin ?? []).toContain("@scope/plugin") }), ) @@ -1014,7 +1014,7 @@ mode: subagent Helper subagent prompt`, ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["helper"]).toMatchObject({ name: "helper", model: "test/model", @@ -1212,7 +1212,7 @@ it.instance("migrates legacy tools config to permissions - allow", () => agent: { test: { tools: { bash: true, read: true } } }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test"]?.permission).toEqual({ bash: "allow", read: "allow", @@ -1228,7 +1228,7 @@ it.instance("migrates legacy tools config to permissions - deny", () => agent: { test: { tools: { bash: false, webfetch: false } } }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test"]?.permission).toEqual({ bash: "deny", webfetch: "deny", @@ -1244,7 +1244,7 @@ it.instance("migrates legacy write tool to edit permission", () => agent: { test: { tools: { write: true } } }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" }) }), ) @@ -1261,7 +1261,7 @@ it.instance( share: "disabled", }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.model).toBe("managed/model") expect(config.share).toBe("disabled") expect(config.username).toBe("testuser") @@ -1278,7 +1278,7 @@ it.instance( disabled_providers: ["openai"], }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.autoupdate).toBe(false) expect(config.disabled_providers).toEqual(["openai"]) }), @@ -1288,7 +1288,7 @@ it.instance( it.instance( "missing managed settings file is not an error", Effect.gen(function* () { - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.model).toBe("user/model") }), { config: { model: "user/model" } }, @@ -1302,7 +1302,7 @@ it.instance("migrates legacy edit tool to edit permission", () => agent: { test: { tools: { edit: false } } }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test"]?.permission).toEqual({ edit: "deny" }) }), ) @@ -1315,7 +1315,7 @@ it.instance("migrates legacy patch tool to edit permission", () => agent: { test: { tools: { patch: true } } }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test"]?.permission).toEqual({ edit: "allow" }) }), ) @@ -1328,7 +1328,7 @@ it.instance("migrates mixed legacy tools config", () => agent: { test: { tools: { bash: true, write: true, read: false, webfetch: true } } }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test"]?.permission).toEqual({ bash: "allow", edit: "allow", @@ -1346,7 +1346,7 @@ it.instance("merges legacy tools with existing permission config", () => agent: { test: { permission: { glob: "allow" }, tools: { bash: true } } }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.agent?.["test"]?.permission).toEqual({ glob: "allow", bash: "allow", @@ -1375,7 +1375,7 @@ it.instance("permission config preserves user key order", () => }, }) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(Object.keys(config.permission!)).toEqual([ "*", "edit", @@ -1451,7 +1451,7 @@ it.instance("project config can override MCP server enabled status", () => "opencode.jsonc", ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.mcp?.jira).toEqual({ type: "remote", url: "https://jira.example.com/mcp", @@ -1496,7 +1496,7 @@ it.instance("MCP config deep merges preserving base config properties", () => "opencode.jsonc", ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.mcp?.myserver).toEqual({ type: "remote", url: "https://myserver.example.com/mcp", @@ -1537,7 +1537,7 @@ it.instance("local .opencode config can override MCP from project config", () => "opencode.json", ) - const config = yield* Config.Service.use((svc) => svc.get()) + const config = yield* Config.use.get() expect(config.mcp?.docs?.enabled).toBe(true) }), ) diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index 2857b707d6..c3ddb507b6 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -93,7 +93,7 @@ it.instance("keeps server and tui plugin merge semantics aligned", () => plugin: [["shared-plugin@2.0.0", { source: "local" }], "local-only@1.0.0"], }) - const server = yield* Config.Service.use((svc) => svc.get()) + const server = yield* Config.use.get() const tui = yield* getTuiConfig(test.directory) const serverPlugins = (server.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item)) const tuiPlugins = (tui.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item)) diff --git a/packages/opencode/test/control-plane/workspace.test.ts b/packages/opencode/test/control-plane/workspace.test.ts index 48a431c21a..5cc07359f3 100644 --- a/packages/opencode/test/control-plane/workspace.test.ts +++ b/packages/opencode/test/control-plane/workspace.test.ts @@ -5,31 +5,31 @@ import Http from "node:http" import path from "node:path" import { setTimeout as delay } from "node:timers/promises" import { NodeHttpServer } from "@effect/platform-node" -import { Effect, Exit, Fiber, Layer, Schema } from "effect" +import { Effect, Layer, Schema } from "effect" import { FetchHttpClient, HttpServer, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { eq } from "drizzle-orm" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import * as Log from "@opencode-ai/core/util/log" import { GlobalBus, type GlobalEvent } from "@/bus/global" import { Database } from "@/storage/db" import { ProjectID } from "@/project/schema" import { ProjectTable } from "@/project/project.sql" +import { context, type InstanceContext } from "@/project/instance-context" import { InstanceRef } from "@/effect/instance-ref" -import { InstanceState } from "@/effect/instance-state" import { Session as SessionNs } from "@/session/session" import { SessionID } from "@/session/schema" import { SessionTable } from "@/session/session.sql" import { SyncEvent } from "@/sync" import { EventSequenceTable } from "@/sync/event.sql" import { resetDatabase } from "../fixture/db" -import { disposeAllInstances, provideTmpdirInstance, TestInstance, tmpdirScoped } from "../fixture/fixture" +import { disposeAllInstances, provideTmpdirInstance, TestInstance, tmpdir } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { registerAdapter } from "../../src/control-plane/adapters" import { WorkspaceID } from "../../src/control-plane/schema" import { WorkspaceTable } from "../../src/control-plane/workspace.sql" import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-plane/types" 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" @@ -66,8 +66,6 @@ const testServerLayer = Layer.mergeAll( NodeHttpServer.layer(Http.createServer, { host: "127.0.0.1", port: 0 }), workspaceLayer(true), SessionNs.defaultLayer, - InstanceStore.defaultLayer.pipe(Layer.provide(InstanceBootstrap.defaultLayer)), - CrossSpawnSpawner.defaultLayer, ) const it = testEffect(testServerLayer) @@ -123,6 +121,12 @@ afterEach(async () => { await resetDatabase() }) +async function withInstance(fn: (ctx: InstanceContext) => T | Promise) { + await using tmp = await tmpdir({ git: true }) + const ctx = await AppRuntime.runPromise(InstanceStore.use.load({ directory: tmp.path })) + return await context.provide(ctx, () => fn(ctx)) +} + async function initGitRepo(dir: string) { await fs.mkdir(dir, { recursive: true }) await $`git init`.cwd(dir).quiet() @@ -135,6 +139,47 @@ async function initGitRepo(dir: string) { await $`git commit -m "base"`.cwd(dir).quiet() } +function currentInstance() { + try { + return context.use() + } catch { + return undefined + } +} + +const runWorkspace = (effect: Effect.Effect) => { + const ctx = currentInstance() + return AppRuntime.runPromise(ctx ? effect.pipe(Effect.provideService(InstanceRef, ctx)) : effect) +} +const createWorkspace = (input: Workspace.CreateInput) => + runWorkspace(Workspace.use.create(input)) +const warpWorkspaceSession = (input: Workspace.SessionWarpInput) => + runWorkspace(Workspace.use.sessionWarp(input)) +const listWorkspaces = (project: Parameters[0]) => + runWorkspace(Workspace.use.list(project)) +const syncListWorkspaces = (project: Parameters[0]) => + runWorkspace(Workspace.use.syncList(project)) +const getWorkspace = (id: WorkspaceID) => runWorkspace(Workspace.use.get(id)) +const removeWorkspace = (id: WorkspaceID) => runWorkspace(Workspace.use.remove(id)) +const workspaceStatus = () => runWorkspace(Workspace.use.status()) +const isWorkspaceSyncing = (id: WorkspaceID) => + runWorkspace(Workspace.use.isSyncing(id)) +const startWorkspaceSyncing = (projectID: ProjectID) => { + void runWorkspace(Workspace.use.startWorkspaceSyncing(projectID)) +} +const startWorkspaceSyncingWithFlag = (projectID: ProjectID, experimentalWorkspaces: boolean) => + Effect.runPromise( + Workspace.use.startWorkspaceSyncing(projectID).pipe( + Effect.provide(workspaceLayer(experimentalWorkspaces)), + ), + ) +const waitForWorkspaceSync = ( + workspaceID: WorkspaceID, + state: Record, + signal?: AbortSignal, + timeout?: number, +) => runWorkspace(Workspace.use.waitForSync(workspaceID, state, signal, timeout)) + function captureGlobalEvents() { const events: GlobalEvent[] = [] const handler = (event: GlobalEvent) => events.push(event) @@ -376,326 +421,289 @@ describe("workspace schemas and exports", () => { }) describe("workspace CRUD", () => { - it.instance( - "get returns undefined for a missing workspace", - () => - Effect.gen(function* () { - expect(yield* Workspace.use.get(WorkspaceID.ascending("wrk_missing_get"))).toBeUndefined() - }), - { git: true }, - ) + test("get returns undefined for a missing workspace", async () => { + await withInstance(async () => { + expect(await getWorkspace(WorkspaceID.ascending("wrk_missing_get"))).toBeUndefined() + }) + }) - it.instance( - "list maps database rows, filters by project, and sorts by id", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - const otherProjectID = ProjectID.make("project-other") - insertProject(otherProjectID, "/tmp/other") - const a = workspaceInfo(instance.project.id, "manual", { - id: WorkspaceID.ascending("wrk_a_list"), - branch: "a", - directory: "/a", - extra: { a: true }, - }) - const b = workspaceInfo(instance.project.id, "manual", { - id: WorkspaceID.ascending("wrk_b_list"), - branch: "b", - directory: "/b", - extra: ["b"], - }) - const other = workspaceInfo(otherProjectID, "manual", { id: WorkspaceID.ascending("wrk_c_list") }) - insertWorkspace(b) - insertWorkspace(other) - insertWorkspace(a) + test("list maps database rows, filters by project, and sorts by id", async () => { + await withInstance(async (instance) => { + const otherProjectID = ProjectID.make("project-other") + insertProject(otherProjectID, "/tmp/other") + const a = workspaceInfo(instance.project.id, "manual", { + id: WorkspaceID.ascending("wrk_a_list"), + branch: "a", + directory: "/a", + extra: { a: true }, + }) + const b = workspaceInfo(instance.project.id, "manual", { + id: WorkspaceID.ascending("wrk_b_list"), + branch: "b", + directory: "/b", + extra: ["b"], + }) + const other = workspaceInfo(otherProjectID, "manual", { id: WorkspaceID.ascending("wrk_c_list") }) + insertWorkspace(b) + insertWorkspace(other) + insertWorkspace(a) - expect(yield* Workspace.use.list(instance.project)).toEqual([a, b]) - }), - { git: true }, - ) + expect(await listWorkspaces(instance.project)).toEqual([a, b]) + }) + }) - it.instance( - "create configures, persists, creates, starts local sync, and passes environment", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({ test: { type: "api", key: "secret" } }) - process.env.OTEL_EXPORTER_OTLP_HEADERS = "authorization=otel" - process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://otel.test" - process.env.OTEL_RESOURCE_ATTRIBUTES = "service.name=opencode-test" + test("create configures, persists, creates, starts local sync, and passes environment", async () => { + await withInstance(async (instance) => { + process.env.OPENCODE_AUTH_CONTENT = JSON.stringify({ test: { type: "api", key: "secret" } }) + process.env.OTEL_EXPORTER_OTLP_HEADERS = "authorization=otel" + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://otel.test" + process.env.OTEL_RESOURCE_ATTRIBUTES = "service.name=opencode-test" - const workspaceID = WorkspaceID.ascending("wrk_create_local") - const type = unique("create-local") - const targetDir = path.join(instance.directory, "created-local") - const recorded = recordedAdapter({ - configure(info) { - return { - ...info, - branch: "configured-branch", - name: "Configured Name", - directory: targetDir, - extra: { configured: true }, - } - }, - async create() { - await fs.mkdir(targetDir, { recursive: true }) - }, - target() { - return { type: "local", directory: targetDir } - }, - }) - registerAdapter(instance.project.id, type, recorded.adapter) + const workspaceID = WorkspaceID.ascending("wrk_create_local") + const type = unique("create-local") + const targetDir = path.join(instance.directory, "created-local") + const recorded = recordedAdapter({ + configure(info) { + return { + ...info, + branch: "configured-branch", + name: "Configured Name", + directory: targetDir, + extra: { configured: true }, + } + }, + async create() { + await fs.mkdir(targetDir, { recursive: true }) + }, + target() { + return { type: "local", directory: targetDir } + }, + }) + registerAdapter(instance.project.id, type, recorded.adapter) - const info = yield* Workspace.use.create({ - id: workspaceID, - type, - branch: null, - projectID: instance.project.id, - extra: null, - }) + const info = await createWorkspace({ + id: workspaceID, + type, + branch: null, + projectID: instance.project.id, + extra: null, + }) - expect(info).toEqual({ - id: workspaceID, - type, - branch: "configured-branch", - name: "Configured Name", - directory: targetDir, - extra: { configured: true }, - projectID: instance.project.id, - timeUsed: info.timeUsed, - }) - expect(yield* Workspace.use.get(workspaceID)).toEqual(info) - expect(yield* Workspace.use.list(instance.project)).toEqual([info]) - expect(recorded.calls.configure).toHaveLength(1) - expect(recorded.calls.configure[0]).toMatchObject({ id: workspaceID, type, directory: null }) - expect(recorded.calls.create).toHaveLength(1) - expect(recorded.calls.create[0].info).toEqual({ - id: workspaceID, - type, - branch: "configured-branch", - name: "Configured Name", - directory: targetDir, - extra: { configured: true }, - projectID: instance.project.id, - }) - expect(JSON.parse(recorded.calls.create[0].env.OPENCODE_AUTH_CONTENT ?? "{}")).toEqual({ - test: { type: "api", key: "secret" }, - }) - expect(recorded.calls.create[0].env.OPENCODE_WORKSPACE_ID).toBe(workspaceID) - expect(recorded.calls.create[0].env.OPENCODE_EXPERIMENTAL_WORKSPACES).toBe("true") - expect(recorded.calls.create[0].env.OTEL_EXPORTER_OTLP_HEADERS).toBe("authorization=otel") - expect(recorded.calls.create[0].env.OTEL_EXPORTER_OTLP_ENDPOINT).toBe("https://otel.test") - expect(recorded.calls.create[0].env.OTEL_RESOURCE_ATTRIBUTES).toBe("service.name=opencode-test") - expect((yield* Workspace.use.status()).find((item) => item.workspaceID === workspaceID)?.status).toBe( - "connected", - ) + expect(info).toEqual({ + id: workspaceID, + type, + branch: "configured-branch", + name: "Configured Name", + directory: targetDir, + extra: { configured: true }, + projectID: instance.project.id, + timeUsed: info.timeUsed, + }) + expect(await getWorkspace(workspaceID)).toEqual(info) + expect(await listWorkspaces(instance.project)).toEqual([info]) + expect(recorded.calls.configure).toHaveLength(1) + expect(recorded.calls.configure[0]).toMatchObject({ id: workspaceID, type, directory: null }) + expect(recorded.calls.create).toHaveLength(1) + expect(recorded.calls.create[0].info).toEqual({ + id: workspaceID, + type, + branch: "configured-branch", + name: "Configured Name", + directory: targetDir, + extra: { configured: true }, + projectID: instance.project.id, + }) + expect(JSON.parse(recorded.calls.create[0].env.OPENCODE_AUTH_CONTENT ?? "{}")).toEqual({ + test: { type: "api", key: "secret" }, + }) + expect(recorded.calls.create[0].env.OPENCODE_WORKSPACE_ID).toBe(workspaceID) + expect(recorded.calls.create[0].env.OPENCODE_EXPERIMENTAL_WORKSPACES).toBe("true") + expect(recorded.calls.create[0].env.OTEL_EXPORTER_OTLP_HEADERS).toBe("authorization=otel") + expect(recorded.calls.create[0].env.OTEL_EXPORTER_OTLP_ENDPOINT).toBe("https://otel.test") + expect(recorded.calls.create[0].env.OTEL_RESOURCE_ATTRIBUTES).toBe("service.name=opencode-test") + expect((await workspaceStatus()).find((item) => item.workspaceID === workspaceID)?.status).toBe("connected") - yield* Workspace.use.remove(workspaceID) - expect((yield* Workspace.use.status()).find((item) => item.workspaceID === workspaceID)?.status).toBeUndefined() - }), - { git: true }, - ) + await removeWorkspace(workspaceID) + expect((await workspaceStatus()).find((item) => item.workspaceID === workspaceID)?.status).toBeUndefined() + }) + }) - it.instance( - "create propagates configure failures and does not insert a workspace", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - const type = unique("configure-failure") - registerAdapter( - instance.project.id, - type, - recordedAdapter({ - configure() { - throw new Error("configure exploded") - }, - target() { - return { type: "local", directory: "/unused" } - }, - }).adapter, - ) - - const exit = yield* Workspace.use - .create({ type, branch: null, projectID: instance.project.id, extra: null }) - .pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(String(exit.cause)).toContain("configure exploded") - expect(yield* Workspace.use.list(instance.project)).toEqual([]) - }), - { git: true }, - ) - - it.instance( - "create leaves the inserted row when adapter create fails", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - const type = unique("create-failure") - const recorded = recordedAdapter({ - async create() { - throw new Error("create exploded") + test("create propagates configure failures and does not insert a workspace", async () => { + await withInstance(async (instance) => { + const type = unique("configure-failure") + registerAdapter( + instance.project.id, + type, + recordedAdapter({ + configure() { + throw new Error("configure exploded") }, target() { return { type: "local", directory: "/unused" } }, - }) - registerAdapter(instance.project.id, type, recorded.adapter) + }).adapter, + ) - const exit = yield* Workspace.use - .create({ type, branch: "branch", projectID: instance.project.id, extra: { x: 1 } }) - .pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(String(exit.cause)).toContain("create exploded") + await expect( + createWorkspace({ type, branch: null, projectID: instance.project.id, extra: null }), + ).rejects.toThrow("configure exploded") + expect(await listWorkspaces(instance.project)).toEqual([]) + }) + }) - const rows = yield* Workspace.use.list(instance.project) - expect(rows).toHaveLength(1) - expect(rows[0]).toMatchObject({ type, branch: "branch", extra: { x: 1 } }) - expect(recorded.calls.target).toHaveLength(0) - yield* Workspace.use.remove(rows[0].id) - }), - { git: true }, - ) + test("create leaves the inserted row when adapter create fails", async () => { + await withInstance(async (instance) => { + const type = unique("create-failure") + const recorded = recordedAdapter({ + async create() { + throw new Error("create exploded") + }, + target() { + return { type: "local", directory: "/unused" } + }, + }) + registerAdapter(instance.project.id, type, recorded.adapter) - it.instance( - "create returns after a local workspace reports error", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - const type = unique("local-error") - const missing = path.join(instance.directory, "missing-local-target") - const recorded = localAdapter(missing, { createDir: false }) - registerAdapter(instance.project.id, type, recorded.adapter) + await expect( + createWorkspace({ type, branch: "branch", projectID: instance.project.id, extra: { x: 1 } }), + ).rejects.toThrow("create exploded") - const info = yield* Workspace.use.create({ type, branch: null, projectID: instance.project.id, extra: null }) + const rows = await listWorkspaces(instance.project) + expect(rows).toHaveLength(1) + expect(rows[0]).toMatchObject({ type, branch: "branch", extra: { x: 1 } }) + expect(recorded.calls.target).toHaveLength(0) + await removeWorkspace(rows[0].id) + }) + }) - expect(info.directory).toBe(missing) - expect((yield* Workspace.use.status()).find((item) => item.workspaceID === info.id)?.status).toBe("error") - yield* Workspace.use.remove(info.id) - }), - { git: true }, - ) + test("create returns after a local workspace reports error", async () => { + await withInstance(async (instance) => { + const type = unique("local-error") + const missing = path.join(instance.directory, "missing-local-target") + const recorded = localAdapter(missing, { createDir: false }) + registerAdapter(instance.project.id, type, recorded.adapter) - it.instance( - "syncList registers adapter-listed workspaces that are missing by name", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - const type = unique("list-sync") - const existing = workspaceInfo(instance.project.id, type, { - id: WorkspaceID.ascending("wrk_list_sync_existing"), - name: "existing", - directory: path.join(instance.directory, "existing"), - }) - insertWorkspace(existing) + const info = await createWorkspace({ type, branch: null, projectID: instance.project.id, extra: null }) - const discovered = { - type, - name: "discovered", - branch: "feature/discovered", - directory: path.join(instance.directory, "discovered"), - extra: { source: "adapter" }, - projectID: instance.project.id, - } - const recorded = recordedAdapter({ - list() { - return [ - { - type, - name: existing.name, - branch: "ignored", - directory: path.join(instance.directory, "ignored"), - extra: null, - projectID: instance.project.id, - }, - discovered, - ] - }, - target(info) { - return { type: "local", directory: info.directory ?? instance.directory } - }, - }) - registerAdapter(instance.project.id, type, recorded.adapter) + expect(info.directory).toBe(missing) + expect((await workspaceStatus()).find((item) => item.workspaceID === info.id)?.status).toBe("error") + await removeWorkspace(info.id) + }) + }) - yield* Workspace.use.syncList(instance.project) - const synced = (yield* Workspace.use.list(instance.project)).filter((item) => item.name === discovered.name) + test("syncList registers adapter-listed workspaces that are missing by name", async () => { + await withInstance(async (instance) => { + const type = unique("list-sync") + const existing = workspaceInfo(instance.project.id, type, { + id: WorkspaceID.ascending("wrk_list_sync_existing"), + name: "existing", + directory: path.join(instance.directory, "existing"), + }) + insertWorkspace(existing) - expect(synced).toHaveLength(1) - expect(synced[0]).toMatchObject(discovered) - expect(synced[0]?.id).toStartWith("wrk_") - expect(yield* Workspace.use.list(instance.project)).toEqual(expect.arrayContaining([existing, synced[0]])) - expect(recorded.calls.list).toBe(1) - expect(recorded.calls.configure).toHaveLength(0) - expect(recorded.calls.create).toHaveLength(0) - expect(recorded.calls.target).toHaveLength(1) - }), - { git: true }, - ) + const discovered = { + type, + name: "discovered", + branch: "feature/discovered", + directory: path.join(instance.directory, "discovered"), + extra: { source: "adapter" }, + projectID: instance.project.id, + } + const recorded = recordedAdapter({ + list() { + return [ + { + type, + name: existing.name, + branch: "ignored", + directory: path.join(instance.directory, "ignored"), + extra: null, + projectID: instance.project.id, + }, + discovered, + ] + }, + target(info) { + return { type: "local", directory: info.directory ?? instance.directory } + }, + }) + registerAdapter(instance.project.id, type, recorded.adapter) - it.instance( - "syncList calls every registered adapter with a list method", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - const typeA = unique("list-sync-a") - const typeB = unique("list-sync-b") - const adapterA = recordedAdapter({ - list() { - return [ - { - type: typeA, - name: "adapter-a", - branch: null, - directory: path.join(instance.directory, "adapter-a"), - extra: null, - projectID: instance.project.id, - }, - ] - }, - target(info) { - return { type: "local", directory: info.directory ?? instance.directory } - }, - }) - const adapterB = recordedAdapter({ - list() { - return [ - { - type: typeB, - name: "adapter-b", - branch: null, - directory: path.join(instance.directory, "adapter-b"), - extra: null, - projectID: instance.project.id, - }, - ] - }, - target(info) { - return { type: "local", directory: info.directory ?? instance.directory } - }, - }) - const noList = recordedAdapter({ - target() { - return { type: "local", directory: instance.directory } - }, - }) - registerAdapter(instance.project.id, typeA, adapterA.adapter) - registerAdapter(instance.project.id, typeB, adapterB.adapter) - registerAdapter(instance.project.id, unique("list-sync-none"), noList.adapter) + await syncListWorkspaces(instance.project) + const synced = (await listWorkspaces(instance.project)).filter((item) => item.name === discovered.name) - yield* Workspace.use.syncList(instance.project) - const synced = yield* Workspace.use.list(instance.project) + expect(synced).toHaveLength(1) + expect(synced[0]).toMatchObject(discovered) + expect(synced[0]?.id).toStartWith("wrk_") + expect(await listWorkspaces(instance.project)).toEqual(expect.arrayContaining([existing, synced[0]])) + expect(recorded.calls.list).toBe(1) + expect(recorded.calls.configure).toHaveLength(0) + expect(recorded.calls.create).toHaveLength(0) + expect(recorded.calls.target).toHaveLength(1) + }) + }) - expect( - synced - .filter((item) => item.type === typeA || item.type === typeB) - .map((item) => item.name) - .toSorted(), - ).toEqual(["adapter-a", "adapter-b"]) - expect(adapterA.calls.list).toBe(1) - expect(adapterB.calls.list).toBe(1) - expect(noList.calls.list).toBe(0) - }), - { git: true }, - ) + test("syncList calls every registered adapter with a list method", async () => { + await withInstance(async (instance) => { + const typeA = unique("list-sync-a") + const typeB = unique("list-sync-b") + const adapterA = recordedAdapter({ + list() { + return [ + { + type: typeA, + name: "adapter-a", + branch: null, + directory: path.join(instance.directory, "adapter-a"), + extra: null, + projectID: instance.project.id, + }, + ] + }, + target(info) { + return { type: "local", directory: info.directory ?? instance.directory } + }, + }) + const adapterB = recordedAdapter({ + list() { + return [ + { + type: typeB, + name: "adapter-b", + branch: null, + directory: path.join(instance.directory, "adapter-b"), + extra: null, + projectID: instance.project.id, + }, + ] + }, + target(info) { + return { type: "local", directory: info.directory ?? instance.directory } + }, + }) + const noList = recordedAdapter({ + target() { + return { type: "local", directory: instance.directory } + }, + }) + registerAdapter(instance.project.id, typeA, adapterA.adapter) + registerAdapter(instance.project.id, typeB, adapterB.adapter) + registerAdapter(instance.project.id, unique("list-sync-none"), noList.adapter) + + await syncListWorkspaces(instance.project) + const synced = await listWorkspaces(instance.project) + + expect( + synced + .filter((item) => item.type === typeA || item.type === typeB) + .map((item) => item.name) + .toSorted(), + ).toEqual(["adapter-a", "adapter-b"]) + expect(adapterA.calls.list).toBe(1) + expect(adapterB.calls.list).toBe(1) + expect(noList.calls.list).toBe(0) + }) + }) it.live("remote create connects to routed event and history endpoints", () => { const calls: FetchCall[] = [] @@ -747,14 +755,11 @@ describe("workspace CRUD", () => { }) }) - it.instance( - "remove returns undefined for a missing workspace", - () => - Effect.gen(function* () { - expect(yield* Workspace.use.remove(WorkspaceID.ascending("wrk_missing_remove"))).toBeUndefined() - }), - { git: true }, - ) + test("remove returns undefined for a missing workspace", async () => { + await withInstance(async () => { + expect(await removeWorkspace(WorkspaceID.ascending("wrk_missing_remove"))).toBeUndefined() + }) + }) it.instance( "remove deletes the workspace, associated sessions, adapter resources, and status", @@ -790,32 +795,28 @@ describe("workspace CRUD", () => { { git: true }, ) - it.instance( - "remove still deletes the row when the adapter cannot remove resources", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - const type = unique("remove-throws") - const info = workspaceInfo(instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") }) - registerAdapter( - instance.project.id, - type, - recordedAdapter({ - async remove() { - throw new Error("remove exploded") - }, - target() { - return { type: "local", directory: "/unused" } - }, - }).adapter, - ) - insertWorkspace(info) + test("remove still deletes the row when the adapter cannot remove resources", async () => { + await withInstance(async (instance) => { + const type = unique("remove-throws") + const info = workspaceInfo(instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") }) + registerAdapter( + instance.project.id, + type, + recordedAdapter({ + async remove() { + throw new Error("remove exploded") + }, + target() { + return { type: "local", directory: "/unused" } + }, + }).adapter, + ) + insertWorkspace(info) - expect(yield* Workspace.use.remove(info.id)).toEqual(info) - expect(yield* Workspace.use.get(info.id)).toBeUndefined() - }), - { git: true }, - ) + expect(await removeWorkspace(info.id)).toEqual(info) + expect(await getWorkspace(info.id)).toBeUndefined() + }) + }) it.instance( "sessionWarp moves a session into a local workspace and claims ownership", @@ -923,40 +924,42 @@ describe("workspace CRUD", () => { { git: true }, ) - it.instance( - "sessionWarp detaches to the source project when invoked from a workspace instance", - () => - Effect.gen(function* () { - const instance = yield* InstanceState.context - const projectID = instance.project.id - const workspaceDir = yield* tmpdirScoped({ git: true }) - const previousType = unique("warp-detach-workspace-instance") - const previous = workspaceInfo(projectID, previousType) - insertWorkspace(previous) - registerAdapter(projectID, previousType, localAdapter(workspaceDir, { createDir: false }).adapter) - const session = yield* SessionNs.use.create({}) - attachSessionToWorkspace(session.id, previous.id) + test("sessionWarp detaches to the source project when invoked from a workspace instance", async () => { + await withInstance(async (instance) => { + const projectID = instance.project.id + await using workspaceTmp = await tmpdir({ git: true }) + const previousType = unique("warp-detach-workspace-instance") + const previous = workspaceInfo(projectID, previousType) + insertWorkspace(previous) + registerAdapter(projectID, previousType, localAdapter(workspaceTmp.path, { createDir: false }).adapter) + const session = await AppRuntime.runPromise( + SessionNs.use.create({}).pipe(Effect.provideService(InstanceRef, instance)), + ) + attachSessionToWorkspace(session.id, previous.id) - const workspaceCtx = yield* InstanceStore.Service.use((store) => store.load({ directory: workspaceDir })) - expect(workspaceCtx.project.id).not.toBe(projectID) - yield* Workspace.use - .sessionWarp({ workspaceID: null, sessionID: session.id }) - .pipe(Effect.provideService(InstanceRef, workspaceCtx)) + const workspaceCtx = await AppRuntime.runPromise( + InstanceStore.use.load({ directory: workspaceTmp.path }), + ) + const workspaceProjectID = await context.provide(workspaceCtx, async () => { + const id = workspaceCtx.project.id + expect(id).not.toBe(projectID) + await warpWorkspaceSession({ workspaceID: null, sessionID: session.id }) + return id + }) - expect( - Database.use((db) => - db - .select({ workspaceID: SessionTable.workspace_id }) - .from(SessionTable) - .where(eq(SessionTable.id, session.id)) - .get(), - )?.workspaceID, - ).toBeNull() - expect(sessionSequenceOwner(session.id)).toBe(projectID) - expect(sessionSequenceOwner(session.id)).not.toBe(workspaceCtx.project.id) - }), - { git: true }, - ) + expect( + Database.use((db) => + db + .select({ workspaceID: SessionTable.workspace_id }) + .from(SessionTable) + .where(eq(SessionTable.id, session.id)) + .get(), + )?.workspaceID, + ).toBeNull() + expect(sessionSequenceOwner(session.id)).toBe(projectID) + expect(sessionSequenceOwner(session.id)).not.toBe(workspaceProjectID) + }) + }) it.live("sessionWarp syncs previous remote history, replays it, steals, and claims the sequence", () => { const calls: FetchCall[] = [] @@ -1069,12 +1072,7 @@ describe("workspace sync state", () => { insertWorkspace(info) registerAdapter(instance.project.id, type, localAdapter(path.join(dir, "flag-disabled")).adapter) - // Isolated runtime with experimentalWorkspaces=false so we can verify the flag gates sync. - yield* Effect.promise(() => - Effect.runPromise( - Workspace.use.startWorkspaceSyncing(instance.project.id).pipe(Effect.provide(workspaceLayer(false))), - ), - ) + yield* Effect.promise(() => startWorkspaceSyncingWithFlag(instance.project.id, false)) yield* Effect.sleep("25 millis") expect((yield* workspace.status()).find((item) => item.workspaceID === info.id)?.status).toBeUndefined() @@ -1565,111 +1563,89 @@ describe("workspace sync state", () => { }) describe("workspace waitForSync", () => { - it.instance( - "returns immediately for an empty fence", - () => - Effect.gen(function* () { - expect(yield* Workspace.use.waitForSync(WorkspaceID.ascending("wrk_wait_empty"), {})).toBeUndefined() - }), - { git: true }, - ) + test("returns immediately for an empty fence", async () => { + await withInstance(async () => { + await expect(waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_empty"), {})).resolves.toBeUndefined() + }) + }) - it.instance( - "returns immediately when the stored sequence already satisfies the fence", - () => - Effect.gen(function* () { - const sessionID = SessionID.descending("ses_wait_done") - Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 4 }).run()) + test("returns immediately when the stored sequence already satisfies the fence", async () => { + await withInstance(async () => { + const sessionID = SessionID.descending("ses_wait_done") + Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 4 }).run()) - expect( - yield* Workspace.use.waitForSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 }), - ).toBeUndefined() - expect( - yield* Workspace.use.waitForSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 }), - ).toBeUndefined() - }), - { git: true }, - ) + await expect( + waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_done"), { [sessionID]: 4 }), + ).resolves.toBeUndefined() + await expect( + waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_done_2"), { [sessionID]: 3 }), + ).resolves.toBeUndefined() + }) + }) - it.instance( - "waits until the database reaches the requested sequence and a workspace event arrives", - () => - Effect.gen(function* () { - const workspaceID = WorkspaceID.ascending("wrk_wait_event") - const sessionID = SessionID.descending("ses_wait_event") - Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 1 }).run()) + test("waits until the database reaches the requested sequence and a workspace event arrives", async () => { + await withInstance(async () => { + const workspaceID = WorkspaceID.ascending("wrk_wait_event") + const sessionID = SessionID.descending("ses_wait_event") + Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 1 }).run()) - const waited = yield* Workspace.use.waitForSync(workspaceID, { [sessionID]: 2 }).pipe(Effect.forkScoped) - yield* Effect.sleep("10 millis") - Database.use((db) => - db.update(EventSequenceTable).set({ seq: 2 }).where(eq(EventSequenceTable.aggregate_id, sessionID)).run(), - ) - GlobalBus.emit("event", { workspace: workspaceID, payload: { type: "anything" } }) + const waited = waitForWorkspaceSync(workspaceID, { [sessionID]: 2 }) + await delay(10) + Database.use((db) => + db.update(EventSequenceTable).set({ seq: 2 }).where(eq(EventSequenceTable.aggregate_id, sessionID)).run(), + ) + GlobalBus.emit("event", { workspace: workspaceID, payload: { type: "anything" } }) - expect(yield* Fiber.join(waited)).toBeUndefined() - }), - { git: true }, - ) + await expect(waited).resolves.toBeUndefined() + }) + }) - it.instance( - "a sync event for a different workspace can also release the fence", - () => - Effect.gen(function* () { - const workspaceID = WorkspaceID.ascending("wrk_wait_sync_any") - const sessionID = SessionID.descending("ses_wait_sync_any") - Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 0 }).run()) + test("a sync event for a different workspace can also release the fence", async () => { + await withInstance(async () => { + const workspaceID = WorkspaceID.ascending("wrk_wait_sync_any") + const sessionID = SessionID.descending("ses_wait_sync_any") + Database.use((db) => db.insert(EventSequenceTable).values({ aggregate_id: sessionID, seq: 0 }).run()) - const waited = yield* Workspace.use.waitForSync(workspaceID, { [sessionID]: 1 }).pipe(Effect.forkScoped) - yield* Effect.sleep("10 millis") - Database.use((db) => - db.update(EventSequenceTable).set({ seq: 1 }).where(eq(EventSequenceTable.aggregate_id, sessionID)).run(), - ) - GlobalBus.emit("event", { - workspace: WorkspaceID.ascending("wrk_other_workspace"), - payload: { type: "sync" }, - }) + const waited = waitForWorkspaceSync(workspaceID, { [sessionID]: 1 }) + await delay(10) + Database.use((db) => + db.update(EventSequenceTable).set({ seq: 1 }).where(eq(EventSequenceTable.aggregate_id, sessionID)).run(), + ) + GlobalBus.emit("event", { + workspace: WorkspaceID.ascending("wrk_other_workspace"), + payload: { type: "sync" }, + }) - expect(yield* Fiber.join(waited)).toBeUndefined() - }), - { git: true }, - ) + await expect(waited).resolves.toBeUndefined() + }) + }) - it.instance( - "rejects with the abort reason when aborted", - () => - Effect.gen(function* () { - const abort = new AbortController() - const reason = new Error("caller aborted") - const waited = yield* Workspace.use - .waitForSync( - WorkspaceID.ascending("wrk_wait_abort"), - { [SessionID.descending("ses_wait_abort")]: 1 }, - abort.signal, - ) - .pipe(Effect.flip, Effect.forkScoped) - abort.abort(reason) - const error = yield* Fiber.join(waited) - expect(error).toMatchObject({ - _tag: "WorkspaceSyncAbortedError", - message: reason.message, - cause: reason, - }) - }), - { git: true }, - ) + test("rejects with the abort reason when aborted", async () => { + await withInstance(async () => { + const abort = new AbortController() + const reason = new Error("caller aborted") + const waited = waitForWorkspaceSync( + WorkspaceID.ascending("wrk_wait_abort"), + { [SessionID.descending("ses_wait_abort")]: 1 }, + abort.signal, + ) + abort.abort(reason) - it.instance( - "times out with the requested fence in the error message", - () => - Effect.gen(function* () { - const sessionID = SessionID.descending("ses_wait_timeout") + await expect(waited).rejects.toMatchObject({ + _tag: "WorkspaceSyncAbortedError", + message: reason.message, + cause: reason, + }) + }) + }) - const failure = yield* Workspace.use - .waitForSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }, undefined, 25) - .pipe(Effect.flip) - expect(String(failure)).toContain(`Timed out waiting for sync fence: {"${sessionID}":1}`) - }), - { git: true, config: undefined }, - 7000, - ) + test("times out with the requested fence in the error message", async () => { + await withInstance(async () => { + const sessionID = SessionID.descending("ses_wait_timeout") + + await expect( + waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }, undefined, 25), + ).rejects.toThrow(`Timed out waiting for sync fence: {"${sessionID}":1}`) + }) + }, 7000) }) diff --git a/packages/opencode/test/file/fsmonitor.test.ts b/packages/opencode/test/file/fsmonitor.test.ts index b8d3bd6055..82e0233326 100644 --- a/packages/opencode/test/file/fsmonitor.test.ts +++ b/packages/opencode/test/file/fsmonitor.test.ts @@ -36,7 +36,7 @@ describe("file fsmonitor", () => { const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow()) expect(before.exitCode).not.toBe(0) - yield* File.Service.use((svc) => svc.status()) + yield* File.use.status() const after = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow()) expect(after.exitCode).not.toBe(0) @@ -63,7 +63,7 @@ describe("file fsmonitor", () => { const before = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow()) expect(before.exitCode).not.toBe(0) - yield* File.Service.use((svc) => svc.read("tracked.txt")) + yield* File.use.read("tracked.txt") const after = yield* Effect.promise(() => $`git fsmonitor--daemon status`.cwd(directory).quiet().nothrow()) expect(after.exitCode).not.toBe(0) diff --git a/packages/opencode/test/file/path-traversal.test.ts b/packages/opencode/test/file/path-traversal.test.ts index 336f214d1a..16712b8752 100644 --- a/packages/opencode/test/file/path-traversal.test.ts +++ b/packages/opencode/test/file/path-traversal.test.ts @@ -10,8 +10,8 @@ import { TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" const it = testEffect(File.defaultLayer) -const read = (file: string) => File.Service.use((svc) => svc.read(file)) -const list = (dir?: string) => File.Service.use((svc) => svc.list(dir)) +const read = (file: string) => File.use.read(file) +const list = (dir?: string) => File.use.list(dir) const expectAccessDenied = (effect: Effect.Effect) => Effect.gen(function* () { const exit = yield* effect.pipe(Effect.exit) diff --git a/packages/opencode/test/file/ripgrep.test.ts b/packages/opencode/test/file/ripgrep.test.ts index d71ce205ea..4996dae2e1 100644 --- a/packages/opencode/test/file/ripgrep.test.ts +++ b/packages/opencode/test/file/ripgrep.test.ts @@ -85,7 +85,7 @@ describe("file.ripgrep", () => { Effect.gen(function* () { const dir = yield* tmpdir((dir) => write(path.join(dir, "match.ts"), "const value = 'other'\n")) - const result = yield* Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle" })) + const result = yield* Ripgrep.use.search({ cwd: dir, pattern: "needle" }) expect(result.partial).toBe(false) expect(result.items).toEqual([]) }), @@ -100,7 +100,7 @@ describe("file.ripgrep", () => { }), ) - const result = yield* Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle" })) + const result = yield* Ripgrep.use.search({ cwd: dir, pattern: "needle" }) expect(result.partial).toBe(false) expect(result.items).toHaveLength(1) expect(result.items[0]?.path.text).toBe(path.join("src", "match.ts")) @@ -118,7 +118,7 @@ describe("file.ripgrep", () => { }), ) - const result = yield* Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle", glob: ["*.ts"] })) + const result = yield* Ripgrep.use.search({ cwd: dir, pattern: "needle", glob: ["*.ts"] }) expect(result.partial).toBe(false) expect(result.items).toHaveLength(1) expect(result.items[0]?.path.text).toContain("match.ts") @@ -136,7 +136,7 @@ describe("file.ripgrep", () => { ) const file = path.join(dir, "match.ts") - const result = yield* Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle", file: [file] })) + const result = yield* Ripgrep.use.search({ cwd: dir, pattern: "needle", file: [file] }) expect(result.partial).toBe(false) expect(result.items).toHaveLength(1) expect(result.items[0]?.path.text).toBe(file) @@ -200,7 +200,7 @@ describe("file.ripgrep", () => { const result = yield* withRipgrepConfig( path.join(dir, "missing-ripgreprc"), - Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle" })), + Ripgrep.use.search({ cwd: dir, pattern: "needle" }), ) expect(result.items).toHaveLength(1) }), @@ -212,7 +212,7 @@ describe("file.ripgrep", () => { const result = yield* withRipgrepConfig( path.join(dir, "missing-ripgreprc"), - Ripgrep.Service.use((rg) => rg.search({ cwd: dir, pattern: "needle" })), + Ripgrep.use.search({ cwd: dir, pattern: "needle" }), ) expect(result.items).toHaveLength(1) }), diff --git a/packages/opencode/test/format/format.test.ts b/packages/opencode/test/format/format.test.ts index c9e57d204e..41468c4d05 100644 --- a/packages/opencode/test/format/format.test.ts +++ b/packages/opencode/test/format/format.test.ts @@ -133,7 +133,7 @@ describe("Format", () => { const file = `${dir}/test.txt` yield* Effect.promise(() => Bun.write(file, "x")) - const formatted = yield* Format.Service.use((fmt) => fmt.file(file)) + const formatted = yield* Format.use.file(file) expect(formatted).toBe(false) }), { @@ -146,10 +146,10 @@ describe("Format", () => { it.live("status() initializes formatter state per directory", () => Effect.gen(function* () { - const a = yield* provideTmpdirInstance(() => Format.Service.use((fmt) => fmt.status()), { + const a = yield* provideTmpdirInstance(() => Format.use.status(), { config: { formatter: false }, }) - const b = yield* provideTmpdirInstance(() => Format.Service.use((fmt) => fmt.status()), { + const b = yield* provideTmpdirInstance(() => Format.use.status(), { config: { formatter: true, }, diff --git a/packages/opencode/test/installation/installation.test.ts b/packages/opencode/test/installation/installation.test.ts index 8193ab8d10..afd2c3b08a 100644 --- a/packages/opencode/test/installation/installation.test.ts +++ b/packages/opencode/test/installation/installation.test.ts @@ -58,7 +58,7 @@ describe("installation", () => { "reads release version from GitHub releases", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("unknown")) + const result = yield* Installation.use.latest("unknown") expect(result).toBe("1.2.3") }), ) @@ -67,7 +67,7 @@ describe("installation", () => { "strips v prefix from GitHub release tag", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("curl")) + const result = yield* Installation.use.latest("curl") expect(result).toBe("4.0.0-beta.1") }), ) @@ -80,7 +80,7 @@ describe("installation", () => { }), ).effect("reads npm versions via registry", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("npm")) + const result = yield* Installation.use.latest("npm") expect(result).toBe("1.5.0") expect(npmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) }), @@ -94,7 +94,7 @@ describe("installation", () => { }), ).effect("reads bun versions via registry", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("bun")) + const result = yield* Installation.use.latest("bun") expect(result).toBe("1.6.0") expect(bunCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) }), @@ -108,7 +108,7 @@ describe("installation", () => { }), ).effect("reads pnpm versions via registry", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("pnpm")) + const result = yield* Installation.use.latest("pnpm") expect(result).toBe("1.7.0") expect(pnpmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) }), @@ -116,7 +116,7 @@ describe("installation", () => { testEffect(testLayer(() => jsonResponse({ version: "2.3.4" }))).effect("reads scoop manifest versions", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("scoop")) + const result = yield* Installation.use.latest("scoop") expect(result).toBe("2.3.4") }), ) @@ -125,7 +125,7 @@ describe("installation", () => { "reads chocolatey feed versions", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("choco")) + const result = yield* Installation.use.latest("choco") expect(result).toBe("3.4.5") }), ) @@ -142,7 +142,7 @@ describe("installation", () => { ), ).effect("reads brew formulae API versions", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("brew")) + const result = yield* Installation.use.latest("brew") expect(result).toBe("2.0.0") }), ) @@ -161,7 +161,7 @@ describe("installation", () => { ), ).effect("reads brew tap info JSON via CLI", () => Effect.gen(function* () { - const result = yield* Installation.Service.use((svc) => svc.latest("brew")) + const result = yield* Installation.use.latest("brew") expect(result).toBe("2.1.0") }), ) diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index 6fb15c4594..17bdba690f 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -175,7 +175,7 @@ mcpTest.instance("state() generates a new state when none is saved", () => auth, ) - const entryBefore = yield* McpAuth.Service.use((auth) => auth.get("test-state-gen")) + const entryBefore = yield* McpAuth.use.get("test-state-gen") expect(entryBefore?.oauthState).toBeUndefined() // state() should generate and return a new state, not throw @@ -184,7 +184,7 @@ mcpTest.instance("state() generates a new state when none is saved", () => expect(state.length).toBe(64) // 32 bytes as hex // The generated state should be persisted - const entryAfter = yield* McpAuth.Service.use((auth) => auth.get("test-state-gen")) + const entryAfter = yield* McpAuth.use.get("test-state-gen") expect(entryAfter?.oauthState).toBe(state) }), ) @@ -202,7 +202,7 @@ mcpTest.instance("state() returns existing state when one is saved", () => // Pre-save a state const existingState = "pre-saved-state-value" - yield* McpAuth.Service.use((auth) => auth.updateOAuthState("test-state-existing", existingState)) + yield* McpAuth.use.updateOAuthState("test-state-existing", existingState) // state() should return the existing state const state = yield* Effect.promise(() => provider.state()) diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts index f2084b095d..1ee8b5488e 100644 --- a/packages/opencode/test/permission-task.test.ts +++ b/packages/opencode/test/permission-task.test.ts @@ -6,7 +6,7 @@ import { testEffect } from "./lib/effect" const it = testEffect(Config.defaultLayer) -const load = Config.Service.use((svc) => svc.get()) +const load = Config.use.get() describe("Permission.evaluate for permission.task", () => { const createRuleset = (rules: Record): Permission.Ruleset => diff --git a/packages/opencode/test/plugin/auth-override.test.ts b/packages/opencode/test/plugin/auth-override.test.ts index adc66e48c5..08b4f293ae 100644 --- a/packages/opencode/test/plugin/auth-override.test.ts +++ b/packages/opencode/test/plugin/auth-override.test.ts @@ -72,10 +72,10 @@ describe("plugin.auth-override", () => { const plain = yield* tmpdirScoped({ git: true }) const plugin = pathToFileURL(path.join(pluginDir, "custom-copilot-auth.ts")).href - const methods = yield* ProviderAuth.Service.use((svc) => svc.methods()).pipe( + const methods = yield* ProviderAuth.use.methods().pipe( Effect.provide(layer(tmp.directory, [plugin])), ) - const plainMethods = yield* ProviderAuth.Service.use((svc) => svc.methods()).pipe( + const plainMethods = yield* ProviderAuth.use.methods().pipe( Effect.provide(layer(plain, [])), provideInstance(plain), ) diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 3f55723663..763b724b63 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -18,7 +18,7 @@ const set = (k: string, v: string) => Effect.gen(function* () { if (!originalEnv.has(k)) originalEnv.set(k, process.env[k]) process.env[k] = v - yield* Env.Service.use((svc) => svc.set(k, v)) + yield* Env.use.set(k, v) }) afterEach(async () => { @@ -30,7 +30,7 @@ afterEach(async () => { await disposeAllInstances() }) -const list = Provider.Service.use((svc) => svc.list()) +const list = Provider.use.list() const withAuthJson = (contents: string) => Effect.acquireRelease( diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index f1d8c93bff..6852dfc60a 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -35,14 +35,14 @@ const set = (k: string, v: string) => Effect.gen(function* () { rememberEnv(k) process.env[k] = v - yield* Env.Service.use((svc) => svc.set(k, v)) + yield* Env.use.set(k, v) }) const remove = (k: string) => Effect.gen(function* () { rememberEnv(k) delete process.env[k] - yield* Env.Service.use((svc) => svc.remove(k)) + yield* Env.use.remove(k) }) afterEach(async () => { @@ -332,7 +332,7 @@ test("parseModel handles model IDs with slashes", () => { it.instance("defaultModel returns first available model when no config set", () => Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") - const model = yield* Provider.Service.use((provider) => provider.defaultModel()) + const model = yield* Provider.use.defaultModel() expect(model.providerID).toBeDefined() expect(model.modelID).toBeDefined() }), @@ -342,7 +342,7 @@ it.instance( "defaultModel respects config model setting", Effect.gen(function* () { yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key") - const model = yield* Provider.Service.use((provider) => provider.defaultModel()) + const model = yield* Provider.use.defaultModel() expect(String(model.providerID)).toBe("anthropic") expect(String(model.modelID)).toBe("claude-sonnet-4-20250514") }), @@ -1032,7 +1032,7 @@ it.instance("getProvider returns undefined for nonexistent provider", () => it.instance("getProvider returns provider info", () => Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") - const provider = yield* Provider.Service.use((svc) => svc.getProvider(ProviderID.anthropic)) + const provider = yield* Provider.use.getProvider(ProviderID.anthropic) expect(provider).toBeDefined() expect(String(provider?.id)).toBe("anthropic") }), @@ -1680,7 +1680,7 @@ it.effect("opencode loader keeps paid models when config apiKey is present", () }) const listIn = (directory: string) => - Provider.Service.use((svc) => svc.list()) + Provider.use.list() .pipe(provideInstanceEffect(directory)) .pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer)) @@ -1698,7 +1698,7 @@ it.effect("opencode loader keeps paid models when auth exists", () => const keyedDir = yield* tmpdirScoped() const listIn = (directory: string) => - Provider.Service.use((svc) => svc.list()) + Provider.use.list() .pipe(provideInstanceEffect(directory)) .pipe(Effect.provide(InstanceLayer.layer), Effect.provide(CrossSpawnSpawner.defaultLayer)) diff --git a/packages/opencode/test/server/global-session-list.test.ts b/packages/opencode/test/server/global-session-list.test.ts index 0fdba1f663..7bf4e24551 100644 --- a/packages/opencode/test/server/global-session-list.test.ts +++ b/packages/opencode/test/server/global-session-list.test.ts @@ -13,7 +13,7 @@ const it = testEffect(Layer.mergeAll(SessionNs.defaultLayer, Project.defaultLaye const withSession = (input?: Parameters[0]) => Effect.acquireRelease( - SessionNs.Service.use((session) => session.create(input)), + SessionNs.use.create(input), (created) => SessionNs.Service.use((session) => session.remove(created.id).pipe(Effect.ignore)), ) @@ -34,8 +34,8 @@ describe("session.listGlobal", () => { expect(ids).toContain(firstSession.id) expect(ids).toContain(secondSession.id) - const firstProject = yield* Project.Service.use((project) => project.get(firstSession.projectID)) - const secondProject = yield* Project.Service.use((project) => project.get(secondSession.projectID)) + const firstProject = yield* Project.use.get(firstSession.projectID) + const secondProject = yield* Project.use.get(secondSession.projectID) const firstItem = sessions.find((session) => session.id === firstSession.id) const secondItem = sessions.find((session) => session.id === secondSession.id) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts index 2613ee3850..aa7e4946da 100644 --- a/packages/opencode/test/server/httpapi-experimental.test.ts +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -31,7 +31,7 @@ function request(path: string, directory: string, init: RequestInit = {}) { } function createSession(input?: Session.CreateInput) { - return Session.Service.use((svc) => svc.create(input)) + return Session.use.create(input) } function json(response: Response) { diff --git a/packages/opencode/test/server/httpapi-instance-context.test.ts b/packages/opencode/test/server/httpapi-instance-context.test.ts index 7c08d0e8d4..35dbf97ba0 100644 --- a/packages/opencode/test/server/httpapi-instance-context.test.ts +++ b/packages/opencode/test/server/httpapi-instance-context.test.ts @@ -74,7 +74,7 @@ const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: stri projectID: input.projectID, }) }), - (info) => Workspace.Service.use((workspace) => workspace.remove(info.id)).pipe(Effect.ignore), + (info) => Workspace.use.remove(info.id).pipe(Effect.ignore), ) const probeInstanceContext = Effect.gen(function* () { diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index 61bc3e8f27..313245f732 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/packages/opencode/test/server/httpapi-session.test.ts @@ -52,7 +52,7 @@ function pathFor(path: string, params: Record) { } function createSession(input?: Session.CreateInput) { - return Session.Service.use((svc) => svc.create(input)) + return Session.use.create(input) } function createTextMessage(sessionID: SessionIDType, text: string) { @@ -101,7 +101,7 @@ const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: stri }), ) }), - (info) => Workspace.Service.use((svc) => svc.remove(info.id)).pipe(Effect.ignore), + (info) => Workspace.use.remove(info.id).pipe(Effect.ignore), ) const insertLegacyAssistantMessage = (sessionID: SessionIDType, time = 1) => diff --git a/packages/opencode/test/server/httpapi-sync.test.ts b/packages/opencode/test/server/httpapi-sync.test.ts index b7fe0aa058..6a1c1624cc 100644 --- a/packages/opencode/test/server/httpapi-sync.test.ts +++ b/packages/opencode/test/server/httpapi-sync.test.ts @@ -36,7 +36,7 @@ describe("sync HttpApi", () => { const tmp = yield* TestInstance const headers = { "x-opencode-directory": tmp.directory, "content-type": "application/json" } const info = spyOn(Log.create({ service: "server.sync" }), "info") - const session = yield* Session.Service.use((svc) => svc.create({ title: "sync" })) + const session = yield* Session.use.create({ title: "sync" }) const started = yield* Effect.promise(() => Promise.resolve(app().request(SyncPaths.start, { method: "POST", headers })), diff --git a/packages/opencode/test/server/httpapi-workspace-routing.test.ts b/packages/opencode/test/server/httpapi-workspace-routing.test.ts index 9d6dc8c3e3..02a1361ba4 100644 --- a/packages/opencode/test/server/httpapi-workspace-routing.test.ts +++ b/packages/opencode/test/server/httpapi-workspace-routing.test.ts @@ -128,7 +128,7 @@ const createWorkspace = (input: { projectID: Project.Info["id"]; type: string; a projectID: input.projectID, }) }), - (info) => Workspace.Service.use((workspace) => workspace.remove(info.id)).pipe(Effect.ignore), + (info) => Workspace.use.remove(info.id).pipe(Effect.ignore), ) const createRemoteWorkspace = (input: { diff --git a/packages/opencode/test/server/httpapi-workspace.test.ts b/packages/opencode/test/server/httpapi-workspace.test.ts index d34bb762ff..19537122aa 100644 --- a/packages/opencode/test/server/httpapi-workspace.test.ts +++ b/packages/opencode/test/server/httpapi-workspace.test.ts @@ -208,7 +208,7 @@ describe("workspace HttpApi", () => { const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info expect(workspace).toMatchObject({ type: "local-test", name: "local-test" }) - const session = yield* Session.Service.use((svc) => svc.create({})).pipe(provideInstance(dir)) + const session = yield* Session.use.create({}).pipe(provideInstance(dir)) const warped = yield* request(WorkspacePaths.warp, dir, { method: "POST", headers: { "content-type": "application/json" }, @@ -424,7 +424,7 @@ describe("workspace HttpApi", () => { body: JSON.stringify({ type: "remote-session-target", branch: null }), }) const workspace = (yield* Effect.promise(() => created.json())) as Workspace.Info - const session = yield* Session.Service.use((svc) => svc.create()).pipe( + const session = yield* Session.use.create().pipe( Effect.provideService(WorkspaceRef, workspace.id), provideInstance(dir), ) diff --git a/packages/opencode/test/server/project-init-git.test.ts b/packages/opencode/test/server/project-init-git.test.ts index c3e77fb2dd..b22777861b 100644 --- a/packages/opencode/test/server/project-init-git.test.ts +++ b/packages/opencode/test/server/project-init-git.test.ts @@ -83,7 +83,7 @@ describe("project.initGit endpoint", () => { worktree: tmp.directory, }) - const ctx = yield* InstanceStore.Service.use((store) => store.reload({ directory: tmp.directory })) + const ctx = yield* InstanceStore.use.reload({ directory: tmp.directory }) const tracked = yield* Snapshot.Service.use((snapshot) => snapshot.track()).pipe( Effect.provideService(InstanceRef, ctx), ) diff --git a/packages/opencode/test/server/session-actions.test.ts b/packages/opencode/test/server/session-actions.test.ts index 44e324b712..465a5c22a2 100644 --- a/packages/opencode/test/server/session-actions.test.ts +++ b/packages/opencode/test/server/session-actions.test.ts @@ -22,8 +22,8 @@ describe("session action routes", () => { Effect.gen(function* () { const test = yield* TestInstance const session = yield* Effect.acquireRelease( - SessionNs.Service.use((svc) => svc.create({})), - (created) => SessionNs.Service.use((svc) => svc.remove(created.id)).pipe(Effect.ignore), + SessionNs.use.create({}), + (created) => SessionNs.use.remove(created.id).pipe(Effect.ignore), ) const res = yield* Effect.promise(() => diff --git a/packages/opencode/test/server/session-diff-missing-patch.test.ts b/packages/opencode/test/server/session-diff-missing-patch.test.ts index 875c031d57..d06eaadfff 100644 --- a/packages/opencode/test/server/session-diff-missing-patch.test.ts +++ b/packages/opencode/test/server/session-diff-missing-patch.test.ts @@ -35,8 +35,8 @@ function pathFor(template: string, params: Record) { const withSession = (input?: Parameters[0]) => Effect.acquireRelease( - Session.Service.use((session) => session.create(input)), - (created) => Session.Service.use((session) => session.remove(created.id)).pipe(Effect.ignore), + Session.use.create(input), + (created) => Session.use.remove(created.id).pipe(Effect.ignore), ) describe("session diff with missing patch (#26574)", () => { diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 4aab0c4b1f..14be56213a 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -28,7 +28,7 @@ const it = testEffect( const withSession = (input?: Parameters[0]) => Effect.acquireRelease( - SessionNs.Service.use((session) => session.create(input)), + SessionNs.use.create(input), (created) => SessionNs.Service.use((session) => session.remove(created.id).pipe(Effect.ignore)), ) @@ -56,7 +56,7 @@ describe("session.list", () => { provideInstance(path.join(test.directory, "packages", "app")), ) - const ids = (yield* SessionNs.Service.use((session) => session.list())).map((session) => session.id) + const ids = (yield* SessionNs.use.list()).map((session) => session.id) expect(ids).toContain(root.id) expect(ids).toContain(parent.id) expect(ids).toContain(current.id) @@ -179,7 +179,7 @@ describe("session.list", () => { const root = yield* withSession({ title: "root-session" }) const child = yield* withSession({ title: "child-session", parentID: root.id }) - const sessions = yield* SessionNs.Service.use((session) => session.list({ roots: true })) + const sessions = yield* SessionNs.use.list({ roots: true }) const ids = sessions.map((session) => session.id) expect(ids).toContain(root.id) @@ -206,7 +206,7 @@ describe("session.list", () => { yield* withSession({ title: "unique-search-term-abc" }) yield* withSession({ title: "other-session-xyz" }) - const sessions = yield* SessionNs.Service.use((session) => session.list({ search: "unique-search" })) + const sessions = yield* SessionNs.use.list({ search: "unique-search" }) const titles = sessions.map((session) => session.title) expect(titles).toContain("unique-search-term-abc") @@ -223,7 +223,7 @@ describe("session.list", () => { yield* withSession({ title: "session-2" }) yield* withSession({ title: "session-3" }) - const sessions = yield* SessionNs.Service.use((session) => session.list({ limit: 2 })) + const sessions = yield* SessionNs.use.list({ limit: 2 }) expect(sessions.length).toBe(2) }), { git: true }, diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index e603accbbe..e3065a855a 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -40,8 +40,8 @@ const withoutWatcher = (effect: Effect.Effect) => { } const sessionScoped = Effect.acquireRelease( - SessionNs.Service.use((svc) => svc.create({})), - (session) => SessionNs.Service.use((svc) => svc.remove(session.id)).pipe(Effect.ignore), + SessionNs.use.create({}), + (session) => SessionNs.use.remove(session.id).pipe(Effect.ignore), ) const fill = Effect.fn("SessionMessagesTest.fill")(function* ( diff --git a/packages/opencode/test/server/session-select.test.ts b/packages/opencode/test/server/session-select.test.ts index 9e24ed0ecc..0f3875ae14 100644 --- a/packages/opencode/test/server/session-select.test.ts +++ b/packages/opencode/test/server/session-select.test.ts @@ -16,7 +16,7 @@ describe("tui.selectSession endpoint", () => { () => Effect.gen(function* () { const tmp = yield* TestInstance - const session = yield* Session.Service.use((svc) => svc.create({})) + const session = yield* Session.use.create({}) const app = Server.Default().app const response = yield* Effect.promise(() => diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index fa546a4d7a..62e6388e46 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -292,7 +292,7 @@ function createSummaryCompaction(sessionID: SessionID) { } function readCompactionPart(sessionID: SessionID) { - return SessionNs.Service.use((ssn) => ssn.messages({ sessionID })).pipe( + return SessionNs.use.messages({ sessionID }).pipe( Effect.map((messages) => messages.at(-2)?.parts.find((item): item is MessageV2.CompactionPart => item.type === "compaction"), ), diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index e7cbd213ec..9a2b155781 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -35,7 +35,7 @@ const awaitDeferred = (deferred: Deferred.Deferred, message: string) => Effect.sleep("2 seconds").pipe(Effect.flatMap(() => Effect.fail(new Error(message)))), ) -const remove = (id: SessionID) => SessionNs.Service.use((svc) => svc.remove(id)) +const remove = (id: SessionID) => SessionNs.use.remove(id) const subscribeGlobal = (type: string, callback: (event: NonNullable) => void) => { const listener = (event: GlobalEvent) => { diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 14ecff7452..c0a7b5f29b 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -128,7 +128,7 @@ describe("ShareNext", () => { Effect.gen(function* () { yield* seed("https://control.example.com", "org-1") - const req = yield* ShareNext.Service.use((svc) => svc.request()).pipe(Effect.provide(live(none))) + const req = yield* ShareNext.use.request().pipe(Effect.provide(live(none))) expect(req.api.create).toBe("/api/shares") expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync") @@ -147,7 +147,7 @@ describe("ShareNext", () => { provideTmpdirInstance( () => Effect.gen(function* () { - const session = yield* Session.Service.use((svc) => svc.create({ title: "test" })) + const session = yield* Session.use.create({ title: "test" }) const seen: HttpClientRequest.HttpClientRequest[] = [] const client = HttpClient.make((req) => { seen.push(req) @@ -163,7 +163,7 @@ describe("ShareNext", () => { return Effect.succeed(json(req, { ok: true })) }) - const result = yield* ShareNext.Service.use((svc) => svc.create(session.id)).pipe( + const result = yield* ShareNext.use.create(session.id).pipe( Effect.provide(live(client)), ) @@ -188,7 +188,7 @@ describe("ShareNext", () => { provideTmpdirInstance( () => Effect.gen(function* () { - const session = yield* Session.Service.use((svc) => svc.create({ title: "test" })) + const session = yield* Session.use.create({ title: "test" }) const seen: HttpClientRequest.HttpClientRequest[] = [] const client = HttpClient.make((req) => { seen.push(req) @@ -205,8 +205,8 @@ describe("ShareNext", () => { }) yield* Effect.gen(function* () { - yield* ShareNext.Service.use((svc) => svc.create(session.id)) - yield* ShareNext.Service.use((svc) => svc.remove(session.id)) + yield* ShareNext.use.create(session.id) + yield* ShareNext.use.remove(session.id) }).pipe(Effect.provide(live(client))) expect(share(session.id)).toBeUndefined() @@ -222,7 +222,7 @@ describe("ShareNext", () => { it.live("create fails on a non-ok response and does not persist a share", () => provideTmpdirInstance(() => Effect.gen(function* () { - const session = yield* Session.Service.use((svc) => svc.create({ title: "test" })) + const session = yield* Session.use.create({ title: "test" }) const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500))) const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id))).pipe(