1
0
Эх сурвалжийг харах

Speed up targeted opencode tests

Reduce avoidable setup costs in slow opencode tests while preserving reviewed coverage and recording the benchmark evidence for follow-up test-suite work.
Kit Langton 3 сар өмнө
parent
commit
896ad7b884

+ 2 - 0
packages/opencode/package.json

@@ -10,6 +10,8 @@
     "test": "bun test --timeout 30000",
     "test": "bun test --timeout 30000",
     "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
     "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
     "test:httpapi": "bun run script/httpapi-exercise.ts --mode coverage --fail-on-missing --fail-on-skip && bun run script/httpapi-exercise.ts --mode auth --fail-on-missing --fail-on-skip && bun run script/httpapi-exercise.ts --mode effect --fail-on-missing --fail-on-skip",
     "test:httpapi": "bun run script/httpapi-exercise.ts --mode coverage --fail-on-missing --fail-on-skip && bun run script/httpapi-exercise.ts --mode auth --fail-on-missing --fail-on-skip && bun run script/httpapi-exercise.ts --mode effect --fail-on-missing --fail-on-skip",
+    "bench:test": "bun run script/bench-test-suite.ts",
+    "profile:test": "bun run script/profile-test-files.ts",
     "build": "bun run script/build.ts",
     "build": "bun run script/build.ts",
     "fix-node-pty": "bun run script/fix-node-pty.ts",
     "fix-node-pty": "bun run script/fix-node-pty.ts",
     "dev": "bun run --conditions=browser ./src/index.ts",
     "dev": "bun run --conditions=browser ./src/index.ts",

+ 52 - 0
packages/opencode/script/bench-test-suite.ts

@@ -0,0 +1,52 @@
+// Full-suite timing harness for the test-speed research in ../../perf/test-suite.md.
+// Use this for periodic sanity checks; use profile-test-files.ts for discovery.
+// Env: BENCH_WARMUPS=0 BENCH_RUNS=1 bun run bench:test
+const warmups = Number(Bun.env.BENCH_WARMUPS ?? 0)
+const runs = Number(Bun.env.BENCH_RUNS ?? 1)
+const timings: number[] = []
+
+if (!Number.isInteger(warmups) || warmups < 0) {
+  console.error("BENCH_WARMUPS must be a non-negative integer")
+  process.exit(1)
+}
+if (!Number.isInteger(runs) || runs < 1) {
+  console.error("BENCH_RUNS must be a positive integer")
+  process.exit(1)
+}
+
+for (const index of Array.from({ length: warmups + runs }, (_, index) => index)) {
+  const measured = index >= warmups
+  const label = measured ? `run ${index - warmups + 1}/${runs}` : `warmup ${index + 1}/${warmups}`
+  const start = performance.now()
+  console.log(`bench:test ${label}`)
+
+  const proc = Bun.spawn(["bun", "test", "--timeout", "30000"], {
+    cwd: import.meta.dir + "/..",
+    stdout: "inherit",
+    stderr: "inherit",
+    env: Bun.env,
+  })
+
+  const exitCode = await proc.exited
+  if (exitCode !== 0) {
+    console.error(`bench:test failed during ${label} with exit code ${exitCode}`)
+    process.exit(exitCode)
+  }
+
+  const seconds = (performance.now() - start) / 1000
+  console.log(`bench:test ${label} ${seconds.toFixed(3)}s`)
+  if (measured) timings.push(seconds)
+}
+
+const sorted = timings.toSorted((a, b) => a - b)
+const median = sorted[Math.floor(sorted.length / 2)]
+const mean = timings.reduce((sum, timing) => sum + timing, 0) / timings.length
+const best = sorted[0] ?? median
+const worst = sorted.at(-1) ?? median
+
+console.log(
+  `bench:test median=${median.toFixed(3)}s mean=${mean.toFixed(3)}s best=${best.toFixed(3)}s worst=${worst.toFixed(3)}s`,
+)
+console.log(`METRIC test_suite_seconds=${median.toFixed(3)}`)
+console.log(`METRIC test_suite_best_seconds=${best.toFixed(3)}`)
+console.log(`METRIC test_suite_worst_seconds=${worst.toFixed(3)}`)

+ 42 - 0
packages/opencode/script/profile-test-files.ts

@@ -0,0 +1,42 @@
+// Per-file profiler for finding candidate test-speed work; see ../../perf/test-suite.md
+// for the benchmark notes, kept wins, and discarded experiments.
+// Example: TEST_PROFILE_GLOB='test/server/**/*.test.ts' TEST_PROFILE_TOP=15 bun run profile:test
+const pattern = Bun.env.TEST_PROFILE_GLOB ?? "test/**/*.test.{ts,tsx}"
+const limit = Number(Bun.env.TEST_PROFILE_LIMIT ?? 0)
+const timeout = Bun.env.TEST_PROFILE_TIMEOUT ?? "30000"
+const files = Array.fromAsync(new Bun.Glob(pattern).scan({ cwd: import.meta.dir + "/..", onlyFiles: true }))
+  .then((files) => files.toSorted())
+  .then((files) => (limit > 0 ? files.slice(0, limit) : files))
+
+const results = []
+for (const file of await files) {
+  const start = performance.now()
+  const proc = Bun.spawn(["bun", "test", "--timeout", timeout, file], {
+    cwd: import.meta.dir + "/..",
+    stdout: "pipe",
+    stderr: "pipe",
+    env: Bun.env,
+  })
+  const [output, error, exitCode] = await Promise.all([
+    new Response(proc.stdout).text(),
+    new Response(proc.stderr).text(),
+    proc.exited,
+  ])
+  const seconds = (performance.now() - start) / 1000
+  results.push({ file, seconds, exitCode })
+  console.log(`${exitCode === 0 ? "PASS" : "FAIL"} ${seconds.toFixed(3)}s ${file}`)
+  if (exitCode !== 0) console.log((output + error).trim())
+}
+
+const sorted = results.toSorted((a, b) => b.seconds - a.seconds)
+console.log("\nSlowest test files:")
+for (const result of sorted.slice(0, Number(Bun.env.TEST_PROFILE_TOP ?? 20))) {
+  console.log(`${result.seconds.toFixed(3)}s ${result.exitCode === 0 ? "PASS" : "FAIL"} ${result.file}`)
+}
+
+if (sorted[0]) {
+  console.log(`METRIC slowest_test_file_seconds=${sorted[0].seconds.toFixed(3)}`)
+  console.log(`METRIC profiled_test_files=${results.length}`)
+}
+
+if (results.some((result) => result.exitCode !== 0)) process.exit(1)

+ 14 - 7
packages/opencode/src/cli/cmd/tui/plugin/runtime.ts

@@ -114,6 +114,7 @@ type RuntimeState = {
   plugins: PluginEntry[]
   plugins: PluginEntry[]
   plugins_by_id: Map<string, PluginEntry>
   plugins_by_id: Map<string, PluginEntry>
   pending: Map<string, ConfigPlugin.Origin>
   pending: Map<string, ConfigPlugin.Origin>
+  dispose_timeout_ms: number
 }
 }
 
 
 const log = Log.create({ service: "tui.plugin" })
 const log = Log.create({ service: "tui.plugin" })
@@ -394,7 +395,7 @@ async function syncPluginThemes(plugin: PluginEntry) {
   }
   }
 }
 }
 
 
