From d3df8e118066b941628e4f6aa9ac8c5939df62a7 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 29 Apr 2026 09:46:17 -0400 Subject: [PATCH] test(httpapi): clean up SDK parity tests --- .../opencode/test/server/httpapi-sdk.test.ts | 586 +++++++++--------- 1 file changed, 299 insertions(+), 287 deletions(-) diff --git a/packages/opencode/test/server/httpapi-sdk.test.ts b/packages/opencode/test/server/httpapi-sdk.test.ts index c0984170be..e96ea6c889 100644 --- a/packages/opencode/test/server/httpapi-sdk.test.ts +++ b/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 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(request: () => Promise) { + return Effect.promise(request) } -async function capture(result: Promise) { - const response = await result - return { - status: response.response.status, - data: response.data, - error: response.error, - } +function capture(request: () => Promise) { + 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 }>) { + 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>>) { +function statuses(input: Record) { return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, value.status])) } @@ -121,75 +146,91 @@ function sessionTitles(value: unknown) { .sort() } -async function runSession(directory: string, effect: Effect.Effect) { - 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(name: string, effect: Effect.Effect) { + it.live(name, effect) +} + +function parity(name: string, scenario: (backend: Backend) => Effect.Effect) { + 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(scenario: (backend: Backend) => Promise) { - const legacy = await scenario("legacy") - await Instance.disposeAll() - await resetDatabase() - const httpapi = await scenario("httpapi") - expect(httpapi).toEqual(legacy) -} - -async function withTmp(backend: Backend, fn: (input: { sdk: Sdk; directory: string }) => Promise) { - 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 }) -} - -async function withFakeLlm( +function withProject( backend: Backend, - fn: (input: { sdk: Sdk; directory: string; llm: TestLLMServer["Service"] }) => Promise, + options: { git?: boolean; config?: Partial; setup?: (dir: string) => Effect.Effect }, + run: (input: ProjectFixture) => Effect.Effect, ) { - 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)), + 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 })), + ) +} + +function withStandardProject(backend: Backend, run: (input: ProjectFixture) => Effect.Effect) { + return withProject(backend, { setup: writeStandardFiles }, run) +} + +function withFakeLlm(backend: Backend, run: (input: LlmProjectFixture) => Effect.Effect) { + 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() + 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(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) + }), + ) - 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) - } + 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 })) - 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) + 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) - await expectStatus(sdk.auth.set({ providerID: "test" }), 400) - }) + 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("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), - ]) - }) - - 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[0]["part"]>, + part: { ...seeded.part, text: "updated message" } as NonNullable[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, + }, + } + }), + ), + ) })