Selaa lähdekoodia

test(httpapi): clean up SDK parity tests

Kit Langton 4 kuukautta sitten
vanhempi
commit
d3df8e1180
1 muutettua tiedostoa jossa 300 lisäystä ja 288 poistoa
  1. 300 288
      packages/opencode/test/server/httpapi-sdk.test.ts

+ 300 - 288
packages/opencode/test/server/httpapi-sdk.test.ts

@@ -1,5 +1,6 @@
-import { afterEach, describe, expect, test } from "bun:test"
+import { afterEach, describe, expect } from "bun:test"
 import { Effect } from "effect"
+import type * as Scope from "effect/Scope"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2"
 import { Instance } from "../../src/project/instance"
@@ -7,20 +8,26 @@ import { Server } from "../../src/server/server"
 import { MessageID, PartID, SessionID } from "../../src/session/schema"
 import { MessageV2 } from "../../src/session/message-v2"
 import { ModelID, ProviderID } from "../../src/provider/schema"
+import type { Config } from "@/config/config"
 import { Session as SessionNs } from "@/session/session"
 import { TestLLMServer } from "../lib/llm-server"
 import path from "path"
 import { resetDatabase } from "../fixture/db"
 import { tmpdir } from "../fixture/fixture"
+import { it } from "../lib/effect"
 
 const original = {
   OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI,
   OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
   OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
 }
+
 type Backend = "legacy" | "httpapi"
 type Sdk = ReturnType<typeof createOpencodeClient>
 type SdkResult = { response: Response; data?: unknown; error?: unknown }
+type Captured = { status: number; data?: unknown; error?: unknown }
+type ProjectFixture = { sdk: Sdk; directory: string }
+type LlmProjectFixture = ProjectFixture & { llm: TestLLMServer["Service"] }
 
 function app(backend: Backend, input?: { password?: string; username?: string }) {
   Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = backend === "httpapi"
@@ -85,17 +92,35 @@ function providerConfig(url: string) {
   }
 }
 
