| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840 |
- import { afterEach, describe, expect } from "bun:test"
- import { ConfigProvider, Effect, Layer } from "effect"
- import type * as Scope from "effect/Scope"
- import { HttpRouter } from "effect/unstable/http"
- import { ChildProcessSpawner } from "effect/unstable/process"
- import { AppFileSystem } from "@opencode-ai/core/filesystem"
- import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
- import { Flag } from "@opencode-ai/core/flag/flag"
- import { createOpencodeClient } from "@opencode-ai/sdk/v2"
- import { validateSession } from "../../src/cli/cmd/tui/validate-session"
- import { InstanceBootstrap } from "../../src/project/bootstrap-service"
- import { InstanceStore } from "../../src/project/instance-store"
- import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
- 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 { errorMessage } from "../../src/util/error"
- import { TestLLMServer } from "../lib/llm-server"
- import path from "path"
- import { resetDatabase } from "../fixture/db"
- import { disposeAllInstances, TestInstance, tmpdirScoped } from "../fixture/fixture"
- import { testEffect } from "../lib/effect"
- const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
- const it = testEffect(
- Layer.mergeAll(
- AppFileSystem.defaultLayer,
- CrossSpawnSpawner.defaultLayer,
- InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)),
- ),
- )
- const original = {
- OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
- OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
- }
- type ServerPath = "default" | "raw"
- 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"] }
- type TestServices = AppFileSystem.Service | ChildProcessSpawner.ChildProcessSpawner | InstanceStore.Service
- type TestScope = Scope.Scope | TestServices
- function app(serverPath: ServerPath, input?: { password?: string; username?: string }) {
- Flag.OPENCODE_SERVER_PASSWORD = input?.password
- Flag.OPENCODE_SERVER_USERNAME = input?.username
- if (serverPath === "default") return Server.Default().app
- const handler = HttpRouter.toWebHandler(
- ExperimentalHttpApiServer.routes.pipe(
- Layer.provide(
- ConfigProvider.layer(
- ConfigProvider.fromUnknown({
- OPENCODE_SERVER_PASSWORD: input?.password,
- OPENCODE_SERVER_USERNAME: input?.username,
- }),
- ),
- ),
- ),
- { disableLogger: true },
- ).handler
- return {
- fetch: (request: Request) => handler(request, ExperimentalHttpApiServer.context),
- request(input: string | URL | Request, init?: RequestInit) {
- return this.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init))
- },
- }
- }
- function client(
- serverPath: ServerPath,
- directory?: string,
- input?: { password?: string; username?: string; headers?: Record<string, string> },
- ) {
- return createOpencodeClient({
- baseUrl: "http://localhost",
- directory,
- headers: input?.headers,
- fetch: serverFetch(serverPath, input),
- })
- }
- function serverFetch(serverPath: ServerPath, input?: { password?: string; username?: string }) {
- const serverApp = app(serverPath, input)
- return Object.assign(
- async (request: RequestInfo | URL, init?: RequestInit) =>
- await serverApp.fetch(request instanceof Request ? request : new Request(request, init)),
- { preconnect: globalThis.fetch.preconnect },
- ) satisfies typeof globalThis.fetch
- }
- function authorization(username: string, password: string) {
- return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
- }
- function providerConfig(url: string) {
- return {
- formatter: false,
- lsp: false,
- provider: {
- test: {
- name: "Test",
- id: "test",
- env: [],
- npm: "@ai-sdk/openai-compatible",
- models: {
- "test-model": {
- id: "test-model",
- name: "Test Model",
- attachment: false,
- reasoning: false,
- temperature: false,
- tool_call: true,
- release_date: "2025-01-01",
- limit: { context: 100000, output: 10000 },
- cost: { input: 0, output: 0 },
- options: {},
- },
- },
- options: {
- apiKey: "test-key",
- baseURL: url,
- },
- },
- },
- }
- }
- function call<T>(request: () => Promise<T>) {
- return Effect.promise(request)
- }
- function capture(request: () => Promise<SdkResult>) {
- return call(request).pipe(
- Effect.map((result) => ({
- status: result.response.status,
- data: result.data,
- error: result.error,
- })),
- )
- }
- function captureThrown(request: () => Promise<unknown>) {
- return call(async () => {
- try {
- await request()
- } catch (error) {
- return 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: (signal: AbortSignal) => Promise<{ stream: AsyncIterator<unknown> }>) {
- return Effect.acquireRelease(
- Effect.sync(() => new AbortController()),
- (controller) => Effect.sync(() => controller.abort()),
- ).pipe(
- Effect.flatMap((controller) =>
- Effect.acquireRelease(call(() => open(controller.signal)), (events) =>
- call(async () => void (await events.stream.return?.(undefined))).pipe(Effect.ignore),
- ).pipe(
- Effect.flatMap((events) =>
- call(() => events.stream.next()).pipe(
- Effect.timeoutOrElse({
- duration: "1 second",
- orElse: () => Effect.fail(new Error("timed out waiting for SDK event")),
- }),
- ),
- ),
- Effect.map((result) => result.value),
- ),
- ),
- )
- }
- function record(value: unknown) {
- return value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : {}
- }
- function array(value: unknown) {
- return Array.isArray(value) ? value : []
- }
- function statuses(input: Record<string, Captured>) {
- return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, value.status]))
- }
- function firstPartText(value: unknown) {
- return record(array(record(value).parts)[0]).text
- }
- function sessionTitles(value: unknown) {
- return array(value)
- .map((item) => record(item).title)
- .filter((title): title is string => typeof title === "string")
- .sort()
- }
- function resetState() {
- return Effect.promise(async () => {
- await disposeAllInstances()
- await resetDatabase()
- })
- }
- function httpapi<A, E>(name: string, effect: Effect.Effect<A, E, TestScope>) {
- it.live(name, effect)
- }
- function httpapiInstance<A, E>(
- name: string,
- options: {
- serverPath: ServerPath
- git?: boolean
- config?: Partial<Config.Info>
- setup?: (dir: string) => Effect.Effect<void, E, TestServices>
- },
- run: (input: ProjectFixture) => Effect.Effect<A, E, TestScope>,
- ) {
- it.instance(
- name,
- Effect.gen(function* () {
- const instance = yield* TestInstance
- yield* (options.setup?.(instance.directory) ?? Effect.void)
- return yield* run({ sdk: client(options.serverPath, instance.directory), directory: instance.directory })
- }),
- { git: options.git ?? true, config: { formatter: false, lsp: false, ...options.config } },
- )
- }
- function serverPathParity<A, E>(name: string, scenario: (serverPath: ServerPath) => Effect.Effect<A, E, TestScope>) {
- it.live(
- name,
- Effect.gen(function* () {
- const standard = yield* scenario("default")
- yield* resetState()
- const raw = yield* scenario("raw")
- expect(raw).toEqual(standard)
- }),
- )
- }
- function withProject<A, E, E2 = never>(
- serverPath: ServerPath,
- options: { git?: boolean; config?: Partial<Config.Info>; setup?: (dir: string) => Effect.Effect<void, E2, TestServices> },
- run: (input: ProjectFixture) => Effect.Effect<A, E, TestScope>,
- ) {
- return Effect.gen(function* () {
- const directory = yield* tmpdirScoped({ git: options.git ?? true, config: { formatter: false, lsp: false, ...options.config } })
- yield* (options.setup?.(directory) ?? Effect.void)
- return yield* run({ sdk: client(serverPath, directory), directory })
- })
- }
- function withStandardProject<A, E>(serverPath: ServerPath, run: (input: ProjectFixture) => Effect.Effect<A, E, TestScope>) {
- return withProject(serverPath, { setup: writeStandardFiles }, run)
- }
- function withFakeLlm<A, E>(serverPath: ServerPath, run: (input: LlmProjectFixture) => Effect.Effect<A, E, TestScope>) {
- return Effect.gen(function* () {
- const llm = yield* TestLLMServer
- return yield* withProject(serverPath, { config: providerConfig(llm.url) }, (input) => run({ ...input, llm }))
- }).pipe(Effect.provide(TestLLMServer.layer))
- }
- function withFakeLlmProject<A, E>(
- serverPath: ServerPath,
- options: { setup?: (dir: string) => Effect.Effect<void, E, TestServices> },
- run: (input: LlmProjectFixture) => Effect.Effect<A, E, TestScope>,
- ) {
- return Effect.gen(function* () {
- const llm = yield* TestLLMServer
- return yield* withProject(
- serverPath,
- {
- config: providerConfig(llm.url),
- setup: options.setup,
- },
- (input) => run({ ...input, llm }),
- )
- }).pipe(Effect.provide(TestLLMServer.layer))
- }
- function writeStandardFiles(dir: string) {
- return AppFileSystem.Service.use((fs) =>
- Effect.all([
- fs.writeWithDirs(path.join(dir, "hello.txt"), "hello"),
- fs.writeWithDirs(path.join(dir, "needle.ts"), "export const needle = 'sdk-parity'\n"),
- ]).pipe(Effect.asVoid),
- )
- }
- function writeProjectSkill(dir: string) {
- return AppFileSystem.Service.use((fs) =>
- fs.writeWithDirs(
- path.join(dir, ".opencode", "skills", "project-rest-skill", "SKILL.md"),
- `---
- name: project-rest-skill
- description: A project skill visible to REST API prompts.
- ---
- # Project REST Skill
- `,
- ),
- )
- }
- function seedMessage(directory: string, sessionID: string) {
- const id = SessionID.make(sessionID)
- return InstanceStore.Service.use((store) =>
- store.provide(
- { 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: {},
- } 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)),
- ),
- )
- }
- afterEach(async () => {
- Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
- Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
- await disposeAllInstances()
- await resetDatabase()
- })
- describe("HttpApi SDK", () => {
- httpapi(
- "uses the generated SDK for global and control routes",
- Effect.gen(function* () {
- const sdk = client("raw")
- 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((signal) => sdk.global.event({ signal }))).toMatchObject({
- payload: { type: "server.connected" },
- })
- expect(log.response.status).toBe(200)
- expect(log.data).toBe(true)
- yield* expectStatus(() => sdk.auth.set({ providerID: "test" }), 400)
- }),
- )
- httpapiInstance(
- "uses the generated SDK for safe instance routes",
- { serverPath: "raw", 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),
- ])
- }),
- )
- serverPathParity("matches generated SDK global and control behavior", (serverPath) =>
- Effect.gen(function* () {
- const sdk = client(serverPath)
- 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,
- }
- }),
- )
- serverPathParity("matches generated SDK global event stream", (serverPath) =>
- firstEvent((signal) => client(serverPath).global.event({ signal })).pipe(
- Effect.map((event) => ({ type: record(record(event).payload).type })),
- ),
- )
- serverPathParity("matches generated SDK instance event stream", (serverPath) =>
- withStandardProject(serverPath, ({ sdk }) =>
- firstEvent((signal) => sdk.event.subscribe(undefined, { signal })).pipe(
- Effect.map((event) => ({ type: record(record(event).payload).type })),
- ),
- ),
- )
- serverPathParity("matches generated SDK missing session errors", (serverPath) =>
- withStandardProject(serverPath, ({ sdk }) =>
- Effect.gen(function* () {
- const sessionID = "ses_missing"
- const expected = {
- name: "NotFoundError",
- data: { message: `Session not found: ${sessionID}` },
- }
- const missing = yield* capture(() => sdk.session.get({ sessionID }))
- const thrown = yield* captureThrown(() => sdk.session.get({ sessionID }, { throwOnError: true }))
- // Result-tuple path: error body is preserved as-is so existing
- // consumers reading `result.error.name` / `JSON.stringify(error)`
- // keep working byte-for-byte.
- expect(missing.error).toEqual(expected)
- // throwOnError path: SDK wraps the body in a real Error with the
- // server's message, with the original parsed body preserved under
- // `.cause.body`.
- expect(thrown).toBeInstanceOf(Error)
- expect((thrown as Error).message).toBe(expected.data.message)
- expect(((thrown as Error).cause as { body: unknown }).body).toEqual(expected)
- return {
- status: missing.status,
- error: missing.error,
- thrown,
- }
- }),
- ),
- )
- serverPathParity("formats missing session validation errors for -s", (serverPath) =>
- withStandardProject(serverPath, ({ directory }) =>
- Effect.gen(function* () {
- const sessionID = "ses_206f84f18ffeZ6hhD7pFYAiW5T"
- const thrown = yield* captureThrown(() =>
- validateSession({
- url: "http://localhost",
- directory,
- sessionID,
- fetch: serverFetch(serverPath),
- }),
- )
- expect(errorMessage(thrown)).toBe(`Session not found: ${sessionID}`)
- return errorMessage(thrown)
- }),
- ),
- )
- httpapiInstance(
- "uses generated SDK basic auth behavior",
- { serverPath: "raw", setup: writeStandardFiles },
- ({ directory }) =>
- Effect.gen(function* () {
- const missing = yield* capture(() =>
- client("raw", directory, { password: "secret" }).file.read({ path: "hello.txt" }),
- )
- const bad = yield* capture(() =>
- client("raw", directory, {
- password: "secret",
- headers: { authorization: authorization("opencode", "wrong") },
- }).file.read({ path: "hello.txt" }),
- )
- const good = yield* capture(() =>
- client("raw", directory, {
- password: "secret",
- headers: { authorization: authorization("opencode", "secret") },
- }).file.read({ path: "hello.txt" }),
- )
- return {
- statuses: statuses({ missing, bad, good }),
- content: record(good.data).content,
- }
- }),
- )
- serverPathParity("matches generated SDK instance read routes", (serverPath) =>
- withStandardProject(serverPath, ({ 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({
- project,
- projects,
- paths,
- config,
- providers,
- file,
- files,
- fileStatus,
- findFiles,
- findText,
- agents,
- skills,
- tools,
- vcs,
- formatter,
- lsp,
- }),
- project: { worktreeSelected: record(project.data).worktree === directory },
- paths: { directorySelected: record(paths.data).directory === directory },
- file: record(file.data).content,
- hasProject: array(projects.data).length > 0,
- foundFile: JSON.stringify(findFiles.data).includes("hello.txt"),
- foundText: JSON.stringify(findText.data ?? null).includes("sdk-parity"),
- listedFile: JSON.stringify(files.data).includes("hello.txt"),
- }
- }),
- ),
- )
- serverPathParity("matches generated SDK session lifecycle routes", (serverPath) =>
- withStandardProject(serverPath, ({ sdk }) =>
- Effect.gen(function* () {
- const parent = yield* capture(() => sdk.session.create({ title: "parent" }))
- const parentID = String(record(parent.data).id)
- const child = yield* capture(() => sdk.session.create({ title: "child", parentID }))
- const childID = String(record(child.data).id)
- 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({
- parent,
- child,
- get,
- update,
- roots,
- all,
- children,
- todo,
- status,
- messages,
- missingGet,
- missingMessages,
- invalidCursor,
- deleted,
- getDeleted,
- }),
- getTitle: record(get.data).title,
- updatedTitle: record(update.data).title,
- rootTitles: sessionTitles(roots.data),
- allTitles: sessionTitles(all.data),
- childCount: array(children.data).length,
- todoCount: array(todo.data).length,
- messageCount: array(messages.data).length,
- }
- }),
- ),
- )
- serverPathParity("matches generated SDK session message and part routes", (serverPath) =>
- withStandardProject(serverPath, ({ sdk, directory }) =>
- Effect.gen(function* () {
- const session = yield* capture(() => sdk.session.create({ title: "messages" }))
- const sessionID = String(record(session.data).id)
- 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"]
- >,
- }),
- )
- 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 = 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({
- session,
- list,
- page,
- message,
- partUpdate,
- updated,
- partDelete,
- withoutPart,
- deleteMessage,
- missingMessage,
- }),
- listCount: array(list.data).length,
- pageCount: array(page.data).length,
- initialText: firstPartText(message.data),
- updatedText: firstPartText(updated.data),
- partCountAfterDelete: array(record(withoutPart.data).parts).length,
- }
- }),
- ),
- )
- serverPathParity("matches generated SDK prompt no-reply routes", (serverPath) =>
- withStandardProject(serverPath, ({ sdk }) =>
- Effect.gen(function* () {
- const session = yield* capture(() => sdk.session.create({ title: "prompt" }))
- const sessionID = String(record(session.data).id)
- const prompt = yield* capture(() =>
- sdk.session.prompt({
- sessionID,
- agent: "build",
- noReply: true,
- parts: [{ type: "text", text: "hello" }],
- }),
- )
- const asyncPrompt = yield* capture(() =>
- sdk.session.promptAsync({
- sessionID,
- agent: "build",
- noReply: true,
- parts: [{ type: "text", text: "async hello" }],
- }),
- )
- const messages = yield* capture(() => sdk.session.messages({ sessionID }))
- return {
- statuses: statuses({ session, prompt, asyncPrompt, messages }),
- promptRole: record(record(prompt.data).info).role,
- messageCount: array(messages.data).length,
- messageTexts: array(messages.data)
- .flatMap((item) => array(record(item).parts))
- .map((part) => record(part).text)
- .filter((text): text is string => typeof text === "string")
- .sort(),
- }
- }),
- ),
- )
- serverPathParity("matches generated SDK prompt streaming through fake LLM", (serverPath) =>
- withFakeLlm(serverPath, ({ 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 = yield* capture(() =>
- sdk.session.prompt({
- sessionID,
- agent: "build",
- model: { providerID: "test", modelID: "test-model" },
- parts: [{ type: "text", text: "hello llm" }],
- }),
- )
- const messages = yield* capture(() => sdk.session.messages({ sessionID }))
- const inputs = yield* llm.inputs
- return {
- statuses: statuses({ session, prompt, messages }),
- calls: inputs.length,
- requestedModel: inputs[0]?.model,
- responseText: JSON.stringify(prompt.data).includes("fake world"),
- persistedText: JSON.stringify(messages.data).includes("fake world"),
- userText: JSON.stringify(messages.data).includes("hello llm"),
- }
- }),
- ),
- )
- httpapi(
- "includes project skills in REST API async prompt context",
- withFakeLlmProject("default", { setup: writeProjectSkill }, ({ sdk, llm }) =>
- Effect.gen(function* () {
- yield* llm.text("skill context ok", { usage: { input: 11, output: 7 } })
- const session = yield* capture(() =>
- sdk.session.create({
- title: "project skill prompt",
- permission: [{ permission: "*", pattern: "*", action: "allow" }],
- }),
- )
- const sessionID = String(record(session.data).id)
- const prompt = yield* capture(() =>
- sdk.session.promptAsync({
- sessionID,
- agent: "build",
- model: { providerID: "test", modelID: "test-model" },
- parts: [{ type: "text", text: "hello skill context" }],
- }),
- )
- yield* llm.wait(1)
- const inputs = yield* llm.inputs
- expect(session.status).toBe(200)
- expect(prompt.status).toBe(204)
- expect(JSON.stringify(inputs[0])).toContain("project-rest-skill")
- }),
- ),
- )
- serverPathParity("matches generated SDK TUI validation and command routes", (serverPath) =>
- withStandardProject(serverPath, ({ sdk }) =>
- Effect.gen(function* () {
- const session = yield* capture(() => sdk.session.create({ title: "tui" }))
- const sessionID = String(record(session.data).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({
- session,
- appendPrompt,
- openHelp,
- openSessions,
- openThemes,
- openModels,
- submitPrompt,
- clearPrompt,
- executeCommand,
- showToast,
- selectSession,
- missingSession,
- invalidSession,
- }),
- data: {
- appendPrompt: appendPrompt.data,
- openHelp: openHelp.data,
- openSessions: openSessions.data,
- openThemes: openThemes.data,
- openModels: openModels.data,
- submitPrompt: submitPrompt.data,
- clearPrompt: clearPrompt.data,
- executeCommand: executeCommand.data,
- showToast: showToast.data,
- selectSession: selectSession.data,
- },
- }
- }),
- ),
- )
- serverPathParity("matches generated SDK project git initialization", (serverPath) =>
- withProject(serverPath, { 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 === directory,
- },
- after: {
- vcs: record(after.data).vcs,
- worktreeSelected: record(after.data).worktree === directory,
- },
- }
- }),
- ),
- )
- })
|