Removes all direct AppRuntime usage from packages/opencode/test —
`grep -r AppRuntime packages/opencode/test` now returns nothing.
Two patterns are applied:
1. Event tests rewritten in the httpapi-cors.test.ts style. The two
/event SSE tests now serve HttpApiApp.routes on
NodeHttpServer.layerTest and hit them via HttpClient. Pub/sub
identity with the in-process routes is preserved via a new opt-in
`testEffectShared` (in test/lib/effect.ts) that builds the test
layer through the shared process-wide memoMap so Bus.defaultLayer
resolves to the same Bus.Service the routes subscribed to.
The SSE reader helpers move to test/lib/sse.ts and use HttpClient +
Effect.Stream + Queue<SseEvent>.
The D7 diagnostic case is removed: the AppRuntime-vs-test-runtime
distinction it diagnosed no longer exists.
2. Surgical swap in the remaining four files
(provider/{amazon-bedrock,provider}, session/llm,
control-plane/workspace). Each `AppRuntime.runPromise(...)` becomes a
module-level `ManagedRuntime.make(Service.defaultLayer, { memoMap })`.
The shared memoMap preserves service identity, so behavior is
unchanged.
Tests: event (9/9), amazon-bedrock (19/19), provider (84/84),
workspace (35/35); 147 pass across the 5 affected files. The 3
pre-existing failures in session/llm.test.ts are independent of this
change (verified by stashing the diff).
279 lines
9.4 KiB
TypeScript
279 lines
9.4 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test"
|
|
import { Effect, Layer } from "effect"
|
|
import path from "path"
|
|
import { unlink } from "fs/promises"
|
|
import { Global } from "@opencode-ai/core/global"
|
|
import { Filesystem } from "@/util/filesystem"
|
|
import { Env } from "../../src/env"
|
|
import { Provider } from "@/provider/provider"
|
|
import { ProviderID } from "../../src/provider/schema"
|
|
import { disposeAllInstances } from "../fixture/fixture"
|
|
import { testEffect } from "../lib/effect"
|
|
|
|
const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer))
|
|
|
|
const originalEnv = new Map<string, string | undefined>()
|
|
|
|
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))
|
|
})
|
|
|
|
afterEach(async () => {
|
|
for (const [key, value] of originalEnv) {
|
|
if (value === undefined) delete process.env[key]
|
|
else process.env[key] = value
|
|
}
|
|
originalEnv.clear()
|
|
await disposeAllInstances()
|
|
})
|
|
|
|
const list = Provider.Service.use((svc) => svc.list())
|
|
|
|
const withAuthJson = (contents: string) =>
|
|
Effect.acquireRelease(
|
|
Effect.promise(async () => {
|
|
const authPath = path.join(Global.Path.data, "auth.json")
|
|
let original: string | undefined
|
|
try {
|
|
original = await Filesystem.readText(authPath)
|
|
} catch {
|
|
original = undefined
|
|
}
|
|
await Filesystem.write(authPath, contents)
|
|
return { authPath, original }
|
|
}),
|
|
({ authPath, original }) =>
|
|
Effect.promise(async () => {
|
|
if (original !== undefined) {
|
|
await Filesystem.write(authPath, original)
|
|
return
|
|
}
|
|
await unlink(authPath).catch(() => undefined)
|
|
}),
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: config region takes precedence over AWS_REGION env var",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_REGION", "us-east-1")
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
|
}),
|
|
{ config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } },
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: falls back to AWS_REGION env var when no config region",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_REGION", "eu-west-1")
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
|
}),
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: loads when bearer token from auth.json is present",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* withAuthJson(JSON.stringify({ "amazon-bedrock": { type: "api", key: "test-bearer-token" } }))
|
|
yield* set("AWS_PROFILE", "")
|
|
yield* set("AWS_ACCESS_KEY_ID", "")
|
|
yield* set("AWS_BEARER_TOKEN_BEDROCK", "")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
|
|
}),
|
|
{ config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } },
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: config profile takes precedence over AWS_PROFILE env var",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
yield* set("AWS_ACCESS_KEY_ID", "test-key-id")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
|
|
}),
|
|
{
|
|
config: {
|
|
provider: { "amazon-bedrock": { options: { profile: "my-custom-profile", region: "us-east-1" } } },
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: includes custom endpoint in options when specified",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].options?.endpoint).toBe(
|
|
"https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
|
|
)
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { endpoint: "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
|
|
yield* set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
|
|
yield* set("AWS_PROFILE", "")
|
|
yield* set("AWS_ACCESS_KEY_ID", "")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
|
|
}),
|
|
{ config: { provider: { "amazon-bedrock": { options: { region: "us-east-1" } } } } },
|
|
)
|
|
|
|
// Cross-region inference profile prefix handling.
|
|
// Models from models.dev may come with prefixes already (e.g. us., eu., global.).
|
|
// These should NOT be double-prefixed when passed to the SDK.
|
|
|
|
it.instance(
|
|
"Bedrock: model with us. prefix should not be double-prefixed",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "us-east-1" },
|
|
models: { "us.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (US)" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: model with global. prefix should not be prefixed",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "us-east-1" },
|
|
models: { "global.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (Global)" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: model with eu. prefix should not be double-prefixed",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "eu-west-1" },
|
|
models: { "eu.anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5 (EU)" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"Bedrock: model without prefix in US region should get us. prefix added",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
yield* set("AWS_PROFILE", "default")
|
|
const providers = yield* list
|
|
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
|
|
expect(providers[ProviderID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
|
|
}),
|
|
{
|
|
config: {
|
|
provider: {
|
|
"amazon-bedrock": {
|
|
options: { region: "us-east-1" },
|
|
models: { "anthropic.claude-opus-4-5-20251101-v1:0": { name: "Claude Opus 4.5" } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
// Direct unit tests for cross-region inference profile prefix detection.
|
|
describe("Bedrock cross-region prefix detection", () => {
|
|
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
|
|
|
|
test("should detect global. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "global.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect us. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "us.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect eu. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "eu.anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect jp. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "jp.anthropic.claude-sonnet-4-20250514-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect apac. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "apac.anthropic.claude-sonnet-4-20250514-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should detect au. prefix", () => {
|
|
expect(crossRegionPrefixes.some((p) => "au.anthropic.claude-sonnet-4-5-20250929-v1:0".startsWith(p))).toBe(true)
|
|
})
|
|
|
|
test("should NOT detect prefix for non-prefixed model", () => {
|
|
expect(crossRegionPrefixes.some((p) => "anthropic.claude-opus-4-5-20251101-v1:0".startsWith(p))).toBe(false)
|
|
})
|
|
|
|
test("should NOT detect prefix for amazon nova models", () => {
|
|
expect(crossRegionPrefixes.some((p) => "amazon.nova-pro-v1:0".startsWith(p))).toBe(false)
|
|
})
|
|
|
|
test("should NOT detect prefix for cohere models", () => {
|
|
expect(crossRegionPrefixes.some((p) => "cohere.command-r-plus-v1:0".startsWith(p))).toBe(false)
|
|
})
|
|
})
|