-async function expectStatus(result: Promise<{ response: Response }>, status: number) {
-  expect((await result).response.status).toBe(status)
+function call<T>(request: () => Promise<T>) {
+  return Effect.promise(request)
 }
 
-async function capture(result: Promise<SdkResult>) {
-  const response = await result
-  return {
-    status: response.response.status,
-    data: response.data,
-    error: response.error,
-  }
+function capture(request: () => Promise<SdkResult>) {
+  return call(request).pipe(
+    Effect.map((result) => ({
+      status: result.response.status,
+      data: result.data,
+      error: result.error,
+    })),
+  )
+}
+
+function expectStatus(request: () => Promise<{ response: Response }>, status: number) {
+  return call(request).pipe(
+    Effect.tap((result) => Effect.sync(() => expect(result.response.status).toBe(status))),
+    Effect.asVoid,
+  )
+}
+
+function firstEvent(open: () => Promise<{ stream: AsyncIterator<unknown> }>) {
+  return Effect.acquireRelease(
+    call(open),
+    (events) => call(async () => void (await events.stream.return?.(undefined))).pipe(Effect.ignore),
+  ).pipe(
+    Effect.flatMap((events) => call(() => events.stream.next())),
+    Effect.map((result) => result.value),
+  )
 }
 
 function record(value: unknown) {
@@ -106,7 +131,7 @@ function array(value: unknown) {
   return Array.isArray(value) ? value : []
 }
 
-function statuses(input: Record<string, Awaited<ReturnType<typeof capture>>>) {
+function statuses(input: Record<string, Captured>) {
   return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, value.status]))
 }
 
@@ -121,75 +146,91 @@ function sessionTitles(value: unknown) {
     .sort()
 }
 
-async function runSession<A, E>(directory: string, effect: Effect.Effect<A, E, SessionNs.Service>) {
-  return Instance.provide({
-    directory,
-    fn: () => Effect.runPromise(effect.pipe(Effect.provide(SessionNs.defaultLayer))),
+function resetState() {
+  return Effect.promise(async () => {
+    await Instance.disposeAll()
+    await resetDatabase()
   })
 }
 
-async function seedMessage(directory: string, sessionID: string) {
-  const id = SessionID.make(sessionID)
-  return runSession(
-    directory,
-    SessionNs.Service.use((svc) =>
-      Effect.gen(function* () {
-        const message = yield* svc.updateMessage({
-          id: MessageID.ascending(),
-          sessionID: id,
-          role: "user",
-          time: { created: Date.now() },
-          agent: "test",
-          model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
-          tools: {},
-          mode: "",
-        } as unknown as MessageV2.Info)
-        const part = yield* svc.updatePart({
-          id: PartID.ascending(),
-          sessionID: id,
-          messageID: message.id,
-          type: "text",
-          text: "seeded message",
-        })
-        return { message, part }
-      }),
-    ),
+function httpapi<A, E>(name: string, effect: Effect.Effect<A, E, Scope.Scope>) {
+  it.live(name, effect)
+}
+
+function parity<A, E>(name: string, scenario: (backend: Backend) => Effect.Effect<A, E, Scope.Scope>) {
+  it.live(
+    name,
+    Effect.gen(function* () {
+      const legacy = yield* scenario("legacy")
+      yield* resetState()
+      const httpapi = yield* scenario("httpapi")
+      expect(httpapi).toEqual(legacy)
+    }),
   )
 }
 
-async function compareBackends<T>(scenario: (backend: Backend) => Promise<T>) {
-  const legacy = await scenario("legacy")
-  await Instance.disposeAll()
-  await resetDatabase()
-  const httpapi = await scenario("httpapi")
-  expect(httpapi).toEqual(legacy)
+function withProject<A, E, R>(
+  backend: Backend,
+  options: { git?: boolean; config?: Partial<Config.Info>; setup?: (dir: string) => Effect.Effect<void> },
+  run: (input: ProjectFixture) => Effect.Effect<A, E, R>,
+) {
+  return Effect.acquireRelease(
+    call(() => tmpdir({ git: options.git ?? true, config: { formatter: false, lsp: false, ...options.config } })),
+    (tmp) => call(() => tmp[Symbol.asyncDispose]()).pipe(Effect.ignore),
+  ).pipe(
+    Effect.tap((tmp) => options.setup?.(tmp.path) ?? Effect.void),
+    Effect.flatMap((tmp) => run({ sdk: client(backend, tmp.path), directory: tmp.path })),
+  )
 }
 
-async function withTmp<T>(backend: Backend, fn: (input: { sdk: Sdk; directory: string }) => Promise<T>) {
-  await using tmp = await tmpdir({
-    git: true,
-    config: { formatter: false, lsp: false },
-    init: async (dir) => {
-      await Bun.write(path.join(dir, "hello.txt"), "hello")
-      await Bun.write(path.join(dir, "needle.ts"), "export const needle = 'sdk-parity'\n")
-    },
-  })
-  return fn({ sdk: client(backend, tmp.path), directory: tmp.path })
+function withStandardProject<A, E, R>(backend: Backend, run: (input: ProjectFixture) => Effect.Effect<A, E, R>) {
+  return withProject(backend, { setup: writeStandardFiles }, run)
 }
 
-async function withFakeLlm<T>(
-  backend: Backend,
-  fn: (input: { sdk: Sdk; directory: string; llm: TestLLMServer["Service"] }) => Promise<T>,
-) {
-  return Effect.runPromise(
-    Effect.gen(function* () {
-      const llm = yield* TestLLMServer
-      const tmp = yield* Effect.acquireRelease(
-        Effect.promise(() => tmpdir({ git: true, config: providerConfig(llm.url) })),
-        (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
-      )
-      return yield* Effect.promise(() => fn({ sdk: client(backend, tmp.path), directory: tmp.path, llm }))
-    }).pipe(Effect.scoped, Effect.provide(TestLLMServer.layer)),
+function withFakeLlm<A, E, R>(backend: Backend, run: (input: LlmProjectFixture) => Effect.Effect<A, E, R>) {
+  return Effect.gen(function* () {
+    const llm = yield* TestLLMServer
+    return yield* withProject(backend, { config: providerConfig(llm.url) }, (input) => run({ ...input, llm }))
+  }).pipe(Effect.provide(TestLLMServer.layer))
+}
+
+function writeStandardFiles(dir: string) {
+  return Effect.all([
+    call(() => Bun.write(path.join(dir, "hello.txt"), "hello")),
+    call(() => Bun.write(path.join(dir, "needle.ts"), "export const needle = 'sdk-parity'\n")),
+  ]).pipe(Effect.asVoid)
+}
+
+function seedMessage(directory: string, sessionID: string) {
+  const id = SessionID.make(sessionID)
+  return call(async () =>
+    await Instance.provide({
+      directory,
+      fn: () =>
+        Effect.runPromise(
+          SessionNs.Service.use((svc) =>
+            Effect.gen(function* () {
+              const message = yield* svc.updateMessage({
+                id: MessageID.ascending(),
+                sessionID: id,
+                role: "user",
+                time: { created: Date.now() },
+                agent: "test",
+                model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
+                tools: {},
+              } satisfies MessageV2.User)
+              const part = yield* svc.updatePart({
+                id: PartID.ascending(),
+                sessionID: id,
+                messageID: message.id,
+                type: "text",
+                text: "seeded message",
+              })
+              return { message, part }
+            }),
+          ).pipe(Effect.provide(SessionNs.defaultLayer)),
+        ),
+    }),
   )
 }
 
@@ -202,113 +243,91 @@ afterEach(async () => {
 })
 
 describe("HttpApi SDK", () => {
-  test("uses the generated SDK for global and control routes", async () => {
-    const sdk = client("httpapi")
-    const health = await sdk.global.health()
-
-    expect(health.response.status).toBe(200)
-    expect(health.data).toMatchObject({ healthy: true })
-
-    const events = await sdk.global.event({ signal: AbortSignal.timeout(1_000) })
-    try {
-      const first = await events.stream.next()
-      expect(first.value).toMatchObject({ payload: { type: "server.connected" } })
-    } finally {
-      await events.stream.return(undefined)
-    }
-
-    const log = await sdk.app.log({ service: "httpapi-sdk-test", level: "info", message: "hello" })
-    expect(log.response.status).toBe(200)
-    expect(log.data).toBe(true)
-
-    await expectStatus(sdk.auth.set({ providerID: "test" }), 400)
-  })
+  httpapi(
+    "uses the generated SDK for global and control routes",
+    Effect.gen(function* () {
+      const sdk = client("httpapi")
+      const health = yield* call(() => sdk.global.health())
+      const log = yield* call(() => sdk.app.log({ service: "httpapi-sdk-test", level: "info", message: "hello" }))
+
+      expect(health.response.status).toBe(200)
+      expect(health.data).toMatchObject({ healthy: true })
+      expect(yield* firstEvent(() => sdk.global.event({ signal: AbortSignal.timeout(1_000) }))).toMatchObject({
+        payload: { type: "server.connected" },
+      })
+      expect(log.response.status).toBe(200)
+      expect(log.data).toBe(true)
+      yield* expectStatus(() => sdk.auth.set({ providerID: "test" }), 400)
+    }),
+  )
 
-  test("uses the generated SDK for safe instance routes", async () => {
-    await using tmp = await tmpdir({
-      config: { formatter: false, lsp: false },
-      init: (dir) => Bun.write(path.join(dir, "hello.txt"), "hello"),
-    })
-    const sdk = client("httpapi", tmp.path)
-
-    const file = await sdk.file.read({ path: "hello.txt" })
-    expect(file.response.status).toBe(200)
-    expect(file.data).toMatchObject({ content: "hello" })
-
-    const session = await sdk.session.create({ title: "sdk" })
-    expect(session.response.status).toBe(200)
-    expect(session.data).toMatchObject({ title: "sdk" })
-
-    const listed = await sdk.session.list({ roots: true, limit: 10 })
-    expect(listed.response.status).toBe(200)
-    expect(listed.data?.map((item) => item.id)).toContain(session.data?.id)
-
-    await Promise.all([
-      expectStatus(sdk.project.current(), 200),
-      expectStatus(sdk.config.get(), 200),
-      expectStatus(sdk.config.providers(), 200),
-      expectStatus(sdk.find.files({ query: "hello", limit: 10 }), 200),
-    ])
-  })
+  httpapi(
+    "uses the generated SDK for safe instance routes",
+    withProject("httpapi", { git: false, setup: writeStandardFiles }, ({ sdk }) =>
+      Effect.gen(function* () {
+        const file = yield* call(() => sdk.file.read({ path: "hello.txt" }))
+        const session = yield* call(() => sdk.session.create({ title: "sdk" }))
+        const listed = yield* call(() => sdk.session.list({ roots: true, limit: 10 }))
+
+        expect(file.response.status).toBe(200)
+        expect(file.data).toMatchObject({ content: "hello" })
+        expect(session.response.status).toBe(200)
+        expect(session.data).toMatchObject({ title: "sdk" })
+        expect(listed.response.status).toBe(200)
+        expect(listed.data?.map((item) => item.id)).toContain(session.data?.id)
+
+        yield* Effect.all([
+          expectStatus(() => sdk.project.current(), 200),
+          expectStatus(() => sdk.config.get(), 200),
+          expectStatus(() => sdk.config.providers(), 200),
+          expectStatus(() => sdk.find.files({ query: "hello", limit: 10 }), 200),
+        ])
+      }),
+    ),
+  )
 
-  test("matches generated SDK global and control behavior across backends", async () => {
-    await compareBackends(async (backend) => {
+  parity("matches generated SDK global and control behavior across backends", (backend) =>
+    Effect.gen(function* () {
       const sdk = client(backend)
-      const health = await capture(sdk.global.health())
-      const log = await capture(sdk.app.log({ service: "sdk-parity", level: "info", message: "hello" }))
-      const invalidAuth = await capture(sdk.auth.set({ providerID: "test" }))
+      const health = yield* capture(() => sdk.global.health())
+      const log = yield* capture(() => sdk.app.log({ service: "sdk-parity", level: "info", message: "hello" }))
+      const invalidAuth = yield* capture(() => sdk.auth.set({ providerID: "test" }))
 
       return {
         statuses: statuses({ health, log, invalidAuth }),
         health: record(health.data).healthy,
         log: log.data,
       }
-    })
-  })
+    }),
+  )
 
-  test("matches generated SDK global event stream across backends", async () => {
-    await compareBackends(async (backend) => {
-      const events = await client(backend).global.event({ signal: AbortSignal.timeout(1_000) })
-      try {
-        const first = await events.stream.next()
-        return {
-          type: record(record(first.value).payload).type,
-        }
-      } finally {
-        await events.stream.return(undefined)
-      }
-    })
-  })
+  parity("matches generated SDK global event stream across backends", (backend) =>
+    firstEvent(() => client(backend).global.event({ signal: AbortSignal.timeout(1_000) })).pipe(
+      Effect.map((event) => ({ type: record(record(event).payload).type })),
+    ),
+  )
 
-  test("matches generated SDK instance event stream across backends", async () => {
-    await compareBackends((backend) =>
-      withTmp(backend, async ({ sdk }) => {
-        const events = await sdk.event.subscribe(undefined, { signal: AbortSignal.timeout(1_000) })
-        try {
-          const first = await events.stream.next()
-          return {
-            type: record(record(first.value).payload).type,
-          }
-        } finally {
-          await events.stream.return(undefined)
-        }
-      }),
-    )
-  })
+  parity("matches generated SDK instance event stream across backends", (backend) =>
+    withStandardProject(backend, ({ sdk }) =>
+      firstEvent(() => sdk.event.subscribe(undefined, { signal: AbortSignal.timeout(1_000) })).pipe(
+        Effect.map((event) => ({ type: record(record(event).payload).type })),
+      ),
+    ),
+  )
 
-  test("matches generated SDK basic auth behavior across backends", async () => {
-    await compareBackends((backend) =>
-      withTmp(backend, async ({ directory }) => {
-        const missing = await capture(
+  parity("matches generated SDK basic auth behavior across backends", (backend) =>
+    withStandardProject(backend, ({ directory }) =>
+      Effect.gen(function* () {
+        const missing = yield* capture(() =>
           client(backend, directory, { password: "secret" }).file.read({ path: "hello.txt" }),
         )
-        const bad = await capture(
+        const bad = yield* capture(() =>
           client(backend, directory, {
             password: "secret",
             headers: { authorization: authorization("opencode", "wrong") },
           }).file.read({ path: "hello.txt" }),
         )
-        const good = await capture(
+        const good = yield* capture(() =>
           client(backend, directory, {
             password: "secret",
             headers: { authorization: authorization("opencode", "secret") },
@@ -320,28 +339,28 @@ describe("HttpApi SDK", () => {
           content: record(good.data).content,
         }
       }),
-    )
-  })
+    ),
+  )
 
-  test("matches generated SDK instance read routes across backends", async () => {
-    await compareBackends((backend) =>
-      withTmp(backend, async ({ sdk, directory }) => {
-        const project = await capture(sdk.project.current())
-        const projects = await capture(sdk.project.list())
-        const paths = await capture(sdk.path.get())
-        const config = await capture(sdk.config.get())
-        const providers = await capture(sdk.config.providers())
-        const file = await capture(sdk.file.read({ path: "hello.txt" }))
-        const files = await capture(sdk.file.list({ path: "." }))
-        const fileStatus = await capture(sdk.file.status())
-        const findFiles = await capture(sdk.find.files({ query: "hello", limit: 10 }))
-        const findText = await capture(sdk.find.text({ pattern: "sdk-parity" }))
-        const agents = await capture(sdk.app.agents())
-        const skills = await capture(sdk.app.skills())
-        const tools = await capture(sdk.tool.ids())
-        const vcs = await capture(sdk.vcs.get())
-        const formatter = await capture(sdk.formatter.status())
-        const lsp = await capture(sdk.lsp.status())
+  parity("matches generated SDK instance read routes across backends", (backend) =>
+    withStandardProject(backend, ({ sdk, directory }) =>
+      Effect.gen(function* () {
+        const project = yield* capture(() => sdk.project.current())
+        const projects = yield* capture(() => sdk.project.list())
+        const paths = yield* capture(() => sdk.path.get())
+        const config = yield* capture(() => sdk.config.get())
+        const providers = yield* capture(() => sdk.config.providers())
+        const file = yield* capture(() => sdk.file.read({ path: "hello.txt" }))
+        const files = yield* capture(() => sdk.file.list({ path: "." }))
+        const fileStatus = yield* capture(() => sdk.file.status())
+        const findFiles = yield* capture(() => sdk.find.files({ query: "hello", limit: 10 }))
+        const findText = yield* capture(() => sdk.find.text({ pattern: "sdk-parity" }))
+        const agents = yield* capture(() => sdk.app.agents())
+        const skills = yield* capture(() => sdk.app.skills())
+        const tools = yield* capture(() => sdk.tool.ids())
+        const vcs = yield* capture(() => sdk.vcs.get())
+        const formatter = yield* capture(() => sdk.formatter.status())
+        const lsp = yield* capture(() => sdk.lsp.status())
 
         return {
           statuses: statuses({
@@ -362,12 +381,8 @@ describe("HttpApi SDK", () => {
             formatter,
             lsp,
           }),
-          project: {
-            worktreeSelected: record(project.data).worktree === directory,
-          },
-          paths: {
-            cwdSelected: record(paths.data).cwd === directory,
-          },
+          project: { worktreeSelected: record(project.data).worktree === directory },
+          paths: { cwdSelected: record(paths.data).cwd === directory },
           file: record(file.data).content,
           hasProject: array(projects.data).length > 0,
           foundFile: JSON.stringify(findFiles.data).includes("hello.txt"),
@@ -375,29 +390,29 @@ describe("HttpApi SDK", () => {
           listedFile: JSON.stringify(files.data).includes("hello.txt"),
         }
       }),
-    )
-  })
+    ),
+  )
 
-  test("matches generated SDK session lifecycle routes across backends", async () => {
-    await compareBackends((backend) =>
-      withTmp(backend, async ({ sdk }) => {
-        const parent = await capture(sdk.session.create({ title: "parent" }))
+  parity("matches generated SDK session lifecycle routes across backends", (backend) =>
+    withStandardProject(backend, ({ sdk }) =>
+      Effect.gen(function* () {
+        const parent = yield* capture(() => sdk.session.create({ title: "parent" }))
         const parentID = String(record(parent.data).id)
-        const child = await capture(sdk.session.create({ title: "child", parentID }))
+        const child = yield* capture(() => sdk.session.create({ title: "child", parentID }))
         const childID = String(record(child.data).id)
-        const get = await capture(sdk.session.get({ sessionID: parentID }))
-        const update = await capture(sdk.session.update({ sessionID: parentID, title: "renamed" }))
-        const roots = await capture(sdk.session.list({ roots: true, limit: 10 }))
-        const all = await capture(sdk.session.list({ roots: false, limit: 10 }))
-        const children = await capture(sdk.session.children({ sessionID: parentID }))
-        const todo = await capture(sdk.session.todo({ sessionID: parentID }))
-        const status = await capture(sdk.session.status())
-        const messages = await capture(sdk.session.messages({ sessionID: parentID }))
-        const missingGet = await capture(sdk.session.get({ sessionID: "ses_missing" }))
-        const missingMessages = await capture(sdk.session.messages({ sessionID: "ses_missing", limit: 2 }))
-        const invalidCursor = await capture(sdk.session.messages({ sessionID: parentID, limit: 2, before: "bad" }))
-        const deleted = await capture(sdk.session.delete({ sessionID: childID }))
-        const getDeleted = await capture(sdk.session.get({ sessionID: childID }))
+        const get = yield* capture(() => sdk.session.get({ sessionID: parentID }))
+        const update = yield* capture(() => sdk.session.update({ sessionID: parentID, title: "renamed" }))
+        const roots = yield* capture(() => sdk.session.list({ roots: true, limit: 10 }))
+        const all = yield* capture(() => sdk.session.list({ roots: false, limit: 10 }))
+        const children = yield* capture(() => sdk.session.children({ sessionID: parentID }))
+        const todo = yield* capture(() => sdk.session.todo({ sessionID: parentID }))
+        const status = yield* capture(() => sdk.session.status())
+        const messages = yield* capture(() => sdk.session.messages({ sessionID: parentID }))
+        const missingGet = yield* capture(() => sdk.session.get({ sessionID: "ses_missing" }))
+        const missingMessages = yield* capture(() => sdk.session.messages({ sessionID: "ses_missing", limit: 2 }))
+        const invalidCursor = yield* capture(() => sdk.session.messages({ sessionID: parentID, limit: 2, before: "bad" }))
+        const deleted = yield* capture(() => sdk.session.delete({ sessionID: childID }))
+        const getDeleted = yield* capture(() => sdk.session.get({ sessionID: childID }))
 
         return {
           statuses: statuses({
@@ -426,36 +441,33 @@ describe("HttpApi SDK", () => {
           messageCount: array(messages.data).length,
         }
       }),
-    )
-  })
+    ),
+  )
 
-  test("matches generated SDK session message and part routes across backends", async () => {
-    await compareBackends((backend) =>
-      withTmp(backend, async ({ sdk, directory }) => {
-        const session = await capture(sdk.session.create({ title: "messages" }))
+  parity("matches generated SDK session message and part routes across backends", (backend) =>
+    withStandardProject(backend, ({ sdk, directory }) =>
+      Effect.gen(function* () {
+        const session = yield* capture(() => sdk.session.create({ title: "messages" }))
         const sessionID = String(record(session.data).id)
-        const seeded = await seedMessage(directory, sessionID)
-        const list = await capture(sdk.session.messages({ sessionID }))
-        const page = await capture(sdk.session.messages({ sessionID, limit: 1 }))
-        const message = await capture(sdk.session.message({ sessionID, messageID: seeded.message.id }))
-        const partUpdate = await capture(
+        const seeded = yield* seedMessage(directory, sessionID)
+        const list = yield* capture(() => sdk.session.messages({ sessionID }))
+        const page = yield* capture(() => sdk.session.messages({ sessionID, limit: 1 }))
+        const message = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
+        const partUpdate = yield* capture(() =>
           sdk.part.update({
             sessionID,
             messageID: seeded.message.id,
             partID: seeded.part.id,
-            part: {
-              ...seeded.part,
-              text: "updated message",
-            } as NonNullable<Parameters<Sdk["part"]["update"]>[0]["part"]>,
+            part: { ...seeded.part, text: "updated message" } as NonNullable<Parameters<Sdk["part"]["update"]>[0]["part"]>,
           }),
         )
-        const updated = await capture(sdk.session.message({ sessionID, messageID: seeded.message.id }))
-        const partDelete = await capture(
+        const updated = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
+        const partDelete = yield* capture(() =>
           sdk.part.delete({ sessionID, messageID: seeded.message.id, partID: seeded.part.id }),
         )
-        const withoutPart = await capture(sdk.session.message({ sessionID, messageID: seeded.message.id }))
-        const deleteMessage = await capture(sdk.session.deleteMessage({ sessionID, messageID: seeded.message.id }))
-        const missingMessage = await capture(sdk.session.message({ sessionID, messageID: seeded.message.id }))
+        const withoutPart = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
+        const deleteMessage = yield* capture(() => sdk.session.deleteMessage({ sessionID, messageID: seeded.message.id }))
+        const missingMessage = yield* capture(() => sdk.session.message({ sessionID, messageID: seeded.message.id }))
 
         return {
           statuses: statuses({
@@ -477,15 +489,15 @@ describe("HttpApi SDK", () => {
           partCountAfterDelete: array(record(withoutPart.data).parts).length,
         }
       }),
-    )
-  })
+    ),
+  )
 
-  test("matches generated SDK prompt no-reply routes across backends", async () => {
-    await compareBackends((backend) =>
-      withTmp(backend, async ({ sdk }) => {
-        const session = await capture(sdk.session.create({ title: "prompt" }))
+  parity("matches generated SDK prompt no-reply routes across backends", (backend) =>
+    withStandardProject(backend, ({ sdk }) =>
+      Effect.gen(function* () {
+        const session = yield* capture(() => sdk.session.create({ title: "prompt" }))
         const sessionID = String(record(session.data).id)
-        const prompt = await capture(
+        const prompt = yield* capture(() =>
           sdk.session.prompt({
             sessionID,
             agent: "build",
@@ -493,7 +505,7 @@ describe("HttpApi SDK", () => {
             parts: [{ type: "text", text: "hello" }],
           }),
         )
-        const asyncPrompt = await capture(
+        const asyncPrompt = yield* capture(() =>
           sdk.session.promptAsync({
             sessionID,
             agent: "build",
@@ -501,7 +513,7 @@ describe("HttpApi SDK", () => {
             parts: [{ type: "text", text: "async hello" }],
           }),
         )
-        const messages = await capture(sdk.session.messages({ sessionID }))
+        const messages = yield* capture(() => sdk.session.messages({ sessionID }))
 
         return {
           statuses: statuses({ session, prompt, asyncPrompt, messages }),
@@ -514,21 +526,21 @@ describe("HttpApi SDK", () => {
             .sort(),
         }
       }),
-    )
-  })
+    ),
+  )
 
-  test("matches generated SDK prompt streaming through fake LLM across backends", async () => {
-    await compareBackends((backend) =>
-      withFakeLlm(backend, async ({ sdk, llm }) => {
-        await Effect.runPromise(llm.text("fake world", { usage: { input: 11, output: 7 } }))
-        const session = await capture(
+  parity("matches generated SDK prompt streaming through fake LLM across backends", (backend) =>
+    withFakeLlm(backend, ({ sdk, llm }) =>
+      Effect.gen(function* () {
+        yield* llm.text("fake world", { usage: { input: 11, output: 7 } })
+        const session = yield* capture(() =>
           sdk.session.create({
             title: "llm prompt",
             permission: [{ permission: "*", pattern: "*", action: "allow" }],
           }),
         )
         const sessionID = String(record(session.data).id)
-        const prompt = await capture(
+        const prompt = yield* capture(() =>
           sdk.session.prompt({
             sessionID,
             agent: "build",
@@ -536,8 +548,8 @@ describe("HttpApi SDK", () => {
             parts: [{ type: "text", text: "hello llm" }],
           }),
         )
-        const messages = await capture(sdk.session.messages({ sessionID }))
-        const inputs = await Effect.runPromise(llm.inputs)
+        const messages = yield* capture(() => sdk.session.messages({ sessionID }))
+        const inputs = yield* llm.inputs
 
         return {
           statuses: statuses({ session, prompt, messages }),
@@ -548,26 +560,26 @@ describe("HttpApi SDK", () => {
           userText: JSON.stringify(messages.data).includes("hello llm"),
         }
       }),
-    )
-  })
+    ),
+  )
 
-  test("matches generated SDK TUI validation and command routes across backends", async () => {
-    await compareBackends((backend) =>
-      withTmp(backend, async ({ sdk }) => {
-        const session = await capture(sdk.session.create({ title: "tui" }))
+  parity("matches generated SDK TUI validation and command routes across backends", (backend) =>
+    withStandardProject(backend, ({ sdk }) =>
+      Effect.gen(function* () {
+        const session = yield* capture(() => sdk.session.create({ title: "tui" }))
         const sessionID = String(record(session.data).id)
-        const appendPrompt = await capture(sdk.tui.appendPrompt({ text: "hello" }))
-        const openHelp = await capture(sdk.tui.openHelp())
-        const openSessions = await capture(sdk.tui.openSessions())
-        const openThemes = await capture(sdk.tui.openThemes())
-        const openModels = await capture(sdk.tui.openModels())
-        const submitPrompt = await capture(sdk.tui.submitPrompt())
-        const clearPrompt = await capture(sdk.tui.clearPrompt())
-        const executeCommand = await capture(sdk.tui.executeCommand({ command: "session_new" }))
-        const showToast = await capture(sdk.tui.showToast({ title: "SDK", message: "hello", variant: "info" }))
-        const selectSession = await capture(sdk.tui.selectSession({ sessionID }))
-        const missingSession = await capture(sdk.tui.selectSession({ sessionID: "ses_missing" }))
-        const invalidSession = await capture(sdk.tui.selectSession({ sessionID: "invalid_session_id" }))
+        const appendPrompt = yield* capture(() => sdk.tui.appendPrompt({ text: "hello" }))
+        const openHelp = yield* capture(() => sdk.tui.openHelp())
+        const openSessions = yield* capture(() => sdk.tui.openSessions())
+        const openThemes = yield* capture(() => sdk.tui.openThemes())
+        const openModels = yield* capture(() => sdk.tui.openModels())
+        const submitPrompt = yield* capture(() => sdk.tui.submitPrompt())
+        const clearPrompt = yield* capture(() => sdk.tui.clearPrompt())
+        const executeCommand = yield* capture(() => sdk.tui.executeCommand({ command: "session_new" }))
+        const showToast = yield* capture(() => sdk.tui.showToast({ title: "SDK", message: "hello", variant: "info" }))
+        const selectSession = yield* capture(() => sdk.tui.selectSession({ sessionID }))
+        const missingSession = yield* capture(() => sdk.tui.selectSession({ sessionID: "ses_missing" }))
+        const invalidSession = yield* capture(() => sdk.tui.selectSession({ sessionID: "invalid_session_id" }))
 
         return {
           statuses: statuses({
@@ -599,32 +611,32 @@ describe("HttpApi SDK", () => {
           },
         }
       }),
-    )
-  })
+    ),
+  )
 
-  test("matches generated SDK project git initialization across backends", async () => {
-    await compareBackends(async (backend) => {
-      await using tmp = await tmpdir({ config: { formatter: false, lsp: false } })
-      const sdk = client(backend, tmp.path)
-      const before = await capture(sdk.project.current())
-      const init = await capture(sdk.project.initGit())
-      const after = await capture(sdk.project.current())
+  parity("matches generated SDK project git initialization across backends", (backend) =>
+    withProject(backend, { git: false }, ({ sdk, directory }) =>
+      Effect.gen(function* () {
+        const before = yield* capture(() => sdk.project.current())
+        const init = yield* capture(() => sdk.project.initGit())
+        const after = yield* capture(() => sdk.project.current())
 
-      return {
-        statuses: statuses({ before, init, after }),
-        before: {
-          vcs: record(before.data).vcs ?? null,
-          worktree: record(before.data).worktree,
-        },
-        init: {
-          vcs: record(init.data).vcs,
-          worktreeSelected: record(init.data).worktree === tmp.path,
-        },
-        after: {
-          vcs: record(after.data).vcs,
-          worktreeSelected: record(after.data).worktree === tmp.path,
-        },
-      }
-    })
-  })
+        return {
+          statuses: statuses({ before, init, after }),
+          before: {
+            vcs: record(before.data).vcs ?? null,
+            worktree: record(before.data).worktree,
+          },
+          init: {
+            vcs: record(init.data).vcs,
+            worktreeSelected: record(init.data).worktree === directory,
+          },
+          after: {
+            vcs: record(after.data).vcs,
+            worktreeSelected: record(after.data).worktree === directory,
+          },
+        }
+      }),
+    ),
+  )
 })