Skip to content

Commit d99dde6

Browse files
authored
Migrate test inits from Promise to Effect (#25377)
1 parent becf57e commit d99dde6

6 files changed

Lines changed: 199 additions & 160 deletions

File tree

packages/opencode/src/effect/run-service.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Effect, Layer, ManagedRuntime } from "effect"
1+
import { Effect, Fiber, Layer, ManagedRuntime } from "effect"
22
import * as Context from "effect/Context"
33
import { Instance } from "@/project/instance"
44
import { LocalContext } from "@/util/local-context"
@@ -24,15 +24,20 @@ export function attachWith<A, E, R>(effect: Effect.Effect<A, E, R>, refs: Refs):
2424
}
2525

2626
export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
27-
try {
28-
return attachWith(effect, {
29-
instance: Instance.current,
30-
workspace: WorkspaceContext.workspaceID,
31-
})
32-
} catch (err) {
33-
if (!(err instanceof LocalContext.NotFound)) throw err
34-
}
35-
return effect
27+
const workspace = WorkspaceContext.workspaceID
28+
const instance = (() => {
29+
try {
30+
return Instance.current
31+
} catch (err) {
32+
if (!(err instanceof LocalContext.NotFound)) throw err
33+
}
34+
})()
35+
if (instance && workspace !== undefined) return attachWith(effect, { instance, workspace })
36+
const fiber = Fiber.getCurrent()
37+
return attachWith(effect, {
38+
instance: instance ?? (fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined),
39+
workspace: workspace ?? (fiber ? Context.getReferenceUnsafe(fiber.context, WorkspaceRef) : undefined),
40+
})
3641
}
3742