-function createPluginScope(load: PluginLoad, id: string) {
+function createPluginScope(load: PluginLoad, id: string, disposeTimeoutMs: number) {
   const ctrl = new AbortController()
   const ctrl = new AbortController()
   let list: { key: symbol; fn: TuiDispose }[] = []
   let list: { key: symbol; fn: TuiDispose }[] = []
   let done = false
   let done = false
@@ -436,14 +437,14 @@ function createPluginScope(load: PluginLoad, id: string) {
     ctrl.abort()
     ctrl.abort()
     const queue = [...list].reverse()
     const queue = [...list].reverse()
     list = []
     list = []
-    const until = Date.now() + DISPOSE_TIMEOUT_MS
+    const until = Date.now() + disposeTimeoutMs
     for (const item of queue) {
     for (const item of queue) {
       const left = until - Date.now()
       const left = until - Date.now()
       if (left <= 0) {
       if (left <= 0) {
         fail("timed out cleaning up tui plugin", {
         fail("timed out cleaning up tui plugin", {
           path: load.spec,
           path: load.spec,
           id,
           id,
-          timeout: DISPOSE_TIMEOUT_MS,
+          timeout: disposeTimeoutMs,
         })
         })
         break
         break
       }
       }
@@ -454,7 +455,7 @@ function createPluginScope(load: PluginLoad, id: string) {
         fail("timed out cleaning up tui plugin", {
         fail("timed out cleaning up tui plugin", {
           path: load.spec,
           path: load.spec,
           id,
           id,
-          timeout: DISPOSE_TIMEOUT_MS,
+          timeout: disposeTimeoutMs,
         })
         })
         break
         break
       }
       }
@@ -523,7 +524,7 @@ async function activatePluginEntry(state: RuntimeState, plugin: PluginEntry, per
   if (persist) writePluginEnabledState(state.api, plugin.id, true)
   if (persist) writePluginEnabledState(state.api, plugin.id, true)
   if (plugin.scope) return true
   if (plugin.scope) return true
 
 
-  const scope = createPluginScope(plugin.load, plugin.id)
+  const scope = createPluginScope(plugin.load, plugin.id, state.dispose_timeout_ms)
   const api = pluginApi(state, plugin, scope, plugin.id)
   const api = pluginApi(state, plugin, scope, plugin.id)
   const ok = await Promise.resolve()
   const ok = await Promise.resolve()
     .then(async () => {
     .then(async () => {
@@ -1002,7 +1003,12 @@ let loaded: Promise<void> | undefined
 let runtime: RuntimeState | undefined
 let runtime: RuntimeState | undefined
 export const Slot = View
 export const Slot = View
 
 
-export async function init(input: { api: HostPluginApi; config: TuiConfig.Resolved; dispose?: () => void }) {
+export async function init(input: {
+  api: HostPluginApi
+  config: TuiConfig.Resolved
+  dispose?: () => void
+  disposeTimeoutMs?: number
+}) {
   const cwd = process.cwd()
   const cwd = process.cwd()
   if (loaded) {
   if (loaded) {
     if (dir !== cwd) {
     if (dir !== cwd) {
@@ -1052,7 +1058,7 @@ export async function dispose() {
   state.dispose?.()
   state.dispose?.()
 }
 }
 
 
-async function load(input: { api: Api; config: TuiConfig.Resolved; dispose?: () => void }) {
+async function load(input: { api: Api; config: TuiConfig.Resolved; dispose?: () => void; disposeTimeoutMs?: number }) {
   const { api, config } = input
   const { api, config } = input
   const cwd = process.cwd()
   const cwd = process.cwd()
   const slots = setupSlots(api)
   const slots = setupSlots(api)
@@ -1064,6 +1070,7 @@ async function load(input: { api: Api; config: TuiConfig.Resolved; dispose?: ()
     plugins: [],
     plugins: [],
     plugins_by_id: new Map(),
     plugins_by_id: new Map(),
     pending: new Map(),
     pending: new Map(),
+    dispose_timeout_ms: input.disposeTimeoutMs ?? DISPOSE_TIMEOUT_MS,
   }
   }
   runtime = next
   runtime = next
   try {
   try {

+ 3 - 1
packages/opencode/src/control-plane/workspace.ts

@@ -160,6 +160,7 @@ export interface Interface {
     workspaceID: WorkspaceID,
     workspaceID: WorkspaceID,
     state: Record<string, number>,
     state: Record<string, number>,
     signal?: AbortSignal,
     signal?: AbortSignal,
+    timeout?: number,
   ) => Effect.Effect<void, WaitForSyncError>
   ) => Effect.Effect<void, WaitForSyncError>
   readonly startWorkspaceSyncing: (projectID: ProjectID) => Effect.Effect<void>
   readonly startWorkspaceSyncing: (projectID: ProjectID) => Effect.Effect<void>
 }
 }
@@ -946,12 +947,13 @@ export const layer = Layer.effect(
       workspaceID: WorkspaceID,
       workspaceID: WorkspaceID,
       state: Record<string, number>,
       state: Record<string, number>,
       signal?: AbortSignal,
       signal?: AbortSignal,
+      timeout = TIMEOUT,
     ) {
     ) {
       if (synced(state)) return
       if (synced(state)) return
 
 
       yield* Effect.catch(
       yield* Effect.catch(
         waitEvent({
         waitEvent({
-          timeout: TIMEOUT,
+          timeout,
           signal,
           signal,
           fn(event) {
           fn(event) {
             if (event.workspace !== workspaceID && event.payload.type !== "sync") {
             if (event.workspace !== workspaceID && event.payload.type !== "sync") {

+ 2 - 2
packages/opencode/test/cli/tui/plugin-lifecycle.test.ts

@@ -205,10 +205,10 @@ test(
     const { config, restore } = mockTuiRuntime(tmp.path, [tmp.extra.spec])
     const { config, restore } = mockTuiRuntime(tmp.path, [tmp.extra.spec])
 
 
     try {
     try {
-      await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
+      await TuiPluginRuntime.init({ api: createTuiPluginApi(), config, disposeTimeoutMs: 25 })
 
 
       const done = await new Promise<string>((resolve) => {
       const done = await new Promise<string>((resolve) => {
-        const timer = setTimeout(() => resolve("timeout"), 7000)
+        const timer = setTimeout(() => resolve("timeout"), 500)
         void TuiPluginRuntime.dispose().then(() => {
         void TuiPluginRuntime.dispose().then(() => {
           clearTimeout(timer)
           clearTimeout(timer)
           resolve("done")
           resolve("done")

+ 0 - 3
packages/opencode/test/config/config.test.ts

@@ -1101,9 +1101,6 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
       },
       },
     })
     })
 
 
-    // TODO: this is a hack to wait for backgruounded gitignore
-    await new Promise((resolve) => setTimeout(resolve, 1000))
-
     expect(await Filesystem.exists(path.join(tmp.extra, ".gitignore"))).toBe(true)
     expect(await Filesystem.exists(path.join(tmp.extra, ".gitignore"))).toBe(true)
     expect(await Filesystem.readText(path.join(tmp.extra, ".gitignore"))).toContain("package-lock.json")
     expect(await Filesystem.readText(path.join(tmp.extra, ".gitignore"))).toContain("package-lock.json")
   } finally {
   } finally {

+ 9 - 5
packages/opencode/test/control-plane/workspace.test.ts

@@ -173,8 +173,12 @@ const startWorkspaceSyncingWithFlag = (projectID: ProjectID, experimentalWorkspa
       Effect.provide(workspaceLayer(experimentalWorkspaces)),
       Effect.provide(workspaceLayer(experimentalWorkspaces)),
     ),
     ),
   )
   )
-const waitForWorkspaceSync = (workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) =>
-  runWorkspace(Workspace.Service.use((workspace) => workspace.waitForSync(workspaceID, state, signal)))
+const waitForWorkspaceSync = (
+  workspaceID: WorkspaceID,
+  state: Record<string, number>,
+  signal?: AbortSignal,
+  timeout?: number,
+) => runWorkspace(Workspace.Service.use((workspace) => workspace.waitForSync(workspaceID, state, signal, timeout)))
 
 
 function captureGlobalEvents() {
 function captureGlobalEvents() {
   const events: GlobalEvent[] = []
   const events: GlobalEvent[] = []
@@ -1639,9 +1643,9 @@ describe("workspace waitForSync", () => {
     await withInstance(async () => {
     await withInstance(async () => {
       const sessionID = SessionID.descending("ses_wait_timeout")
       const sessionID = SessionID.descending("ses_wait_timeout")
 
 
-      await expect(waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 })).rejects.toThrow(
-        `Timed out waiting for sync fence: {"${sessionID}":1}`,
-      )
+      await expect(
+        waitForWorkspaceSync(WorkspaceID.ascending("wrk_wait_timeout"), { [sessionID]: 1 }, undefined, 25),
+      ).rejects.toThrow(`Timed out waiting for sync fence: {"${sessionID}":1}`)
     })
     })
   }, 7000)
   }, 7000)
 })
 })

+ 10 - 0
packages/opencode/test/fixture/plugin.ts

@@ -0,0 +1,10 @@
+import { mkdir } from "fs/promises"
+import path from "path"
+
+export async function markPluginDependenciesReady(dir: string) {
+  await mkdir(path.join(dir, "node_modules"), { recursive: true })
+  await Bun.write(
+    path.join(dir, "package-lock.json"),
+    JSON.stringify({ packages: { "": { dependencies: { "@opencode-ai/plugin": "0.0.0" } } } }),
+  )
+}

+ 3 - 3
packages/opencode/test/plugin/install-concurrency.test.ts

@@ -66,7 +66,7 @@ describe("plugin.install.concurrent", () => {
   test("serializes concurrent server config updates across processes", async () => {
   test("serializes concurrent server config updates across processes", async () => {
     await using tmp = await tmpdir()
     await using tmp = await tmpdir()
     const target = await plugin(tmp.path, ["server"])
     const target = await plugin(tmp.path, ["server"])
-    const all = mods("mod-server", 12)
+    const all = mods("mod-server", 6)
 
 
     const out = await Promise.all(
     const out = await Promise.all(
       all.map((mod) =>
       all.map((mod) =>
@@ -89,7 +89,7 @@ describe("plugin.install.concurrent", () => {
   test("serializes concurrent server+tui config updates across processes", async () => {
   test("serializes concurrent server+tui config updates across processes", async () => {
     await using tmp = await tmpdir()
     await using tmp = await tmpdir()
     const target = await plugin(tmp.path, ["server", "tui"])
     const target = await plugin(tmp.path, ["server", "tui"])
-    const all = mods("mod-both", 10)
+    const all = mods("mod-both", 6)
 
 
     const out = await Promise.all(
     const out = await Promise.all(
       all.map((mod) =>
       all.map((mod) =>
@@ -118,7 +118,7 @@ describe("plugin.install.concurrent", () => {
     await fs.mkdir(path.dirname(cfg), { recursive: true })
     await fs.mkdir(path.dirname(cfg), { recursive: true })
     await Bun.write(cfg, JSON.stringify({ plugin: ["seed@1.0.0"] }, null, 2))
     await Bun.write(cfg, JSON.stringify({ plugin: ["seed@1.0.0"] }, null, 2))
 
 
-    const next = mods("mod-json", 8)
+    const next = mods("mod-json", 5)
     const out = await Promise.all(
     const out = await Promise.all(
       next.map((mod) =>
       next.map((mod) =>
         run({
         run({

+ 65 - 70
packages/opencode/test/provider/provider.test.ts

@@ -3,6 +3,7 @@ import { mkdir, unlink } from "fs/promises"
 import path from "path"
 import path from "path"
 
 
 import { disposeAllInstances, tmpdir, withTestInstance } from "../fixture/fixture"
 import { disposeAllInstances, tmpdir, withTestInstance } from "../fixture/fixture"
+import { markPluginDependenciesReady } from "../fixture/plugin"
 import { Global } from "@opencode-ai/core/global"
 import { Global } from "@opencode-ai/core/global"
 import type { InstanceContext } from "../../src/project/instance-context"
 import type { InstanceContext } from "../../src/project/instance-context"
 import { Plugin } from "../../src/plugin/index"
 import { Plugin } from "../../src/plugin/index"
@@ -31,12 +32,12 @@ function rememberEnv(k: string) {
 const set = (ctx: InstanceContext, k: string, v: string) => {
 const set = (ctx: InstanceContext, k: string, v: string) => {
   rememberEnv(k)
   rememberEnv(k)
   process.env[k] = v
   process.env[k] = v
-  return env.runSync((svc) => svc.set(k, v).pipe(Effect.provideService(InstanceRef, ctx)))
+  return env.runPromise((svc) => svc.set(k, v).pipe(Effect.provideService(InstanceRef, ctx)))
 }
 }
 const remove = (ctx: InstanceContext, k: string) => {
 const remove = (ctx: InstanceContext, k: string) => {
   rememberEnv(k)
   rememberEnv(k)
   delete process.env[k]
   delete process.env[k]
-  return env.runSync((svc) => svc.remove(k).pipe(Effect.provideService(InstanceRef, ctx)))
+  return env.runPromise((svc) => svc.remove(k).pipe(Effect.provideService(InstanceRef, ctx)))
 }
 }
 
 
 afterEach(async () => {
 afterEach(async () => {
@@ -96,14 +97,6 @@ async function defaultModel(ctx: InstanceContext) {
   return run(ctx, (provider) => provider.defaultModel())
   return run(ctx, (provider) => provider.defaultModel())
 }
 }
 
 
-async function markPluginDependenciesReady(dir: string) {
-  await mkdir(path.join(dir, "node_modules"), { recursive: true })
-  await Bun.write(
-    path.join(dir, "package-lock.json"),
-    JSON.stringify({ packages: { "": { dependencies: { "@opencode-ai/plugin": "0.0.0" } } } }),
-  )
-}
-
 function paid(providers: Awaited<ReturnType<typeof list>>) {
 function paid(providers: Awaited<ReturnType<typeof list>>) {
   const item = providers[ProviderID.make("opencode")]
   const item = providers[ProviderID.make("opencode")]
   expect(item).toBeDefined()
   expect(item).toBeDefined()
@@ -149,7 +142,7 @@ test("provider loaded from env variable", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       // Provider should retain its connection source even if custom loaders
       // Provider should retain its connection source even if custom loaders
@@ -202,7 +195,7 @@ test("disabled_providers excludes provider", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeUndefined()
       expect(providers[ProviderID.anthropic]).toBeUndefined()
     },
     },
@@ -224,8 +217,8 @@ test("enabled_providers restricts to only listed providers", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
-      set(ctx, "OPENAI_API_KEY", "test-openai-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "OPENAI_API_KEY", "test-openai-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.openai]).toBeUndefined()
       expect(providers[ProviderID.openai]).toBeUndefined()
@@ -252,7 +245,7 @@ test("model whitelist filters models for provider", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       const models = Object.keys(providers[ProviderID.anthropic].models)
       const models = Object.keys(providers[ProviderID.anthropic].models)
@@ -281,7 +274,7 @@ test("model blacklist excludes specific models", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       const models = Object.keys(providers[ProviderID.anthropic].models)
       const models = Object.keys(providers[ProviderID.anthropic].models)
@@ -314,7 +307,7 @@ test("custom model alias via config", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined()
       expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined()
@@ -469,7 +462,7 @@ test("env variable takes precedence, config merges options", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "env-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "env-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       // Config options should be merged
       // Config options should be merged
@@ -493,7 +486,7 @@ test("getModel returns model for valid provider/model", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
       const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
       expect(model).toBeDefined()
       expect(model).toBeDefined()
       expect(String(model.providerID)).toBe("anthropic")
       expect(String(model.providerID)).toBe("anthropic")
@@ -518,7 +511,7 @@ test("getModel throws ModelNotFoundError for invalid model", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"), ctx)).rejects.toThrow()
       expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"), ctx)).rejects.toThrow()
     },
     },
   })
   })
@@ -569,7 +562,7 @@ test("defaultModel returns first available model when no config set", async () =
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const model = await defaultModel(ctx)
       const model = await defaultModel(ctx)
       expect(model.providerID).toBeDefined()
       expect(model.providerID).toBeDefined()
       expect(model.modelID).toBeDefined()
       expect(model.modelID).toBeDefined()
@@ -592,7 +585,7 @@ test("defaultModel respects config model setting", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const model = await defaultModel(ctx)
       const model = await defaultModel(ctx)
       expect(String(model.providerID)).toBe("anthropic")
       expect(String(model.providerID)).toBe("anthropic")
       expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
       expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
@@ -725,7 +718,7 @@ test("closest finds model by partial match", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const result = await closest(ProviderID.anthropic, ["sonnet-4"], ctx)
       const result = await closest(ProviderID.anthropic, ["sonnet-4"], ctx)
       expect(result).toBeDefined()
       expect(result).toBeDefined()
       expect(String(result?.providerID)).toBe("anthropic")
       expect(String(result?.providerID)).toBe("anthropic")
@@ -778,7 +771,7 @@ test("getModel uses realIdByKey for aliased models", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
       expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
 
 
@@ -891,7 +884,7 @@ test("model inherits properties from existing database model", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.name).toBe("Custom Name for Sonnet")
       expect(model.name).toBe("Custom Name for Sonnet")
@@ -917,7 +910,7 @@ test("disabled_providers prevents loading even with env var", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "OPENAI_API_KEY", "test-openai-key")
+      await set(ctx, "OPENAI_API_KEY", "test-openai-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.openai]).toBeUndefined()
       expect(providers[ProviderID.openai]).toBeUndefined()
     },
     },
@@ -939,8 +932,8 @@ test("enabled_providers with empty array allows no providers", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
-      set(ctx, "OPENAI_API_KEY", "test-openai-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "OPENAI_API_KEY", "test-openai-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(Object.keys(providers).length).toBe(0)
       expect(Object.keys(providers).length).toBe(0)
     },
     },
@@ -967,7 +960,7 @@ test("whitelist and blacklist can be combined", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       const models = Object.keys(providers[ProviderID.anthropic].models)
       const models = Object.keys(providers[ProviderID.anthropic].models)
@@ -1074,7 +1067,7 @@ test("getSmallModel returns appropriate small model", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const model = await getSmallModel(ProviderID.anthropic, ctx)
       const model = await getSmallModel(ProviderID.anthropic, ctx)
       expect(model).toBeDefined()
       expect(model).toBeDefined()
       expect(model?.id).toContain("haiku")
       expect(model?.id).toContain("haiku")
@@ -1097,7 +1090,7 @@ test("getSmallModel respects config small_model override", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const model = await getSmallModel(ProviderID.anthropic, ctx)
       const model = await getSmallModel(ProviderID.anthropic, ctx)
       expect(model).toBeDefined()
       expect(model).toBeDefined()
       expect(String(model?.providerID)).toBe("anthropic")
       expect(String(model?.providerID)).toBe("anthropic")
@@ -1121,7 +1114,7 @@ test("getSmallModel ignores invalid config small_model", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       expect(await getSmallModel(ProviderID.anthropic, ctx)).toBeUndefined()
       expect(await getSmallModel(ProviderID.anthropic, ctx)).toBeUndefined()
     },
     },
   })
   })
@@ -1164,8 +1157,8 @@ test("multiple providers can be configured simultaneously", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
-      set(ctx, "OPENAI_API_KEY", "test-openai-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
+      await set(ctx, "OPENAI_API_KEY", "test-openai-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.openai]).toBeDefined()
       expect(providers[ProviderID.openai]).toBeDefined()
@@ -1241,7 +1234,7 @@ test("model alias name defaults to alias key when id differs", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
       expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
     },
     },
@@ -1279,7 +1272,7 @@ test("provider with multiple env var options only includes apiKey when single en
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "MULTI_ENV_KEY_1", "test-key")
+      await set(ctx, "MULTI_ENV_KEY_1", "test-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.make("multi-env")]).toBeDefined()
       expect(providers[ProviderID.make("multi-env")]).toBeDefined()
       // When multiple env options exist, key should NOT be auto-set
       // When multiple env options exist, key should NOT be auto-set
@@ -1319,7 +1312,7 @@ test("provider with single env var includes apiKey automatically", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "SINGLE_ENV_KEY", "my-api-key")
+      await set(ctx, "SINGLE_ENV_KEY", "my-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.make("single-env")]).toBeDefined()
       expect(providers[ProviderID.make("single-env")]).toBeDefined()
       // Single env option should auto-set key
       // Single env option should auto-set key
@@ -1354,7 +1347,7 @@ test("model cost overrides existing cost values", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.cost.input).toBe(999)
       expect(model.cost.input).toBe(999)
@@ -1431,9 +1424,9 @@ test("disabled_providers and enabled_providers interaction", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-anthropic")
-      set(ctx, "OPENAI_API_KEY", "test-openai")
-      set(ctx, "GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-anthropic")
+      await set(ctx, "OPENAI_API_KEY", "test-openai")
+      await set(ctx, "GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
       const providers = await list(ctx)
       const providers = await list(ctx)
       // anthropic: in enabled, not in disabled = allowed
       // anthropic: in enabled, not in disabled = allowed
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
@@ -1588,7 +1581,7 @@ test("provider env fallback - second env var used if first missing", async () =>
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
       // Only set fallback, not primary
       // Only set fallback, not primary
-      set(ctx, "FALLBACK_KEY", "fallback-api-key")
+      await set(ctx, "FALLBACK_KEY", "fallback-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       // Provider should load because fallback env var is set
       // Provider should load because fallback env var is set
       expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
       expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
@@ -1610,7 +1603,7 @@ test("getModel returns consistent results", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
       const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
       const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
       const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
       expect(model1.providerID).toEqual(model2.providerID)
       expect(model1.providerID).toEqual(model2.providerID)
@@ -1669,7 +1662,7 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       try {
       try {
         await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4"), ctx) // typo: sonet instead of sonnet
         await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4"), ctx) // typo: sonet instead of sonnet
         expect(true).toBe(false) // Should not reach here
         expect(true).toBe(false) // Should not reach here
@@ -1695,7 +1688,7 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       try {
       try {
         await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4"), ctx) // typo: antropic
         await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4"), ctx) // typo: antropic
         expect(true).toBe(false) // Should not reach here
         expect(true).toBe(false) // Should not reach here
@@ -1721,7 +1714,7 @@ test("ModelNotFoundError suggests catalog models for unloaded providers", async
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      remove(ctx, "OPENCODE_API_KEY")
+      await remove(ctx, "OPENCODE_API_KEY")
       try {
       try {
         await getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model"), ctx)
         await getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model"), ctx)
         throw new Error("expected model lookup to fail")
         throw new Error("expected model lookup to fail")
@@ -1767,7 +1760,7 @@ test("getProvider returns provider info", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const provider = await getProvider(ProviderID.anthropic, ctx)
       const provider = await getProvider(ProviderID.anthropic, ctx)
       expect(provider).toBeDefined()
       expect(provider).toBeDefined()
       expect(String(provider?.id)).toBe("anthropic")
       expect(String(provider?.id)).toBe("anthropic")
@@ -1789,7 +1782,7 @@ test("closest returns undefined when no partial match found", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"], ctx)
       const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"], ctx)
       expect(result).toBeUndefined()
       expect(result).toBeUndefined()
     },
     },
@@ -1810,7 +1803,7 @@ test("closest checks multiple query terms in order", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       // First term won't match, second will
       // First term won't match, second will
       const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"], ctx)
       const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"], ctx)
       expect(result).toBeDefined()
       expect(result).toBeDefined()
@@ -1880,7 +1873,7 @@ test("provider options are deeply merged", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       // Custom options should be merged
       // Custom options should be merged
       expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
       expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
@@ -2010,7 +2003,7 @@ test("custom model inherits npm package from models.dev provider config", async
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "OPENAI_API_KEY", "test-api-key")
+      await set(ctx, "OPENAI_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.openai].models["my-custom-model"]
       const model = providers[ProviderID.openai].models["my-custom-model"]
       expect(model).toBeDefined()
       expect(model).toBeDefined()
@@ -2043,7 +2036,7 @@ test("custom model inherits api.url from models.dev provider", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "OPENROUTER_API_KEY", "test-api-key")
+      await set(ctx, "OPENROUTER_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.openrouter]).toBeDefined()
       expect(providers[ProviderID.openrouter]).toBeDefined()
 
 
@@ -2174,7 +2167,7 @@ test("model variants are generated for reasoning models", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       // Claude sonnet 4 has reasoning capability
       // Claude sonnet 4 has reasoning capability
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
@@ -2210,7 +2203,7 @@ test("model variants can be disabled via config", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants).toBeDefined()
       expect(model.variants).toBeDefined()
@@ -2251,7 +2244,7 @@ test("model variants can be customized via config", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants!["high"]).toBeDefined()
       expect(model.variants!["high"]).toBeDefined()
@@ -2288,7 +2281,7 @@ test("disabled key is stripped from variant config", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants!["max"]).toBeDefined()
       expect(model.variants!["max"]).toBeDefined()
@@ -2324,7 +2317,7 @@ test("all variants can be disabled via config", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants).toBeDefined()
       expect(model.variants).toBeDefined()
@@ -2360,7 +2353,7 @@ test("variant config merges with generated variants", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
       expect(model.variants!["high"]).toBeDefined()
       expect(model.variants!["high"]).toBeDefined()
@@ -2396,7 +2389,7 @@ test("variants filtered in second pass for database models", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "OPENAI_API_KEY", "test-api-key")
+      await set(ctx, "OPENAI_API_KEY", "test-api-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.openai].models["gpt-5"]
       const model = providers[ProviderID.openai].models["gpt-5"]
       expect(model.variants).toBeDefined()
       expect(model.variants).toBeDefined()
@@ -2498,7 +2491,7 @@ test("Google Vertex: retains baseURL for custom proxy", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
+      await set(ctx, "GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
       expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
       expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
       expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
@@ -2541,7 +2534,7 @@ test("Google Vertex: supports OpenAI compatible models", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
+      await set(ctx, "GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
       const providers = await list(ctx)
       const providers = await list(ctx)
       const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
       const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
 
 
@@ -2565,9 +2558,9 @@ test("cloudflare-ai-gateway loads with env variables", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
-      set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
-      set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
+      await set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
+      await set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
+      await set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
       expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
     },
     },
@@ -2595,9 +2588,9 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
-      set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
-      set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
+      await set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
+      await set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
+      await set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
       expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
       expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
       expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
@@ -2673,8 +2666,10 @@ test("plugin config providers persist after instance dispose", async () => {
 test("plugin config enabled and disabled providers are honored", async () => {
 test("plugin config enabled and disabled providers are honored", async () => {
   await using tmp = await tmpdir({
   await using tmp = await tmpdir({
     init: async (dir) => {
     init: async (dir) => {
-      const root = path.join(dir, ".opencode", "plugin")
+      const configDir = path.join(dir, ".opencode")
+      const root = path.join(configDir, "plugin")
       await mkdir(root, { recursive: true })
       await mkdir(root, { recursive: true })
+      await markPluginDependenciesReady(configDir)
       await Bun.write(
       await Bun.write(
         path.join(root, "provider-filter.ts"),
         path.join(root, "provider-filter.ts"),
         [
         [
@@ -2696,8 +2691,8 @@ test("plugin config enabled and disabled providers are honored", async () => {
   await withTestInstance({
   await withTestInstance({
     directory: tmp.path,
     directory: tmp.path,
     fn: async (ctx) => {
     fn: async (ctx) => {
-      set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
-      set(ctx, "OPENAI_API_KEY", "test-openai-key")
+      await set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
+      await set(ctx, "OPENAI_API_KEY", "test-openai-key")
       const providers = await list(ctx)
       const providers = await list(ctx)
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.anthropic]).toBeDefined()
       expect(providers[ProviderID.openai]).toBeUndefined()
       expect(providers[ProviderID.openai]).toBeUndefined()

+ 25 - 45
packages/opencode/test/server/httpapi-listen.test.ts

@@ -169,7 +169,7 @@ async function openPtySocket(listener: Awaited<ReturnType<typeof startListener>>
 
 
 describe("HttpApi Server.listen", () => {
 describe("HttpApi Server.listen", () => {
   testPty("serves HTTP routes and upgrades PTY websocket through Server.listen", async () => {
   testPty("serves HTTP routes and upgrades PTY websocket through Server.listen", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
+    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
     const listener = await startListener()
     const listener = await startListener()
     let stopped = false
     let stopped = false
     try {
     try {
@@ -330,7 +330,7 @@ describe("HttpApi Server.listen", () => {
   })
   })
 
 
   testPty("rejects unsafe PTY ticket mint and connect requests", async () => {
   testPty("rejects unsafe PTY ticket mint and connect requests", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
+    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
     const listener = await startListener()
     const listener = await startListener()
     try {
     try {
       const info = await createCat(listener, tmp.path)
       const info = await createCat(listener, tmp.path)
@@ -338,49 +338,15 @@ describe("HttpApi Server.listen", () => {
       expect((await requestTicket(listener, info.id, tmp.path, { ticketHeader: false })).status).toBe(403)
       expect((await requestTicket(listener, info.id, tmp.path, { ticketHeader: false })).status).toBe(403)
       expect((await requestTicket(listener, info.id, tmp.path, { origin: "https://evil.example" })).status).toBe(403)
       expect((await requestTicket(listener, info.id, tmp.path, { origin: "https://evil.example" })).status).toBe(403)
 
 
-      await expectSocketRejected(socketURL(listener, info.id, tmp.path, "not-a-ticket"))
-
-      const reusable = await connectTicket(listener, info.id, tmp.path)
-      const ws = await openSocket(socketURL(listener, info.id, tmp.path, reusable.ticket))
-      await expectSocketRejected(socketURL(listener, info.id, tmp.path, reusable.ticket))
-      ws.close(1000)
-
-      const other = await createCat(listener, tmp.path)
-      const scoped = await connectTicket(listener, info.id, tmp.path)
-      await expectSocketRejected(socketURL(listener, other.id, tmp.path, scoped.ticket))
-
-      const crossOrigin = await connectTicket(listener, info.id, tmp.path)
-      await expectSocketRejected(socketURL(listener, info.id, tmp.path, crossOrigin.ticket), {
-        headers: { origin: "https://evil.example" },
-      })
-    } finally {
-      await stop(listener, "timed out cleaning up rejected ticket listener").catch(() => undefined)
-    }
-  })
-
-  // Regression for #25698 (Ope): the app's SDK call to
-  // `client.pty.connectToken({ ptyID })` originally omitted `directory`, so
-  // the server resolved the PTY in its own cwd context — where the project
-  // PTY isn't registered — and returned 404. The fix is to always pass
-  // `directory` from the app side; this test locks in two contracts:
-  //   1. Mint without directory cannot find a PTY registered in another dir.
-  //   2. Mint with the project directory succeeds; the resulting ticket
-  //      consumes cleanly when the WS upgrade carries the same directory.
-  testPty("PTY connect token requires matching directory across mint and connect", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
-    const listener = await startListener()
-    try {
-      const info = await createCat(listener, tmp.path)
-
-      // Mint without directory — server uses its own cwd, can't find the PTY.
+      // Regression for #25698: minting without a directory uses the server cwd
+      // and cannot find a PTY registered in a project directory.
       const ambiguous = await fetch(new URL(PtyPaths.connectToken.replace(":ptyID", info.id), listener.url), {
       const ambiguous = await fetch(new URL(PtyPaths.connectToken.replace(":ptyID", info.id), listener.url), {
         method: "POST",
         method: "POST",
         headers: { authorization: authorization(), "x-opencode-ticket": "1" },
         headers: { authorization: authorization(), "x-opencode-ticket": "1" },
       })
       })
       expect(ambiguous.status).toBe(404)
       expect(ambiguous.status).toBe(404)
 
 
-      // Mint with the project directory — succeeds, ticket binds to that scope.
-      const scoped = await fetch(
+      const directoryScoped = await fetch(
         new URL(
         new URL(
           `${PtyPaths.connectToken.replace(":ptyID", info.id)}?directory=${encodeURIComponent(tmp.path)}`,
           `${PtyPaths.connectToken.replace(":ptyID", info.id)}?directory=${encodeURIComponent(tmp.path)}`,
           listener.url,
           listener.url,
@@ -390,19 +356,33 @@ describe("HttpApi Server.listen", () => {
           headers: { authorization: authorization(), "x-opencode-ticket": "1" },
           headers: { authorization: authorization(), "x-opencode-ticket": "1" },
         },
         },
       )
       )
-      expect(scoped.status).toBe(200)
-      const mint = (await scoped.json()) as { ticket: string }
+      expect(directoryScoped.status).toBe(200)
+      const mint = (await directoryScoped.json()) as { ticket: string }
+      const scopedWs = await openSocket(socketURL(listener, info.id, tmp.path, mint.ticket))
+      scopedWs.close(1000)
 
 
-      // Same directory on the WS upgrade → consume succeeds.
-      const ws = await openSocket(socketURL(listener, info.id, tmp.path, mint.ticket))
+      await expectSocketRejected(socketURL(listener, info.id, tmp.path, "not-a-ticket"))
+
+      const reusable = await connectTicket(listener, info.id, tmp.path)
+      const ws = await openSocket(socketURL(listener, info.id, tmp.path, reusable.ticket))
+      await expectSocketRejected(socketURL(listener, info.id, tmp.path, reusable.ticket))
       ws.close(1000)
       ws.close(1000)
+
+      const other = await createCat(listener, tmp.path)
+      const scoped = await connectTicket(listener, info.id, tmp.path)
+      await expectSocketRejected(socketURL(listener, other.id, tmp.path, scoped.ticket))
+
+      const crossOrigin = await connectTicket(listener, info.id, tmp.path)
+      await expectSocketRejected(socketURL(listener, info.id, tmp.path, crossOrigin.ticket), {
+        headers: { origin: "https://evil.example" },
+      })
     } finally {
     } finally {
-      await stop(listener, "timed out cleaning up directory-scope listener").catch(() => undefined)
+      await stop(listener, "timed out cleaning up rejected ticket listener").catch(() => undefined)
     }
     }
   })
   })
 
 
   testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => {
   testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => {
-    await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
+    await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
     const listener = await startNoAuthListener()
     const listener = await startNoAuthListener()
     try {
     try {
       const info = await createCat(listener, tmp.path)
       const info = await createCat(listener, tmp.path)

+ 5 - 0
packages/opencode/test/server/httpapi-provider.test.ts

@@ -6,6 +6,7 @@ import { Server } from "../../src/server/server"
 import * as Log from "@opencode-ai/core/util/log"
 import * as Log from "@opencode-ai/core/util/log"
 import { resetDatabase } from "../fixture/db"
 import { resetDatabase } from "../fixture/db"
 import { TestInstance } from "../fixture/fixture"
 import { TestInstance } from "../fixture/fixture"
+import { markPluginDependenciesReady } from "../fixture/plugin"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
 
 
 void Log.init({ print: false })
 void Log.init({ print: false })
@@ -118,6 +119,7 @@ function requestCallback(input: {
 function writeProviderAuthPlugin(dir: string) {
 function writeProviderAuthPlugin(dir: string) {
   return Effect.gen(function* () {
   return Effect.gen(function* () {
     const fs = yield* AppFileSystem.Service
     const fs = yield* AppFileSystem.Service
+    yield* Effect.promise(() => markPluginDependenciesReady(path.join(dir, ".opencode")))
 
 
     yield* fs.writeWithDirs(
     yield* fs.writeWithDirs(
       path.join(dir, ".opencode", "plugin", "provider-oauth-parity.ts"),
       path.join(dir, ".opencode", "plugin", "provider-oauth-parity.ts"),
@@ -152,6 +154,7 @@ function writeProviderAuthPlugin(dir: string) {
 function writeProviderAuthValidationPlugin(dir: string) {
 function writeProviderAuthValidationPlugin(dir: string) {
   return Effect.gen(function* () {
   return Effect.gen(function* () {
     const fs = yield* AppFileSystem.Service
     const fs = yield* AppFileSystem.Service
+    yield* Effect.promise(() => markPluginDependenciesReady(path.join(dir, ".opencode")))
 
 
     yield* fs.writeWithDirs(
     yield* fs.writeWithDirs(
       path.join(dir, ".opencode", "plugin", "provider-oauth-validation.ts"),
       path.join(dir, ".opencode", "plugin", "provider-oauth-validation.ts"),
@@ -193,6 +196,7 @@ function writeProviderAuthValidationPlugin(dir: string) {
 function writeFunctionOptionsPlugin(dir: string) {
 function writeFunctionOptionsPlugin(dir: string) {
   return Effect.gen(function* () {
   return Effect.gen(function* () {
     const fs = yield* AppFileSystem.Service
     const fs = yield* AppFileSystem.Service
+    yield* Effect.promise(() => markPluginDependenciesReady(path.join(dir, ".opencode")))
 
 
     yield* fs.writeWithDirs(
     yield* fs.writeWithDirs(
       path.join(dir, ".opencode", "plugin", "provider-function-options.ts"),
       path.join(dir, ".opencode", "plugin", "provider-function-options.ts"),
@@ -224,6 +228,7 @@ function writeFunctionOptionsPlugin(dir: string) {
 function writeProviderModelsMutationPlugin(dir: string) {
 function writeProviderModelsMutationPlugin(dir: string) {
   return Effect.gen(function* () {
   return Effect.gen(function* () {
     const fs = yield* AppFileSystem.Service
     const fs = yield* AppFileSystem.Service
+    yield* Effect.promise(() => markPluginDependenciesReady(path.join(dir, ".opencode")))
 
 
     yield* fs.writeWithDirs(
     yield* fs.writeWithDirs(
       path.join(dir, ".opencode", "plugin", "provider-models-mutation.ts"),
       path.join(dir, ".opencode", "plugin", "provider-models-mutation.ts"),

+ 4 - 3
packages/opencode/test/server/httpapi-sdk.test.ts

@@ -265,7 +265,7 @@ function withProject<A, E, E2 = never>(
 ) {
 ) {
   return Effect.gen(function* () {
   return Effect.gen(function* () {
     const directory = yield* tmpdirScoped({
     const directory = yield* tmpdirScoped({
-      git: options.git ?? true,
+      git: options.git ?? false,
       config: { formatter: false, lsp: false, ...options.config },
       config: { formatter: false, lsp: false, ...options.config },
     })
     })
     yield* options.setup?.(directory) ?? Effect.void
     yield* options.setup?.(directory) ?? Effect.void
@@ -516,7 +516,7 @@ describe("HttpApi SDK", () => {
   )
   )
 
 
   serverPathParity("matches generated SDK instance read routes", (serverPath) =>
   serverPathParity("matches generated SDK instance read routes", (serverPath) =>
-    withStandardProject(serverPath, ({ sdk, directory }) =>
+    withProject(serverPath, { git: true, setup: writeStandardFiles }, ({ sdk, directory }) =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const project = yield* capture(() => sdk.project.current())
         const project = yield* capture(() => sdk.project.current())
         const projects = yield* capture(() => sdk.project.list())
         const projects = yield* capture(() => sdk.project.list())
@@ -561,6 +561,7 @@ describe("HttpApi SDK", () => {
           foundFile: JSON.stringify(findFiles.data).includes("hello.txt"),
           foundFile: JSON.stringify(findFiles.data).includes("hello.txt"),
           foundText: JSON.stringify(findText.data ?? null).includes("sdk-parity"),
           foundText: JSON.stringify(findText.data ?? null).includes("sdk-parity"),
           listedFile: JSON.stringify(files.data).includes("hello.txt"),
           listedFile: JSON.stringify(files.data).includes("hello.txt"),
+          vcs: { hasBranch: typeof record(vcs.data).branch === "string" },
         }
         }
       }),
       }),
     ),
     ),
@@ -887,7 +888,7 @@ describe("HttpApi SDK", () => {
   )
   )
 
 
   serverPathParity("matches generated SDK project git initialization", (serverPath) =>
   serverPathParity("matches generated SDK project git initialization", (serverPath) =>
-    withProject(serverPath, { git: false }, ({ sdk, directory }) =>
+    withProject(serverPath, {}, ({ sdk, directory }) =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const before = yield* capture(() => sdk.project.current())
         const before = yield* capture(() => sdk.project.current())
         const init = yield* capture(() => sdk.project.initGit())
         const init = yield* capture(() => sdk.project.initGit())

+ 12 - 12
packages/opencode/test/session/processor-effect.test.ts

@@ -249,7 +249,7 @@ it.live("session.processor effect tests capture llm input cleanly", () =>
         expect(calls).toBe(1)
         expect(calls).toBe(1)
         expect(parts.some((part) => part.type === "text" && part.text === "hello")).toBe(true)
         expect(parts.some((part) => part.type === "text" && part.text === "hello")).toBe(true)
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -331,7 +331,7 @@ it.live("session.processor effect tests preserve text start time", () =>
         if (!text?.time?.start || !text.time.end) return
         if (!text?.time?.start || !text.time.end) return
         expect(text.time.start).toBeLessThan(text.time.end)
         expect(text.time.start).toBeLessThan(text.time.end)
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -377,7 +377,7 @@ it.live("session.processor effect tests stop after token overflow requests compa
         expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true)
         expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true)
         expect(parts.some((part) => part.type === "step-finish")).toBe(true)
         expect(parts.some((part) => part.type === "step-finish")).toBe(true)
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -425,7 +425,7 @@ it.live("session.processor effect tests capture reasoning from http mock", () =>
         expect(reasoning?.text).toBe("think")
         expect(reasoning?.text).toBe("think")
         expect(text?.text).toBe("done")
         expect(text?.text).toBe("done")
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -472,7 +472,7 @@ it.live("session.processor effect tests reset reasoning state across retries", (
         expect(reasoning.some((part) => part.text === "two")).toBe(true)
         expect(reasoning.some((part) => part.text === "two")).toBe(true)
         expect(reasoning.some((part) => part.text === "onetwo")).toBe(false)
         expect(reasoning.some((part) => part.text === "onetwo")).toBe(false)
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -515,7 +515,7 @@ it.live("session.processor effect tests do not retry unknown json errors", () =>
         expect(yield* llm.calls).toBe(1)
         expect(yield* llm.calls).toBe(1)
         expect(handle.message.error?.name).toBe("APIError")
         expect(handle.message.error?.name).toBe("APIError")
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -562,7 +562,7 @@ it.live("session.processor effect tests retry recognized structured json errors"
         expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true)
         expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true)
         expect(handle.message.error).toBeUndefined()
         expect(handle.message.error).toBeUndefined()
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -614,7 +614,7 @@ it.live("session.processor effect tests publish retry status updates", () =>
         expect(yield* llm.calls).toBe(2)
         expect(yield* llm.calls).toBe(2)
         expect(states).toStrictEqual([1])
         expect(states).toStrictEqual([1])
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -657,7 +657,7 @@ it.live("session.processor effect tests compact on structured context overflow",
         expect(yield* llm.calls).toBe(1)
         expect(yield* llm.calls).toBe(1)
         expect(handle.message.error).toBeUndefined()
         expect(handle.message.error).toBeUndefined()
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -721,7 +721,7 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
           expect(call.state.time.end).toBeDefined()
           expect(call.state.time.end).toBeDefined()
         }
         }
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -793,7 +793,7 @@ it.live("session.processor effect tests record aborted errors and idle state", (
         expect(state).toMatchObject({ type: "idle" })
         expect(state).toMatchObject({ type: "idle" })
         expect(errs).toContain("MessageAbortedError")
         expect(errs).toContain("MessageAbortedError")
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )
 
 
@@ -850,6 +850,6 @@ it.live("session.processor effect tests mark interruptions aborted without manua
         }
         }
         expect(state).toMatchObject({ type: "idle" })
         expect(state).toMatchObject({ type: "idle" })
       }),
       }),
-    { git: true, config: (url) => providerCfg(url) },
+    { config: (url) => providerCfg(url) },
   ),
   ),
 )
 )

+ 303 - 359
packages/opencode/test/session/prompt.test.ts

@@ -433,317 +433,290 @@ const boot = Effect.fn("test.boot")(function* (input?: { title?: string }) {
 
 
 // Loop semantics
 // Loop semantics
 
 
-it.instance(
-  "loop exits immediately when last assistant has stop finish",
-  () =>
-    Effect.gen(function* () {
-      const { llm } = yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const chat = yield* sessions.create({ title: "Pinned" })
-      yield* seed(chat.id, { finish: "stop" })
-
-      const result = yield* prompt.loop({ sessionID: chat.id })
-      expect(result.info.role).toBe("assistant")
-      if (result.info.role === "assistant") expect(result.info.finish).toBe("stop")
-      expect(yield* llm.calls).toBe(0)
-    }),
-  { git: true },
+it.instance("loop exits immediately when last assistant has stop finish", () =>
+  Effect.gen(function* () {
+    const { llm } = yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const chat = yield* sessions.create({ title: "Pinned" })
+    yield* seed(chat.id, { finish: "stop" })
+
+    const result = yield* prompt.loop({ sessionID: chat.id })
+    expect(result.info.role).toBe("assistant")
+    if (result.info.role === "assistant") expect(result.info.finish).toBe("stop")
+    expect(yield* llm.calls).toBe(0)
+  }),
 )
 )
 
 
-it.instance(
-  "loop calls LLM and returns assistant message",
-  () =>
-    Effect.gen(function* () {
-      const { llm } = yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const chat = yield* sessions.create({
-        title: "Pinned",
-        permission: [{ permission: "*", pattern: "*", action: "allow" }],
-      })
-      yield* prompt.prompt({
-        sessionID: chat.id,
-        agent: "build",
-        noReply: true,
-        parts: [{ type: "text", text: "hello" }],
-      })
-      yield* llm.text("world")
+it.instance("loop calls LLM and returns assistant message", () =>
+  Effect.gen(function* () {
+    const { llm } = yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const chat = yield* sessions.create({
+      title: "Pinned",
+      permission: [{ permission: "*", pattern: "*", action: "allow" }],
+    })
+    yield* prompt.prompt({
+      sessionID: chat.id,
+      agent: "build",
+      noReply: true,
+      parts: [{ type: "text", text: "hello" }],
+    })
+    yield* llm.text("world")
 
 
-      const result = yield* prompt.loop({ sessionID: chat.id })
-      expect(result.info.role).toBe("assistant")
-      const parts = result.parts.filter((p) => p.type === "text")
-      expect(parts.some((p) => p.type === "text" && p.text === "world")).toBe(true)
-      expect(yield* llm.hits).toHaveLength(1)
-    }),
-  { git: true },
+    const result = yield* prompt.loop({ sessionID: chat.id })
+    expect(result.info.role).toBe("assistant")
+    const parts = result.parts.filter((p) => p.type === "text")
+    expect(parts.some((p) => p.type === "text" && p.text === "world")).toBe(true)
+    expect(yield* llm.hits).toHaveLength(1)
+  }),
 )
 )
 
 
-it.instance(
-  "prompt emits v2 prompted and synthetic events",
-  () =>
-    Effect.gen(function* () {
-      yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const chat = yield* sessions.create({ title: "Pinned" })
-
-      yield* prompt.prompt({
-        sessionID: chat.id,
-        agent: "build",
-        noReply: true,
-        parts: [
-          { type: "text", text: "hello v2" },
-          {
-            type: "file",
-            mime: "text/plain",
-            filename: "note.txt",
-            url: "data:text/plain;base64,bm90ZSBjb250ZW50",
-          },
-        ],
-      })
+it.instance("prompt emits v2 prompted and synthetic events", () =>
+  Effect.gen(function* () {
+    yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const chat = yield* sessions.create({ title: "Pinned" })
+
+    yield* prompt.prompt({
+      sessionID: chat.id,
+      agent: "build",
+      noReply: true,
+      parts: [
+        { type: "text", text: "hello v2" },
+        {
+          type: "file",
+          mime: "text/plain",
+          filename: "note.txt",
+          url: "data:text/plain;base64,bm90ZSBjb250ZW50",
+        },
+      ],
+    })
 
 
-      const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
-        Effect.provide(SessionV2.layer),
-      )
-      const row = Database.use((db) =>
-        db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(),
-      )
-      expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
-      expect(typeof row?.data.time.created).toBe("number")
-      expect(messages).toEqual(
-        expect.arrayContaining([
-          expect.objectContaining({ type: "synthetic", text: expect.stringContaining("Called the Read tool") }),
-          expect.objectContaining({ type: "synthetic", text: "note content" }),
-        ]),
-      )
-    }),
-  { git: true },
+    const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
+      Effect.provide(SessionV2.layer),
+    )
+    const row = Database.use((db) =>
+      db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(),
+    )
+    expect(messages.find((message) => message.type === "user")).toMatchObject({ type: "user", text: "hello v2" })
+    expect(typeof row?.data.time.created).toBe("number")
+    expect(messages).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ type: "synthetic", text: expect.stringContaining("Called the Read tool") }),
+        expect.objectContaining({ type: "synthetic", text: "note content" }),
+      ]),
+    )
+  }),
 )
 )
 
 
-it.instance(
-  "static loop returns assistant text through local provider",
-  () =>
-    Effect.gen(function* () {
-      const { llm } = yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const session = yield* sessions.create({
-        title: "Prompt provider",
-        permission: [{ permission: "*", pattern: "*", action: "allow" }],
-      })
+it.instance("static loop returns assistant text through local provider", () =>
+  Effect.gen(function* () {
+    const { llm } = yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const session = yield* sessions.create({
+      title: "Prompt provider",
+      permission: [{ permission: "*", pattern: "*", action: "allow" }],
+    })
 
 
-      yield* prompt.prompt({
-        sessionID: session.id,
-        agent: "build",
-        noReply: true,
-        parts: [{ type: "text", text: "hello" }],
-      })
+    yield* prompt.prompt({
+      sessionID: session.id,
+      agent: "build",
+      noReply: true,
+      parts: [{ type: "text", text: "hello" }],
+    })
 
 
-      yield* llm.text("world")
+    yield* llm.text("world")
 
 
-      const result = yield* prompt.loop({ sessionID: session.id })
-      expect(result.info.role).toBe("assistant")
-      expect(result.parts.some((part) => part.type === "text" && part.text === "world")).toBe(true)
-      expect(yield* llm.hits).toHaveLength(1)
-      expect(yield* llm.pending).toBe(0)
-    }),
-  { git: true },
+    const result = yield* prompt.loop({ sessionID: session.id })
+    expect(result.info.role).toBe("assistant")
+    expect(result.parts.some((part) => part.type === "text" && part.text === "world")).toBe(true)
+    expect(yield* llm.hits).toHaveLength(1)
+    expect(yield* llm.pending).toBe(0)
+  }),
 )
 )
 
 
-it.instance(
-  "static loop consumes queued replies across turns",
-  () =>
-    Effect.gen(function* () {
-      const { llm } = yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const session = yield* sessions.create({
-        title: "Prompt provider turns",
-        permission: [{ permission: "*", pattern: "*", action: "allow" }],
-      })
+it.instance("static loop consumes queued replies across turns", () =>
+  Effect.gen(function* () {
+    const { llm } = yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const session = yield* sessions.create({
+      title: "Prompt provider turns",
+      permission: [{ permission: "*", pattern: "*", action: "allow" }],
+    })
 
 
-      yield* prompt.prompt({
-        sessionID: session.id,
-        agent: "build",
-        noReply: true,
-        parts: [{ type: "text", text: "hello one" }],
-      })
+    yield* prompt.prompt({
+      sessionID: session.id,
+      agent: "build",
+      noReply: true,
+      parts: [{ type: "text", text: "hello one" }],
+    })
 
 
-      yield* llm.text("world one")
+    yield* llm.text("world one")
 
 
-      const first = yield* prompt.loop({ sessionID: session.id })
-      expect(first.info.role).toBe("assistant")
-      expect(first.parts.some((part) => part.type === "text" && part.text === "world one")).toBe(true)
+    const first = yield* prompt.loop({ sessionID: session.id })
+    expect(first.info.role).toBe("assistant")
+    expect(first.parts.some((part) => part.type === "text" && part.text === "world one")).toBe(true)
 
 
-      yield* prompt.prompt({
-        sessionID: session.id,
-        agent: "build",
-        noReply: true,
-        parts: [{ type: "text", text: "hello two" }],
-      })
+    yield* prompt.prompt({
+      sessionID: session.id,
+      agent: "build",
+      noReply: true,
+      parts: [{ type: "text", text: "hello two" }],
+    })
 
 
-      yield* llm.text("world two")
+    yield* llm.text("world two")
 
 
-      const second = yield* prompt.loop({ sessionID: session.id })
-      expect(second.info.role).toBe("assistant")
-      expect(second.parts.some((part) => part.type === "text" && part.text === "world two")).toBe(true)
+    const second = yield* prompt.loop({ sessionID: session.id })
+    expect(second.info.role).toBe("assistant")
+    expect(second.parts.some((part) => part.type === "text" && part.text === "world two")).toBe(true)
 
 
-      expect(yield* llm.hits).toHaveLength(2)
-      expect(yield* llm.pending).toBe(0)
-    }),
-  { git: true },
+    expect(yield* llm.hits).toHaveLength(2)
+    expect(yield* llm.pending).toBe(0)
+  }),
 )
 )
 
 
-it.instance(
-  "loop continues when finish is tool-calls",
-  () =>
-    Effect.gen(function* () {
-      const { llm } = yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const session = yield* sessions.create({
-        title: "Pinned",
-        permission: [{ permission: "*", pattern: "*", action: "allow" }],
-      })
-      yield* prompt.prompt({
-        sessionID: session.id,
-        agent: "build",
-        noReply: true,
-        parts: [{ type: "text", text: "hello" }],
-      })
-      yield* llm.tool("first", { value: "first" })
-      yield* llm.text("second")
-
-      const result = yield* prompt.loop({ sessionID: session.id })
-      expect(yield* llm.calls).toBe(2)
-      expect(result.info.role).toBe("assistant")
-      if (result.info.role === "assistant") {
-        expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
-        expect(result.info.finish).toBe("stop")
-      }
-    }),
-  { git: true },
+it.instance("loop continues when finish is tool-calls", () =>
+  Effect.gen(function* () {
+    const { llm } = yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const session = yield* sessions.create({
+      title: "Pinned",
+      permission: [{ permission: "*", pattern: "*", action: "allow" }],
+    })
+    yield* prompt.prompt({
+      sessionID: session.id,
+      agent: "build",
+      noReply: true,
+      parts: [{ type: "text", text: "hello" }],
+    })
+    yield* llm.tool("first", { value: "first" })
+    yield* llm.text("second")
+
+    const result = yield* prompt.loop({ sessionID: session.id })
+    expect(yield* llm.calls).toBe(2)
+    expect(result.info.role).toBe("assistant")
+    if (result.info.role === "assistant") {
+      expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
+      expect(result.info.finish).toBe("stop")
+    }
+  }),
 )
 )
 
 
-it.instance(
-  "glob tool keeps instance context during prompt runs",
-  () =>
-    Effect.gen(function* () {
-      const { dir, llm } = yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const session = yield* sessions.create({
-        title: "Glob context",
-        permission: [{ permission: "*", pattern: "*", action: "allow" }],
-      })
-      const file = path.join(dir, "probe.txt")
-      yield* writeText(file, "probe")
-
-      yield* prompt.prompt({
-        sessionID: session.id,
-        agent: "build",
-        noReply: true,
-        parts: [{ type: "text", text: "find text files" }],
-      })
-      yield* llm.tool("glob", { pattern: "**/*.txt" })
-      yield* llm.text("done")
-
-      const result = yield* prompt.loop({ sessionID: session.id })
-      expect(result.info.role).toBe("assistant")
-
-      const msgs = yield* MessageV2.filterCompactedEffect(session.id)
-      const tool = msgs
-        .flatMap((msg) => msg.parts)
-        .find(
-          (part): part is CompletedToolPart =>
-            part.type === "tool" && part.tool === "glob" && part.state.status === "completed",
-        )
-      if (!tool) return
+it.instance("glob tool keeps instance context during prompt runs", () =>
+  Effect.gen(function* () {
+    const { dir, llm } = yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const session = yield* sessions.create({
+      title: "Glob context",
+      permission: [{ permission: "*", pattern: "*", action: "allow" }],
+    })
+    const file = path.join(dir, "probe.txt")
+    yield* writeText(file, "probe")
+
+    yield* prompt.prompt({
+      sessionID: session.id,
+      agent: "build",
+      noReply: true,
+      parts: [{ type: "text", text: "find text files" }],
+    })
+    yield* llm.tool("glob", { pattern: "**/*.txt" })
+    yield* llm.text("done")
+
+    const result = yield* prompt.loop({ sessionID: session.id })
+    expect(result.info.role).toBe("assistant")
+
+    const msgs = yield* MessageV2.filterCompactedEffect(session.id)
+    const tool = msgs
+      .flatMap((msg) => msg.parts)
+      .find(
+        (part): part is CompletedToolPart =>
+          part.type === "tool" && part.tool === "glob" && part.state.status === "completed",
+      )
+    if (!tool) return
 
 
-      expect(tool.state.output).toContain(file)
-      expect(tool.state.output).not.toContain("No context found for instance")
-      expect(result.parts.some((part) => part.type === "text" && part.text === "done")).toBe(true)
-    }),
-  { git: true },
+    expect(tool.state.output).toContain(file)
+    expect(tool.state.output).not.toContain("No context found for instance")
+    expect(result.parts.some((part) => part.type === "text" && part.text === "done")).toBe(true)
+  }),
 )
 )
 
 
-it.instance(
-  "loop continues when finish is stop but assistant has tool parts",
-  () =>
-    Effect.gen(function* () {
-      const { llm } = yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const session = yield* sessions.create({
-        title: "Pinned",
-        permission: [{ permission: "*", pattern: "*", action: "allow" }],
-      })
-      yield* prompt.prompt({
-        sessionID: session.id,
-        agent: "build",
-        noReply: true,
-        parts: [{ type: "text", text: "hello" }],
-      })
-      yield* llm.push(reply().tool("first", { value: "first" }).stop())
-      yield* llm.text("second")
-
-      const result = yield* prompt.loop({ sessionID: session.id })
-      expect(yield* llm.calls).toBe(2)
-      expect(result.info.role).toBe("assistant")
-      if (result.info.role === "assistant") {
-        expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
-        expect(result.info.finish).toBe("stop")
-      }
-    }),
-  { git: true },
+it.instance("loop continues when finish is stop but assistant has tool parts", () =>
+  Effect.gen(function* () {
+    const { llm } = yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const session = yield* sessions.create({
+      title: "Pinned",
+      permission: [{ permission: "*", pattern: "*", action: "allow" }],
+    })
+    yield* prompt.prompt({
+      sessionID: session.id,
+      agent: "build",
+      noReply: true,
+      parts: [{ type: "text", text: "hello" }],
+    })
+    yield* llm.push(reply().tool("first", { value: "first" }).stop())
+    yield* llm.text("second")
+
+    const result = yield* prompt.loop({ sessionID: session.id })
+    expect(yield* llm.calls).toBe(2)
+    expect(result.info.role).toBe("assistant")
+    if (result.info.role === "assistant") {
+      expect(result.parts.some((part) => part.type === "text" && part.text === "second")).toBe(true)
+      expect(result.info.finish).toBe("stop")
+    }
+  }),
 )
 )
 
 
-it.instance(
-  "failed subtask preserves metadata on error tool state",
-  () =>
-    Effect.gen(function* () {
-      const { llm } = yield* useServerConfig((url) => ({
-        ...providerCfg(url),
-        agent: {
-          general: {
-            model: "test/missing-model",
-          },
+it.instance("failed subtask preserves metadata on error tool state", () =>
+  Effect.gen(function* () {
+    const { llm } = yield* useServerConfig((url) => ({
+      ...providerCfg(url),
+      agent: {
+        general: {
+          model: "test/missing-model",
         },
         },
-      }))
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const chat = yield* sessions.create({ title: "Pinned" })
-      yield* llm.tool("task", {
-        description: "inspect bug",
-        prompt: "look into the cache key path",
-        subagent_type: "general",
-      })
-      yield* llm.text("done")
-      const msg = yield* user(chat.id, "hello")
-      yield* addSubtask(chat.id, msg.id)
-
-      const result = yield* prompt.loop({ sessionID: chat.id })
-      expect(result.info.role).toBe("assistant")
-      expect(yield* llm.calls).toBe(2)
-
-      const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
-      const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general")
-      expect(taskMsg?.info.role).toBe("assistant")
-      if (!taskMsg || taskMsg.info.role !== "assistant") return
-
-      const tool = errorTool(taskMsg.parts)
-      if (!tool) return
-
-      expect(tool.state.error).toContain("Tool execution failed")
-      expect(tool.state.metadata).toBeDefined()
-      expect(tool.state.metadata?.sessionId).toBeDefined()
-      expect(tool.state.metadata?.model).toEqual({
-        providerID: ProviderID.make("test"),
-        modelID: ModelID.make("missing-model"),
-      })
-    }),
-  { git: true },
+      },
+    }))
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const chat = yield* sessions.create({ title: "Pinned" })
+    yield* llm.tool("task", {
+      description: "inspect bug",
+      prompt: "look into the cache key path",
+      subagent_type: "general",
+    })
+    yield* llm.text("done")
+    const msg = yield* user(chat.id, "hello")
+    yield* addSubtask(chat.id, msg.id)
+
+    const result = yield* prompt.loop({ sessionID: chat.id })
+    expect(result.info.role).toBe("assistant")
+    expect(yield* llm.calls).toBe(2)
+
+    const msgs = yield* MessageV2.filterCompactedEffect(chat.id)
+    const taskMsg = msgs.find((item) => item.info.role === "assistant" && item.info.agent === "general")
+    expect(taskMsg?.info.role).toBe("assistant")
+    if (!taskMsg || taskMsg.info.role !== "assistant") return
+
+    const tool = errorTool(taskMsg.parts)
+    if (!tool) return
+
+    expect(tool.state.error).toContain("Tool execution failed")
+    expect(tool.state.metadata).toBeDefined()
+    expect(tool.state.metadata?.sessionId).toBeDefined()
+    expect(tool.state.metadata?.model).toEqual({
+      providerID: ProviderID.make("test"),
+      modelID: ModelID.make("missing-model"),
+    })
+  }),
 )
 )
 
 
 it.instance(
 it.instance(
@@ -778,7 +751,6 @@ it.instance(
       yield* prompt.cancel(chat.id)
       yield* prompt.cancel(chat.id)
       yield* Fiber.await(fiber)
       yield* Fiber.await(fiber)
     }),
     }),
-  { git: true },
   5_000,
   5_000,
 )
 )
 
 
@@ -823,7 +795,6 @@ it.instance(
       yield* prompt.cancel(chat.id)
       yield* prompt.cancel(chat.id)
       yield* Fiber.await(fiber)
       yield* Fiber.await(fiber)
     }),
     }),
-  { git: true },
   10_000,
   10_000,
 )
 )
 
 
@@ -848,7 +819,6 @@ it.instance(
       yield* Fiber.await(fiber)
       yield* Fiber.await(fiber)
       expect((yield* status.get(chat.id)).type).toBe("idle")
       expect((yield* status.get(chat.id)).type).toBe("idle")
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
@@ -877,7 +847,6 @@ it.instance(
         expect(exit.value.info.role).toBe("assistant")
         expect(exit.value.info.role).toBe("assistant")
       }
       }
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
@@ -904,7 +873,6 @@ it.instance(
         }
         }
       }
       }
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
@@ -994,7 +962,6 @@ race.instance(
         expect(lastAssistant.info.parentID).toBe(lastUser?.info.id)
         expect(lastAssistant.info.parentID).toBe(lastUser?.info.id)
       }
       }
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
@@ -1041,7 +1008,7 @@ it.instance(
       expect(taskMsg.info.time.completed).toBeDefined()
       expect(taskMsg.info.time.completed).toBeDefined()
       expect(taskMsg.info.finish).toBeDefined()
       expect(taskMsg.info.finish).toBeDefined()
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
   30_000,
   30_000,
 )
 )
 
 
@@ -1077,7 +1044,6 @@ it.instance(
       expect((yield* status.get(chat.id)).type).toBe("idle")
       expect((yield* status.get(chat.id)).type).toBe("idle")
       expect((yield* status.get(childID)).type).toBe("idle")
       expect((yield* status.get(childID)).type).toBe("idle")
     }),
     }),
-  { git: true },
   10_000,
   10_000,
 )
 )
 
 
@@ -1111,22 +1077,19 @@ it.instance(
 
 
 // Queue semantics
 // Queue semantics
 
 
-it.instance(
-  "concurrent loop callers get same result",
-  () =>
-    Effect.gen(function* () {
-      const { prompt, run, chat } = yield* boot()
-      yield* seed(chat.id, { finish: "stop" })
+it.instance("concurrent loop callers get same result", () =>
+  Effect.gen(function* () {
+    const { prompt, run, chat } = yield* boot()
+    yield* seed(chat.id, { finish: "stop" })
 
 
-      const [a, b] = yield* Effect.all([prompt.loop({ sessionID: chat.id }), prompt.loop({ sessionID: chat.id })], {
-        concurrency: "unbounded",
-      })
+    const [a, b] = yield* Effect.all([prompt.loop({ sessionID: chat.id }), prompt.loop({ sessionID: chat.id })], {
+      concurrency: "unbounded",
+    })
 
 
-      expect(a.info.id).toBe(b.info.id)
-      expect(a.info.role).toBe("assistant")
-      yield* run.assertNotBusy(chat.id)
-    }),
-  { git: true },
+    expect(a.info.id).toBe(b.info.id)
+    expect(a.info.role).toBe("assistant")
+    yield* run.assertNotBusy(chat.id)
+  }),
 )
 )
 
 
 it.instance(
 it.instance(
@@ -1147,7 +1110,6 @@ it.instance(
       expect(a.info.id).toBe(b.info.id)
       expect(a.info.id).toBe(b.info.id)
       expect(a.info.role).toBe("assistant")
       expect(a.info.role).toBe("assistant")
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
@@ -1216,7 +1178,6 @@ it.instance(
       expect(inputs).toHaveLength(2)
       expect(inputs).toHaveLength(2)
       expect(JSON.stringify(inputs.at(-1)?.messages)).toContain("second")
       expect(JSON.stringify(inputs.at(-1)?.messages)).toContain("second")
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
@@ -1246,22 +1207,18 @@ it.instance(
       yield* prompt.cancel(chat.id)
       yield* prompt.cancel(chat.id)
       yield* Fiber.await(fiber)
       yield* Fiber.await(fiber)
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
-it.instance(
-  "assertNotBusy succeeds when idle",
-  () =>
-    Effect.gen(function* () {
-      const run = yield* SessionRunState.Service
-      const sessions = yield* Session.Service
+it.instance("assertNotBusy succeeds when idle", () =>
+  Effect.gen(function* () {
+    const run = yield* SessionRunState.Service
+    const sessions = yield* Session.Service
 
 
-      const chat = yield* sessions.create({})
-      const exit = yield* run.assertNotBusy(chat.id).pipe(Effect.exit)
-      expect(Exit.isSuccess(exit)).toBe(true)
-    }),
-  { git: true },
+    const chat = yield* sessions.create({})
+    const exit = yield* run.assertNotBusy(chat.id).pipe(Effect.exit)
+    expect(Exit.isSuccess(exit)).toBe(true)
+  }),
 )
 )
 
 
 // Shell semantics
 // Shell semantics
@@ -1290,7 +1247,6 @@ it.instance(
       yield* prompt.cancel(chat.id)
       yield* prompt.cancel(chat.id)
       yield* Fiber.await(fiber)
       yield* Fiber.await(fiber)
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
@@ -1315,7 +1271,7 @@ unix(
       expect(tool.state.metadata.output).toContain("err")
       expect(tool.state.metadata.output).toContain("err")
       yield* run.assertNotBusy(chat.id)
       yield* run.assertNotBusy(chat.id)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
 )
 )
 
 
 unix(
 unix(
@@ -1339,7 +1295,7 @@ unix(
       expect(tool.state.metadata.output).toContain(dir)
       expect(tool.state.metadata.output).toContain(dir)
       yield* run.assertNotBusy(chat.id)
       yield* run.assertNotBusy(chat.id)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
 )
 )
 
 
 unix(
 unix(
@@ -1361,7 +1317,7 @@ unix(
         expect(tool.state.output).toContain("configured")
         expect(tool.state.output).toContain("configured")
       }),
       }),
     ),
     ),
-  { git: true, config: { ...cfg, shell: "bash" } },
+  { config: { ...cfg, shell: "bash" } },
   30_000,
   30_000,
 )
 )
 
 
@@ -1386,7 +1342,7 @@ unix(
       expect(tool.state.metadata.output).toContain(parent)
       expect(tool.state.metadata.output).toContain(parent)
       yield* run.assertNotBusy(chat.id)
       yield* run.assertNotBusy(chat.id)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
 )
 )
 
 
 unix(
 unix(
@@ -1412,7 +1368,7 @@ unix(
       expect(tool.state.metadata.output).toContain("README.md")
       expect(tool.state.metadata.output).toContain("README.md")
       yield* run.assertNotBusy(chat.id)
       yield* run.assertNotBusy(chat.id)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
 )
 )
 
 
 unix(
 unix(
@@ -1434,7 +1390,7 @@ unix(
       expect(tool.state.metadata.output).toContain("not found")
       expect(tool.state.metadata.output).toContain("not found")
       yield* run.assertNotBusy(chat.id)
       yield* run.assertNotBusy(chat.id)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
 )
 )
 
 
 unix(
 unix(
@@ -1462,7 +1418,7 @@ unix(
         expect(Exit.isSuccess(exit)).toBe(true)
         expect(Exit.isSuccess(exit)).toBe(true)
       }),
       }),
     ),
     ),
-  { git: true, config: cfg },
+  { config: cfg },
   30_000,
   30_000,
 )
 )
 
 
@@ -1572,7 +1528,6 @@ unix(
         expect(JSON.stringify(inputs.at(-1)?.messages)).toContain("configured")
         expect(JSON.stringify(inputs.at(-1)?.messages)).toContain("configured")
       }),
       }),
     ),
     ),
-  { git: true },
   30_000,
   30_000,
 )
 )
 
 
@@ -1808,7 +1763,7 @@ it.instance(
       const exit = yield* Fiber.await(fiber)
       const exit = yield* Fiber.await(fiber)
       expect(Exit.isFailure(exit)).toBe(true)
       expect(Exit.isFailure(exit)).toBe(true)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
   30_000,
   30_000,
 )
 )
 
 
@@ -1843,7 +1798,7 @@ it.instance(
       const exit = yield* Fiber.await(fiber)
       const exit = yield* Fiber.await(fiber)
       expect(Exit.isFailure(exit)).toBe(true)
       expect(Exit.isFailure(exit)).toBe(true)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
   30_000,
   30_000,
 )
 )
 
 
@@ -1882,7 +1837,7 @@ it.instance(
 
 
       yield* sessions.remove(session.id)
       yield* sessions.remove(session.id)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
 )
 )
 
 
 it.instance(
 it.instance(
@@ -1924,7 +1879,7 @@ it.instance(
 
 
       yield* sessions.remove(session.id)
       yield* sessions.remove(session.id)
     }),
     }),
-  { git: true, config: cfg },
+  { config: cfg },
 )
 )
 
 
 it.instance(
 it.instance(
@@ -1973,7 +1928,6 @@ it.instance(
       expect(agents.map((agent) => agent.name)).toEqual(["build"])
       expect(agents.map((agent) => agent.name)).toEqual(["build"])
     }),
     }),
   {
   {
-    git: true,
     config: {
     config: {
       ...cfg,
       ...cfg,
       reference: {
       reference: {
@@ -2013,7 +1967,6 @@ it.instance(
       yield* sessions.remove(session.id)
       yield* sessions.remove(session.id)
     }),
     }),
   {
   {
-    git: true,
     config: {
     config: {
       ...cfg,
       ...cfg,
       reference: {
       reference: {
@@ -2079,7 +2032,6 @@ it.instance(
       yield* sessions.remove(session.id)
       yield* sessions.remove(session.id)
     }),
     }),
   {
   {
-    git: true,
     config: {
     config: {
       ...cfg,
       ...cfg,
       reference: {
       reference: {
@@ -2128,31 +2080,28 @@ it.instance(
 
 
 // Regression: empty assistant turn loop
 // Regression: empty assistant turn loop
 
 
-it.instance(
-  "does not loop empty assistant turns for a simple reply",
-  () =>
-    Effect.gen(function* () {
-      const { llm } = yield* useServerConfig(providerCfg)
-      const prompt = yield* SessionPrompt.Service
-      const sessions = yield* Session.Service
-      const session = yield* sessions.create({ title: "Prompt regression" })
+it.instance("does not loop empty assistant turns for a simple reply", () =>
+  Effect.gen(function* () {
+    const { llm } = yield* useServerConfig(providerCfg)
+    const prompt = yield* SessionPrompt.Service
+    const sessions = yield* Session.Service
+    const session = yield* sessions.create({ title: "Prompt regression" })
 
 
-      yield* llm.text("packages/opencode/src/session/processor.ts")
+    yield* llm.text("packages/opencode/src/session/processor.ts")
 
 
-      const result = yield* prompt.prompt({
-        sessionID: session.id,
-        agent: "build",
-        parts: [{ type: "text", text: "Where is SessionProcessor?" }],
-      })
+    const result = yield* prompt.prompt({
+      sessionID: session.id,
+      agent: "build",
+      parts: [{ type: "text", text: "Where is SessionProcessor?" }],
+    })
 
 
-      expect(result.info.role).toBe("assistant")
-      expect(result.parts.some((part) => part.type === "text" && part.text.includes("processor.ts"))).toBe(true)
+    expect(result.info.role).toBe("assistant")
+    expect(result.parts.some((part) => part.type === "text" && part.text.includes("processor.ts"))).toBe(true)
 
 
-      const msgs = yield* sessions.messages({ sessionID: session.id })
-      expect(msgs.filter((msg) => msg.info.role === "assistant")).toHaveLength(1)
-      expect(yield* llm.calls).toBe(1)
-    }),
-  { git: true },
+    const msgs = yield* sessions.messages({ sessionID: session.id })
+    expect(msgs.filter((msg) => msg.info.role === "assistant")).toHaveLength(1)
+    expect(yield* llm.calls).toBe(1)
+  }),
 )
 )
 
 
 it.instance(
 it.instance(
@@ -2193,7 +2142,6 @@ it.instance(
         expect(last.info.error?.name).toBe("MessageAbortedError")
         expect(last.info.error?.name).toBe("MessageAbortedError")
       }
       }
     }),
     }),
-  { git: true },
   3_000,
   3_000,
 )
 )
 
 
@@ -2244,7 +2192,6 @@ it.instance(
       yield* sessions.remove(session.id)
       yield* sessions.remove(session.id)
     }),
     }),
   {
   {
-    git: true,
     config: {
     config: {
       ...cfg,
       ...cfg,
       provider: {
       provider: {
@@ -2297,7 +2244,6 @@ it.instance(
         }
         }
       }
       }
     }),
     }),
-  { git: true },
   30_000,
   30_000,
 )
 )
 
 
@@ -2326,7 +2272,6 @@ it.instance(
         }
         }
       }
       }
     }),
     }),
-  { git: true },
   30_000,
   30_000,
 )
 )
 
 
@@ -2356,6 +2301,5 @@ it.instance(
         }
         }
       }
       }
     }),
     }),
-  { git: true },
   30_000,
   30_000,
 )
 )

+ 44 - 46
packages/opencode/test/tool/skill.test.ts

@@ -31,14 +31,13 @@ const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
 
 
 describe("tool.skill", () => {
 describe("tool.skill", () => {
   it.live("execute returns skill content block with files", () =>
   it.live("execute returns skill content block with files", () =>
-    provideTmpdirInstance(
-      (dir) =>
-        Effect.gen(function* () {
-          const skill = path.join(dir, ".opencode", "skill", "tool-skill")
-          yield* Effect.promise(() =>
-            Bun.write(
-              path.join(skill, "SKILL.md"),
-              `---
+    provideTmpdirInstance((dir) =>
+      Effect.gen(function* () {
+        const skill = path.join(dir, ".opencode", "skill", "tool-skill")
+        yield* Effect.promise(() =>
+          Bun.write(
+            path.join(skill, "SKILL.md"),
+            `---
 name: tool-skill
 name: tool-skill
 description: Skill for tool tests.
 description: Skill for tool tests.
 ---
 ---
@@ -47,49 +46,48 @@ description: Skill for tool tests.
 
 
 Use this skill.
 Use this skill.
 `,
 `,
-            ),
-          )
-          yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
+          ),
+        )
+        yield* Effect.promise(() => Bun.write(path.join(skill, "scripts", "demo.txt"), "demo"))
 
 
-          const home = process.env.OPENCODE_TEST_HOME
-          process.env.OPENCODE_TEST_HOME = dir
-          yield* Effect.addFinalizer(() =>
-            Effect.sync(() => {
-              process.env.OPENCODE_TEST_HOME = home
-            }),
-          )
+        const home = process.env.OPENCODE_TEST_HOME
+        process.env.OPENCODE_TEST_HOME = dir
+        yield* Effect.addFinalizer(() =>
+          Effect.sync(() => {
+            process.env.OPENCODE_TEST_HOME = home
+          }),
+        )
 
 
-          const registry = yield* ToolRegistry.Service
-          const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
-          const tool = (yield* registry.tools({
-            providerID: "opencode" as any,
-            modelID: "gpt-5" as any,
-            agent,
-          })).find((tool) => tool.id === SkillTool.id)
-          if (!tool) throw new Error("Skill tool not found")
+        const registry = yield* ToolRegistry.Service
+        const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
+        const tool = (yield* registry.tools({
+          providerID: "opencode" as any,
+          modelID: "gpt-5" as any,
+          agent,
+        })).find((tool) => tool.id === SkillTool.id)
+        if (!tool) throw new Error("Skill tool not found")
 
 
-          const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
-          const ctx: Tool.Context = {
-            ...baseCtx,
-            ask: (req) =>
-              Effect.sync(() => {
-                requests.push(req)
-              }),
-          }
+        const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
+        const ctx: Tool.Context = {
+          ...baseCtx,
+          ask: (req) =>
+            Effect.sync(() => {
+              requests.push(req)
+            }),
+        }
 
 
-          const result = yield* tool.execute({ name: "tool-skill" }, ctx)
-          const file = path.resolve(skill, "scripts", "demo.txt")
+        const result = yield* tool.execute({ name: "tool-skill" }, ctx)
+        const file = path.resolve(skill, "scripts", "demo.txt")
 
 
-          expect(requests.length).toBe(1)
-          expect(requests[0].permission).toBe("skill")
-          expect(requests[0].patterns).toContain("tool-skill")
-          expect(requests[0].always).toContain("tool-skill")
-          expect(result.metadata.dir).toBe(skill)
-          expect(result.output).toContain(`<skill_content name="tool-skill">`)
-          expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
-          expect(result.output).toContain(`<file>${file}</file>`)
-        }),
-      { git: true },
+        expect(requests.length).toBe(1)
+        expect(requests[0].permission).toBe("skill")
+        expect(requests[0].patterns).toContain("tool-skill")
+        expect(requests[0].always).toContain("tool-skill")
+        expect(result.metadata.dir).toBe(skill)
+        expect(result.output).toContain(`<skill_content name="tool-skill">`)
+        expect(result.output).toContain(`Base directory for this skill: ${pathToFileURL(skill).href}`)
+        expect(result.output).toContain(`<file>${file}</file>`)
+      }),
     ),
     ),
   )
   )
 })
 })

+ 123 - 0
perf/test-suite.md

@@ -0,0 +1,123 @@
+# Test Suite Speed
+
+## Goal
+
+Speed up the `packages/opencode` test suite without reducing coverage or hiding failures.
+
+## Benchmark Command
+
+Run from `packages/opencode`:
+
+```sh
+bun run bench:test
+```
+
+The full-suite benchmark defaults to one measured run. Use repeated runs only after a targeted win:
+
+```sh
+BENCH_WARMUPS=1 BENCH_RUNS=3 bun run bench:test
+```
+
+To identify slow files, run:
+
+```sh
+bun run profile:test
+```
+
+Scope it while exploring:
+
+```sh
+TEST_PROFILE_GLOB='test/server/**/*.test.ts' bun run profile:test
+TEST_PROFILE_LIMIT=20 bun run profile:test
+```
+
+## Primary Metric
+
+`METRIC test_suite_seconds=<median wall clock seconds>`
+
+## Secondary Metrics
+
+`test_suite_best_seconds`, `test_suite_worst_seconds`, failures, and noisy spread.
+
+For profiling: `slowest_test_file_seconds` and the slowest file list.
+
+## Files In Scope
+
+`packages/opencode/test/**`, test fixtures, package test scripts, and implementation setup paths only when a benchmarked bottleneck points there.
+
+## Signals To Watch
+
+Repeated setup work, long sleeps/timeouts, serial integration tests, filesystem/database fixture costs, and broad test globs pulling unrelated work.
+
+## Hypothesis Loop
+
+| Hypothesis                                                                                                | Change                                                                                         | Before    | After   | Decision | Notes                                                                                                                       |
+| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | --------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
+| Repeated full-suite runs are too expensive for discovery                                                  | Switched full-suite benchmark to one run and added per-file profiler                           | ~250s/run | pending | keep     | Bun has no slowest-test reporter in this version; profile files directly.                                                   |
+| Plugin install concurrency test spends time spawning more workers than needed to exercise lock contention | Reduced worker counts from 12/10/8 to 6/6/5; kept `holdMs: 30`                                 | 7.800s    | 6.204s  | keep     | Median from 3 targeted runs; still covers concurrent cross-process writes to server, server+tui, and existing json config.  |
+| `httpapi-listen` PTY route tests pay for git repositories they do not assert on                           | Removed `git: true` from temp dirs while keeping config setup                                  | 10.554s   | 7.818s  | keep     | Median from 3 targeted runs; HTTP routes, tickets, websocket upgrade, restart, and no-auth paths still pass.                |
+| `workspace.waitForSync` timeout test waits the full production timeout                                    | Added optional timeout parameter defaulting to production timeout; timeout test uses 25ms      | 12.949s   | 8.305s  | keep     | Median from 3 targeted runs; production callers keep the 5000ms default.                                                    |
+| `config.test` waits after dependencies even though `.gitignore` is written synchronously                  | Removed obsolete 1000ms sleep from writable `OPENCODE_CONFIG_DIR` test                         | 10.270s   | 9.433s  | keep     | Median from 5 targeted runs because one run was noisy; simpler test and no fixed sleep.                                     |
+| SDK parity helpers create git repos for tests that only need files/config/session state                   | Changed `withProject` default to no git; explicit git init test still opts into no-git fixture | 8.011s    | 5.180s  | keep     | Median from 5 targeted runs because first run was cold/noisy.                                                               |
+| Provider plugin filter test waits on plugin dependency readiness setup                                    | Marked local plugin dependencies ready using the existing fixture helper                       | 7.543s    | 6.366s  | keep     | Median from 3 targeted runs; matches neighboring plugin provider test setup.                                                |
+| HTTP provider tests generate local plugins without dependency-ready fixture state                         | Marked generated `.opencode` plugin fixtures dependency-ready                                  | 7.905s    | 2.980s  | keep     | Median from 3 targeted runs; avoids unrelated plugin dependency setup in route tests.                                       |
+| TUI plugin lifecycle timeout coverage waits the full production cleanup timeout                           | Added optional runtime dispose timeout override and used 25ms in the timeout test              | 7.330s    | 1.507s  | keep     | Median from 3 targeted runs; production default remains 5000ms.                                                             |
+| Skill tool test initializes git even though it only reads local skill files                               | Removed `git: true` from the temporary directory fixture                                       | 2.320s    | 1.425s  | keep     | Single targeted rerun; still exercises skill discovery, permission request, and bundled file output.                        |
+| Prompt shell semantics tests initialize git though they only assert shell/session behavior                | Removed `git: true` from shell-focused prompt fixtures while preserving config setup           | 26.930s   | 23.400s | keep     | Three targeted reruns passed after the change: 23.80s, 23.55s, 23.40s.                                                      |
+| Remaining prompt behavior tests mostly do not require repository state                                    | Removed git setup from safe loop/reference/error fixtures; restored shell queue/cancel cases   | 23.400s   | 19.610s | keep     | Safety review found shell runner readiness depends on git-backed setup in several tests; current single rerun passes.       |
+| Session processor effect tests do not require repository state                                            | Removed git setup from all processor-effect temp server fixtures                               | 12.500s   | 9.230s  | keep     | Two targeted reruns passed after the change: 9.61s, 9.23s.                                                                  |
+| HTTP listen PTY ticket tests restart the same listener topology twice                                     | Folded directory-scoped ticket regression into the broader unsafe-ticket test                  | 7.051s    | 6.170s  | keep     | Two targeted reruns passed after the change: 6.76s, 6.17s; still covers mint failure and successful same-directory upgrade. |
+
+## Profiling Results
+
+Command shape:
+
+```sh
+TEST_PROFILE_GLOB='test/<area>/**/*.test.ts' TEST_PROFILE_TOP=15 bun run profile:test
+```
+
+Initial slowest files observed during discovery:
+
+| File                                      | Seconds | Scope         |
+| ----------------------------------------- | ------: | ------------- |
+| `test/config/config.test.ts`              |  23.546 | config        |
+| `test/provider/provider.test.ts`          |  18.747 | provider      |
+| `test/control-plane/workspace.test.ts`    |  16.447 | control-plane |
+| `test/plugin/install-concurrency.test.ts` |  14.804 | plugin        |
+| `test/server/httpapi-cors.test.ts`        |  14.620 | server        |
+| `test/server/httpapi-listen.test.ts`      |  10.073 | server        |
+| `test/server/httpapi-sdk.test.ts`         |   8.661 | server        |
+| `test/server/httpapi-provider.test.ts`    |   7.905 | server        |
+| `test/cli/tui/plugin-lifecycle.test.ts`   |   7.330 | cli/tui       |
+| `test/file/index.test.ts`                 |   7.214 | file          |
+
+This table is historical profiling input, not the current ranking after kept changes.
+
+Targeted 3-run baselines:
+
+| File                                      | Runs                   | Median | Notes                                                                        |
+| ----------------------------------------- | ---------------------- | -----: | ---------------------------------------------------------------------------- |
+| `test/control-plane/workspace.test.ts`    | 12.949, 12.949, 12.773 | 12.949 | Stable slow target.                                                          |
+| `test/server/httpapi-listen.test.ts`      | 10.554, 10.631, 10.479 | 10.554 | Stable slow target; WebSocket/listener lifecycle.                            |
+| `test/config/config.test.ts`              | 10.270, 9.042, 10.737  | 10.270 | Large serial file; initial 23s was mixed-scope contention/noise.             |
+| `test/server/httpapi-sdk.test.ts`         | 7.600, 8.011, 8.035    |  8.011 | Stable slow target.                                                          |
+| `test/plugin/install-concurrency.test.ts` | 7.949, 7.800, 7.712    |  7.800 | Stable slow target; many subprocesses.                                       |
+| `test/provider/provider.test.ts`          | 8.323, 7.543, 7.474    |  7.543 | Large serial file.                                                           |
+| `test/server/httpapi-cors.test.ts`        | 2.621, 1.682, 1.518    |  1.682 | Not a standalone top target; initial 14s was mixed-scope noise/order effect. |
+
+Full-suite sanity checks:
+
+| Command              |   Result | Notes                                                                |
+| -------------------- | -------: | -------------------------------------------------------------------- |
+| `bun run bench:test` | 225.069s | Before continuing prompt/session work.                               |
+| `bun run bench:test` | 186.729s | After prompt, processor, and PTY wins before safety review restores. |
+| `bun run bench:test` | 202.317s | After restoring prompt shell coverage and SDK VCS parity coverage.   |
+
+## Dead Ends
+
+| Hypothesis                                                             | Change Tried                                                                             | Before |  After | Decision | Notes                                                                                         |
+| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -----: | -----: | -------- | --------------------------------------------------------------------------------------------- |
+| `file/index.test.ts` pays unnecessary per-test global instance cleanup | Removed `afterEach(disposeAllInstances)` while keeping the explicit disposal test import | 5.262s | 5.089s | discard  | Improvement was within noise and the cleanup is a safety guard for many instance-state tests. |
+| Socket reset retry test can shorten its idle-timeout path              | Reduced Bun server idle timeout and tried forced server close                            | 16.46s | failed | discard  | Shorter idle timeout changed the error shape; forced close hung. Keep the real socket reset.  |
+| `tool/webfetch` can avoid per-test instance setup                      | Switched local HTTP tests from `it.instance` to `it.live`                                | 1.219s | failed | discard  | Tool execution reads instance-local agent state, so the temp instance is required.            |
+| LSP client interop tests can shorten coarse request-handling sleeps    | Reduced fixed post-notification waits from 100ms to 10ms                                 | 4.270s | 4.740s | discard  | First run improved to 3.870s but verification was slower than baseline; not a clear win.      |