|
|
@@ -1,4 +1,4 @@
|
|
|
-import { afterEach, describe, expect, test } from "bun:test"
|
|
|
+import { afterEach, describe, expect } from "bun:test"
|
|
|
import type { UpgradeWebSocket } from "hono/ws"
|
|
|
import { Effect } from "effect"
|
|
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
|
@@ -11,7 +11,8 @@ import { MessageID, PartID } from "../../src/session/schema"
|
|
|
import { Session } from "@/session/session"
|
|
|
import * as Log from "@opencode-ai/core/util/log"
|
|
|
import { resetDatabase } from "../fixture/db"
|
|
|
-import { tmpdir } from "../fixture/fixture"
|
|
|
+import { provideInstance, tmpdir } from "../fixture/fixture"
|
|
|
+import { it } from "../lib/effect"
|
|
|
|
|
|
void Log.init({ print: false })
|
|
|
|
|
|
@@ -23,70 +24,63 @@ function app(experimental: boolean) {
|
|
|
return InstanceRoutes(websocket)
|
|
|
}
|
|
|
|
|
|
-function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>) {
|
|
|
- return Effect.runPromise(fx.pipe(Effect.provide(Session.defaultLayer)))
|
|
|
-}
|
|
|
-
|
|
|
function pathFor(path: string, params: Record<string, string>) {
|
|
|
return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), path)
|
|
|
}
|
|
|
|
|
|
-async function seedSessions(directory: string) {
|
|
|
- return await Instance.provide({
|
|
|
- directory,
|
|
|
- fn: () =>
|
|
|
- runSession(
|
|
|
- Effect.gen(function* () {
|
|
|
- const svc = yield* Session.Service
|
|
|
- const parent = yield* svc.create({ title: "parent" })
|
|
|
- yield* svc.create({ title: "child", parentID: parent.id })
|
|
|
- const message = yield* svc.updateMessage({
|
|
|
- id: MessageID.ascending(),
|
|
|
- role: "user",
|
|
|
- sessionID: parent.id,
|
|
|
- agent: "build",
|
|
|
- model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
|
|
- time: { created: Date.now() },
|
|
|
- })
|
|
|
- yield* svc.updatePart({
|
|
|
- id: PartID.ascending(),
|
|
|
- sessionID: parent.id,
|
|
|
- messageID: message.id,
|
|
|
- type: "text",
|
|
|
- text: "hello",
|
|
|
- })
|
|
|
- return { parent, message }
|
|
|
- }),
|
|
|
- ),
|
|
|
+const seedSessions = Effect.gen(function* () {
|
|
|
+ const svc = yield* Session.Service
|
|
|
+ const parent = yield* svc.create({ title: "parent" })
|
|
|
+ yield* svc.create({ title: "child", parentID: parent.id })
|
|
|
+ const message = yield* svc.updateMessage({
|
|
|
+ id: MessageID.ascending(),
|
|
|
+ role: "user",
|
|
|
+ sessionID: parent.id,
|
|
|
+ agent: "build",
|
|
|
+ model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
|
|
|
+ time: { created: Date.now() },
|
|
|
})
|
|
|
-}
|
|
|
+ yield* svc.updatePart({
|
|
|
+ id: PartID.ascending(),
|
|
|
+ sessionID: parent.id,
|
|
|
+ messageID: message.id,
|
|
|
+ type: "text",
|
|
|
+ text: "hello",
|
|
|
+ })
|
|
|
+ return { parent, message }
|
|
|
+})
|
|
|
|
|
|
-async function readJson(
|
|
|
- label: string,
|
|
|
- app: ReturnType<typeof InstanceRoutes>,
|
|
|
- directory: string,
|
|
|
- path: string,
|
|
|
- headers: HeadersInit,
|
|
|
+function withTmp<A, E, R>(
|
|
|
+ options: Parameters<typeof tmpdir>[0],
|
|
|
+ fn: (tmp: Awaited<ReturnType<typeof tmpdir>>) => Effect.Effect<A, E, R>,
|
|
|
) {
|
|
|
- const response = await Instance.provide({
|
|
|
- directory,
|
|
|
- fn: () => app.request(path, { headers }),
|
|
|
+ return Effect.acquireRelease(
|
|
|
+ Effect.promise(() => tmpdir(options)),
|
|
|
+ (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
|
+ ).pipe(Effect.flatMap((tmp) => fn(tmp).pipe(provideInstance(tmp.path))))
|
|
|
+}
|
|
|
+
|
|
|
+function readJson(label: string, app: ReturnType<typeof InstanceRoutes>, path: string, headers: HeadersInit) {
|
|
|
+ return Effect.promise(async () => {
|
|
|
+ const response = await app.request(path, { headers })
|
|
|
+ if (response.status !== 200) throw new Error(`${label} returned ${response.status}: ${await response.text()}`)
|
|
|
+ return await response.json()
|
|
|
})
|
|
|
- if (response.status !== 200) throw new Error(`${label} returned ${response.status}: ${await response.text()}`)
|
|
|
- return await response.json()
|
|
|
}
|
|
|
|
|
|
-async function expectJsonParity(input: {
|
|
|
+function expectJsonParity(input: {
|
|
|
label: string
|
|
|
legacy: ReturnType<typeof InstanceRoutes>
|
|
|
httpapi: ReturnType<typeof InstanceRoutes>
|
|
|
- directory: string
|
|
|
path: string
|
|
|
headers: HeadersInit
|
|
|
}) {
|
|
|
- const legacy = await readJson(input.label, input.legacy, input.directory, input.path, input.headers)
|
|
|
- const httpapi = await readJson(input.label, input.httpapi, input.directory, input.path, input.headers)
|
|
|
- expect({ label: input.label, body: httpapi }).toEqual({ label: input.label, body: legacy })
|
|
|
+ return Effect.gen(function* () {
|
|
|
+ const legacy = yield* readJson(input.label, input.legacy, input.path, input.headers)
|
|
|
+ const httpapi = yield* readJson(input.label, input.httpapi, input.path, input.headers)
|
|
|
+ expect({ label: input.label, body: httpapi }).toEqual({ label: input.label, body: legacy })
|
|
|
+ return httpapi
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
afterEach(async () => {
|
|
|
@@ -96,32 +90,78 @@ afterEach(async () => {
|
|
|
})
|
|
|
|
|
|
describe("HttpApi JSON parity", () => {
|
|
|
- test("matches legacy JSON shape for session read endpoints", async () => {
|
|
|
- await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
|
|
- const headers = { "x-opencode-directory": tmp.path }
|
|
|
- const seeded = await seedSessions(tmp.path)
|
|
|
- const legacy = app(false)
|
|
|
- const httpapi = app(true)
|
|
|
+ it.live(
|
|
|
+ "matches legacy JSON shape for session read endpoints",
|
|
|
+ withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const headers = { "x-opencode-directory": tmp.path }
|
|
|
+ const seeded = yield* seedSessions.pipe(Effect.provide(Session.defaultLayer))
|
|
|
+ const legacy = app(false)
|
|
|
+ const httpapi = app(true)
|
|
|
|
|
|
- await [
|
|
|
- { label: "session.list roots", path: `${SessionPaths.list}?roots=true`, headers },
|
|
|
- { label: "session.list all", path: SessionPaths.list, headers },
|
|
|
- { label: "session.get", path: pathFor(SessionPaths.get, { sessionID: seeded.parent.id }), headers },
|
|
|
- { label: "session.children", path: pathFor(SessionPaths.children, { sessionID: seeded.parent.id }), headers },
|
|
|
- { label: "session.messages", path: pathFor(SessionPaths.messages, { sessionID: seeded.parent.id }), headers },
|
|
|
- {
|
|
|
- label: "session.message",
|
|
|
- path: pathFor(SessionPaths.message, { sessionID: seeded.parent.id, messageID: seeded.message.id }),
|
|
|
- headers,
|
|
|
- },
|
|
|
- {
|
|
|
- label: "experimental.session",
|
|
|
- path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10" })}`,
|
|
|
- headers,
|
|
|
- },
|
|
|
- ].reduce(
|
|
|
- (promise, input) => promise.then(() => expectJsonParity({ ...input, legacy, httpapi, directory: tmp.path })),
|
|
|
- Promise.resolve(),
|
|
|
- )
|
|
|
- })
|
|
|
+ const rootsFalse = yield* expectJsonParity({
|
|
|
+ label: "session.list roots false",
|
|
|
+ legacy,
|
|
|
+ httpapi,
|
|
|
+ path: `${SessionPaths.list}?roots=false`,
|
|
|
+ headers,
|
|
|
+ })
|
|
|
+ expect((rootsFalse as Session.Info[]).map((session) => session.id)).toContain(seeded.parent.id)
|
|
|
+ expect((rootsFalse as Session.Info[]).length).toBe(2)
|
|
|
+
|
|
|
+ const experimentalRootsFalse = yield* expectJsonParity({
|
|
|
+ label: "experimental.session roots false",
|
|
|
+ legacy,
|
|
|
+ httpapi,
|
|
|
+ path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10", roots: "false" })}`,
|
|
|
+ headers,
|
|
|
+ })
|
|
|
+ expect((experimentalRootsFalse as Session.GlobalInfo[]).length).toBe(2)
|
|
|
+
|
|
|
+ const experimentalArchivedFalse = yield* expectJsonParity({
|
|
|
+ label: "experimental.session archived false",
|
|
|
+ legacy,
|
|
|
+ httpapi,
|
|
|
+ path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10", archived: "false" })}`,
|
|
|
+ headers,
|
|
|
+ })
|
|
|
+ expect((experimentalArchivedFalse as Session.GlobalInfo[]).length).toBe(2)
|
|
|
+
|
|
|
+ yield* Effect.forEach(
|
|
|
+ [
|
|
|
+ { label: "session.list roots", path: `${SessionPaths.list}?roots=true`, headers },
|
|
|
+ { label: "session.list all", path: SessionPaths.list, headers },
|
|
|
+ { label: "session.get", path: pathFor(SessionPaths.get, { sessionID: seeded.parent.id }), headers },
|
|
|
+ {
|
|
|
+ label: "session.children",
|
|
|
+ path: pathFor(SessionPaths.children, { sessionID: seeded.parent.id }),
|
|
|
+ headers,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "session.messages",
|
|
|
+ path: pathFor(SessionPaths.messages, { sessionID: seeded.parent.id }),
|
|
|
+ headers,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "session.messages empty before",
|
|
|
+ path: `${pathFor(SessionPaths.messages, { sessionID: seeded.parent.id })}?before=`,
|
|
|
+ headers,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "session.message",
|
|
|
+ path: pathFor(SessionPaths.message, { sessionID: seeded.parent.id, messageID: seeded.message.id }),
|
|
|
+ headers,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "experimental.session",
|
|
|
+ path: `${ExperimentalPaths.session}?${new URLSearchParams({ directory: tmp.path, limit: "10" })}`,
|
|
|
+ headers,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ (input) => expectJsonParity({ ...input, legacy, httpapi }),
|
|
|
+ { concurrency: 1 },
|
|
|
+ )
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ )
|
|
|
})
|