3843
export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {

packages/opencode/src/project/instance.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,18 @@
11
import { Effect } from "effect"
2-
import { InstanceRef } from "@/effect/instance-ref"
3-
import * as Project from "./project"
42
import { context, type InstanceContext } from "./instance-context"
53
import { InstanceStore } from "./instance-store"
64

75
export type { InstanceContext } from "./instance-context"
86
export type { LoadInput } from "./instance-store"
97

10-
type LegacyLoadInput = {
11-
directory: string
12-
init?: () => Promise<unknown>
13-
project?: Project.Info
14-
worktree?: string
15-
}
16-
17-
// Promise-style legacy inits often read Instance.directory etc. from the ALS context.
18-
// The new Effect-typed init path doesn't bind ALS — it provides InstanceRef. To keep
19-
// legacy inits working without forcing every test to convert, bind ALS around the
20-
// Promise call here using the instance ctx that the store provides via InstanceRef.
21-
const liftLegacyInput = (input: LegacyLoadInput): InstanceStore.LoadInput => {
22-
const { init, ...rest } = input
23-
if (!init) return rest
24-
return {
25-
...rest,
26-
init: Effect.gen(function* () {
27-
const ctx = yield* InstanceRef
28-
yield* Effect.promise(() => (ctx ? context.provide(ctx, init) : init()))
29-
}),
30-
}
31-
}
32-
338
export const Instance = {
34-
load(input: LegacyLoadInput): Promise<InstanceContext> {
35-
return InstanceStore.runtime.runPromise((store) => store.load(liftLegacyInput(input)))
9+
load(input: InstanceStore.LoadInput): Promise<InstanceContext> {
10+
return InstanceStore.runtime.runPromise((store) => store.load(input))
3611
},
37-
async provide<R>(input: { directory: string; init?: () => Promise<unknown>; fn: () => R }): Promise<R> {
38-
return context.provide(await Instance.load({ directory: input.directory, init: input.init }), async () =>
39-
input.fn(),
12+
async provide<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }): Promise<R> {
13+
return context.provide(
14+
await Instance.load({ directory: input.directory, init: input.init }),
15+
async () => input.fn(),
4016
)
4117
},
4218
get current() {
@@ -69,8 +45,8 @@ export const Instance = {
6945
restore<R>(ctx: InstanceContext, fn: () => R): R {
7046
return context.provide(ctx, fn)
7147
},
72-
async reload(input: LegacyLoadInput) {
73-
return InstanceStore.runtime.runPromise((store) => store.reload(liftLegacyInput(input)))
48+
async reload(input: InstanceStore.LoadInput) {
49+
return InstanceStore.runtime.runPromise((store) => store.reload(input))
7450
},
7551
async dispose() {
7652
return InstanceStore.runtime.runPromise((store) => store.dispose(Instance.current))

packages/opencode/test/effect/run-service.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { expect } from "bun:test"
22
import { Effect, Layer, Context } from "effect"
3+
import { InstanceRef } from "../../src/effect/instance-ref"
34
import { makeRuntime } from "../../src/effect/run-service"
5+
import { ProjectID } from "../../src/project/schema"
46
import { it } from "../lib/effect"
57

68
class Shared extends Context.Service<Shared, { readonly id: number }>()("@test/Shared") {}
9+
const testDirectory = "/tmp/opencode-test"
710

811
it.live("makeRuntime shares dependent layers through the shared memo map", () =>
912
Effect.gen(function* () {
@@ -47,3 +50,40 @@ it.live("makeRuntime shares dependent layers through the shared memo map", () =>
4750
expect(n).toBe(1)
4851
}),
4952
)
53+
54+
it.live("makeRuntime inherits InstanceRef from the current fiber", () =>
55+
Effect.gen(function* () {
56+
class NeedsInstance extends Context.Service<
57+
NeedsInstance,
58+
{ readonly directory: () => Effect.Effect<string | undefined> }
59+
>()("@test/NeedsInstance") {}
60+
61+
const runtime = makeRuntime(
62+
NeedsInstance,
63+
Layer.succeed(
64+
NeedsInstance,
65+
NeedsInstance.of({
66+
directory: () =>
67+
Effect.gen(function* () {
68+
return (yield* InstanceRef)?.directory
69+
}),
70+
}),
71+
),
72+
)
73+
74+
const actual = yield* Effect.promise(() => runtime.runPromise((svc) => svc.directory()))
75+
76+
expect(actual).toBe(testDirectory)
77+
}).pipe(
78+
Effect.provideService(InstanceRef, {
79+
directory: testDirectory,
80+
worktree: testDirectory,
81+
project: {
82+
id: ProjectID.global,
83+
worktree: testDirectory,
84+
time: { created: 0, updated: 0 },
85+
sandboxes: [],
86+
},
87+
}),
88+
),
89+
)

packages/opencode/test/project/instance.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,22 @@ describe("InstanceStore", () => {
252252
expect(() => Instance.current).toThrow()
253253
}),
254254
)
255+
256+
it.live("does not install legacy ALS around Effect init", () =>
257+
Effect.gen(function* () {
258+
const dir = yield* tmpdirScoped()
259+
260+
const directory = yield* Effect.promise(() =>
261+
Instance.provide({
262+
directory: dir,
263+
init: Effect.sync(() => {
264+
expect(() => Instance.current).toThrow()
265+
}),
266+
fn: () => Instance.directory,
267+
}),
268+
)
269+
270+
expect(directory).toBe(dir)
271+
}),
272+
)
255273
})

packages/opencode/test/provider/amazon-bedrock.test.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async ()
4545
})
4646
await Instance.provide({
4747
directory: tmp.path,
48-
init: async () => {
48+
init: Effect.promise(async () => {
4949
set("AWS_REGION", "us-east-1")
5050
set("AWS_PROFILE", "default")
51-
},
51+
}).pipe(Effect.asVoid),
5252
fn: async () => {
5353
const providers = await list()
5454
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -70,10 +70,10 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async ()
7070
})
7171
await Instance.provide({
7272
directory: tmp.path,
73-
init: async () => {
73+
init: Effect.promise(async () => {
7474
set("AWS_REGION", "eu-west-1")
7575
set("AWS_PROFILE", "default")
76-
},
76+
}).pipe(Effect.asVoid),
7777
fn: async () => {
7878
const providers = await list()
7979
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -125,11 +125,11 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => {
125125

126126
await Instance.provide({
127127
directory: tmp.path,
128-
init: async () => {
128+
init: Effect.promise(async () => {
129129
set("AWS_PROFILE", "")
130130
set("AWS_ACCESS_KEY_ID", "")
131131
set("AWS_BEARER_TOKEN_BEDROCK", "")
132-
},
132+
}).pipe(Effect.asVoid),
133133
fn: async () => {
134134
const providers = await list()
135135
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -171,10 +171,10 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async
171171
})
172172
await Instance.provide({
173173
directory: tmp.path,
174-
init: async () => {
174+
init: Effect.promise(async () => {
175175
set("AWS_PROFILE", "default")
176176
set("AWS_ACCESS_KEY_ID", "test-key-id")
177-
},
177+
}).pipe(Effect.asVoid),
178178
fn: async () => {
179179
const providers = await list()
180180
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -203,9 +203,9 @@ test("Bedrock: includes custom endpoint in options when specified", async () =>
203203
})
204204
await Instance.provide({
205205
directory: tmp.path,
206-
init: async () => {
206+
init: Effect.promise(async () => {
207207
set("AWS_PROFILE", "default")
208-
},
208+
}).pipe(Effect.asVoid),
209209
fn: async () => {
210210
const providers = await list()
211211
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -236,12 +236,12 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async ()
236236
})
237237
await Instance.provide({
238238
directory: tmp.path,
239-
init: async () => {
239+
init: Effect.promise(async () => {
240240
set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
241241
set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
242242
set("AWS_PROFILE", "")
243243
set("AWS_ACCESS_KEY_ID", "")
244-
},
244+
}).pipe(Effect.asVoid),
245245
fn: async () => {
246246
const providers = await list()
247247
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -279,9 +279,9 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () =>
279279
})
280280
await Instance.provide({
281281
directory: tmp.path,
282-
init: async () => {
282+
init: Effect.promise(async () => {
283283
set("AWS_PROFILE", "default")
284-
},
284+
}).pipe(Effect.asVoid),
285285
fn: async () => {
286286
const providers = await list()
287287
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -316,9 +316,9 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => {
316316
})
317317
await Instance.provide({
318318
directory: tmp.path,
319-
init: async () => {
319+
init: Effect.promise(async () => {
320320
set("AWS_PROFILE", "default")
321-
},
321+
}).pipe(Effect.asVoid),
322322
fn: async () => {
323323
const providers = await list()
324324
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -352,9 +352,9 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () =>
352352
})
353353
await Instance.provide({
354354
directory: tmp.path,
355-
init: async () => {
355+
init: Effect.promise(async () => {
356356
set("AWS_PROFILE", "default")
357-
},
357+
}).pipe(Effect.asVoid),
358358
fn: async () => {
359359
const providers = await list()
360360
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
@@ -388,9 +388,9 @@ test("Bedrock: model without prefix in US region should get us. prefix added", a
388388
})
389389
await Instance.provide({
390390
directory: tmp.path,
391-
init: async () => {
391+
init: Effect.promise(async () => {
392392
set("AWS_PROFILE", "default")
393-
},
393+
}).pipe(Effect.asVoid),
394394
fn: async () => {
395395
const providers = await list()
396396
expect(providers[ProviderID.amazonBedrock]).toBeDefined()

0 commit comments

Comments
 (0)