import { afterEach, describe, expect, mock } from "bun:test" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Effect, Fiber, Layer } from "effect" import { Session as SessionNs } from "@/session/session" import { Job } from "@/job" import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { pollWithTimeout, testEffect } from "../lib/effect" import { httpApiLayer, requestInDirectory } from "./httpapi-layer" const it = testEffect(Layer.mergeAll(LayerNode.compile(SessionNs.node), LayerNode.compile(Job.node), httpApiLayer)) afterEach(async () => { mock.restore() await disposeAllInstances() }) describe("session action routes", () => { it.instance( "reports background subagents as available", () => Effect.gen(function* () { const test = yield* TestInstance const res = yield* requestInDirectory("/experimental/capabilities", test.directory) expect(res.status).toBe(200) expect(yield* res.json).toEqual({ backgroundSubagents: true }) }), { git: true }, ) it.instance( "session routes expose metadata on create, update, get, and fork", () => Effect.gen(function* () { const test = yield* TestInstance const headers = { "Content-Type": "application/json" } const created = yield* requestInDirectory("/session", test.directory, { method: "POST", headers, body: JSON.stringify({ title: "meta-session", metadata: { source: "sdk", trace: { id: "abc" } }, }), }) expect(created.status).toBe(200) const session = (yield* created.json) as SessionNs.Info expect(session.metadata).toEqual({ source: "sdk", trace: { id: "abc" } }) const updated = yield* requestInDirectory(`/session/${session.id}`, test.directory, { method: "PATCH", headers, body: JSON.stringify({ metadata: { source: "sdk", trace: { id: "def" }, tags: ["one"] } }), }) expect(updated.status).toBe(200) const next = (yield* updated.json) as SessionNs.Info expect(next.metadata).toEqual({ source: "sdk", trace: { id: "def" }, tags: ["one"] }) const fetched = yield* requestInDirectory(`/session/${session.id}`, test.directory) expect(fetched.status).toBe(200) expect(((yield* fetched.json) as SessionNs.Info).metadata).toEqual(next.metadata) const forked = yield* requestInDirectory(`/session/${session.id}/fork`, test.directory, { method: "POST", headers, body: JSON.stringify({}), }) expect(forked.status).toBe(200) const fork = (yield* forked.json) as SessionNs.Info expect(fork.metadata).toEqual(next.metadata) const reset = yield* requestInDirectory(`/session/${session.id}`, test.directory, { method: "PATCH", headers, body: JSON.stringify({ metadata: {} }), }) expect(reset.status).toBe(200) expect(((yield* reset.json) as SessionNs.Info).metadata).toEqual({}) yield* SessionNs.Service.use((svc) => svc.remove(fork.id).pipe(Effect.ignore)) yield* SessionNs.Service.use((svc) => svc.remove(session.id).pipe(Effect.ignore)) }), { git: true }, ) it.instance( "abort route returns success", () => Effect.gen(function* () { const test = yield* TestInstance const session = yield* Effect.acquireRelease(SessionNs.use.create({}), (created) => SessionNs.use.remove(created.id).pipe(Effect.ignore), ) const res = yield* requestInDirectory(`/session/${session.id}/abort`, test.directory, { method: "POST" }) expect(res.status).toBe(200) expect(yield* res.json).toBe(true) }), { git: true }, ) it.instance( "experimental background route is a no-op without synchronous subagents", () => Effect.gen(function* () { const test = yield* TestInstance const session = yield* Effect.acquireRelease(SessionNs.use.create({}), (created) => SessionNs.use.remove(created.id).pipe(Effect.ignore), ) const res = yield* requestInDirectory(`/experimental/session/${session.id}/background`, test.directory, { method: "POST", }) expect(res.status).toBe(200) expect(yield* res.json).toBe(false) }), { git: true }, ) it.instance( "experimental background route backgrounds a synchronous subagent", () => Effect.gen(function* () { const test = yield* TestInstance const session = yield* Effect.acquireRelease(SessionNs.use.create({}), (created) => SessionNs.use.remove(created.id).pipe(Effect.ignore), ) const jobs = yield* Job.Service const job = yield* jobs.start({ type: "task", run: Effect.never }) const waiting = yield* jobs.block({ id: job.id, sessionID: session.id }).pipe(Effect.forkChild) const backgrounded = yield* pollWithTimeout( requestInDirectory(`/experimental/session/${session.id}/background`, test.directory, { method: "POST", }).pipe( Effect.flatMap((res) => res.json), Effect.map((value) => (value === true ? true : undefined)), ), "background route never released the synchronous subagent", ) expect(backgrounded).toBe(true) expect(yield* Fiber.join(waiting)).toMatchObject({ type: "backgrounded", info: { id: job.id } }) yield* jobs.cancel(job.id) }), { git: true }, ) })