opencode/packages/server/test/session-import.test.ts
2026-10-20 00:42:13 -05:00

152 lines
5.9 KiB
TypeScript

import { expect } from "bun:test"
import { Session } from "@opencode/schema/session"
import { Effect, Schema } from "effect"
import { it } from "../../core/test/lib/effect"
import { ServerFetch } from "../src/fetch"
const SessionResponse = Schema.Struct({ data: Schema.toEncoded(Session.Info) })
const SessionsResponse = Schema.Struct({ data: Schema.Array(Schema.toEncoded(Session.Info)) })
const setup = Effect.gen(function* () {
const handler = yield* ServerFetch.make({
app: { version: "test" },
database: { path: ":memory:" },
fs: { filewatcher: false },
})
return (path: string, body?: unknown, status = 200) =>
Effect.promise(async () => {
const response = await handler(
new Request(`http://opencode.local${path}`, {
method: body === undefined ? "GET" : "POST",
headers: { "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
}),
)
const json: unknown = await response.json()
expect(response.status).toBe(status)
return json
})
})
it.live("preserves imported parentID through HTTP import, read, and parent filter", () =>
Effect.gen(function* () {
const request = yield* setup
const parent = Schema.decodeUnknownSync(SessionResponse)(yield* request("/api/session", { title: "Parent" }))
const id = Session.ID.create()
const imported = Schema.decodeUnknownSync(SessionResponse)(
yield* request("/api/experimental/session/import", {
info: { ...parent.data, id, parentID: parent.data.id, title: "Imported child" },
messages: [],
}),
)
const read = Schema.decodeUnknownSync(SessionResponse)(yield* request(`/api/session/${id}`))
const children = Schema.decodeUnknownSync(SessionsResponse)(
yield* request(`/api/session?parentID=${parent.data.id}`),
)
expect({
imported: imported.data.parentID,
read: read.data.parentID,
children: children.data.map((child) => child.id),
}).toEqual({ imported: parent.data.id, read: parent.data.id, children: [id] })
}).pipe(Effect.scoped),
)
it.live("upgrades native data in transcripts exported before v2.1", () =>
Effect.gen(function* () {
const request = yield* setup
const template = Schema.decodeUnknownSync(SessionResponse)(yield* request("/api/session", {}))
const id = Session.ID.create()
yield* request("/api/experimental/session/import", {
info: { ...template.data, id },
messages: [
{
id: "msg_legacy",
type: "assistant",
agent: "build",
model: { providerID: "anthropic", id: "claude" },
providerState: { finish: "kept" },
content: [
{ type: "text", text: "Answer", state: { signature: "text" } },
{ type: "reasoning", text: "Thinking", state: { signature: "reasoning" } },
{
type: "tool",
id: "call",
name: "web_search",
executed: true,
providerState: { call: "kept" },
providerResultState: { result: "kept" },
state: { status: "completed", input: {}, content: [{ type: "text", text: "Found" }] },
time: { created: 1, ran: 1, completed: 2 },
},
],
time: { created: 1, completed: 2 },
},
{
id: "msg_compaction",
type: "compaction",
status: "completed",
reason: "manual",
summary: "Summary",
recent: "[User]: kept",
providerState: { compaction: "kept" },
time: { created: 3 },
},
{ id: "msg_moved", type: "location-switched", location: { directory: "/moved" }, time: { created: 4 } },
],
})
expect(yield* request(`/api/session/${id}/message?order=asc`)).toMatchObject({
data: [
{
id: "msg_legacy",
native: { finish: "kept" },
content: [
{ type: "text", native: { signature: "text" } },
{ type: "reasoning", native: { signature: "reasoning" } },
{ type: "tool", native: { call: "kept", result: "kept" } },
],
},
{ id: "msg_compaction", native: { compaction: "kept" }, metadata: { recent: "[User]: kept" } },
{ id: "msg_moved", location: { directory: "/moved" } },
],
})
}).pipe(Effect.scoped),
)
it.live("rejects an invalid imported message without creating the session", () =>
Effect.gen(function* () {
const request = yield* setup
const template = Schema.decodeUnknownSync(SessionResponse)(yield* request("/api/session", {}))
const id = Session.ID.create()
const error = yield* request(
"/api/experimental/session/import",
{ info: { ...template.data, id }, messages: [{ id: "msg_invalid", type: "assistant" }] },
400,
)
expect(error).toMatchObject({
_tag: "InvalidRequestError",
kind: "Payload",
message: expect.stringContaining(`["messages"][0]`),
})
expect(yield* request(`/api/session/${id}`, undefined, 404)).toMatchObject({ _tag: "SessionNotFoundError" })
}).pipe(Effect.scoped),
)
;["missing", "self"].forEach((parent) => {
it.live(`rejects a ${parent} parent without creating the imported session`, () =>
Effect.gen(function* () {
const request = yield* setup
const template = Schema.decodeUnknownSync(SessionResponse)(yield* request("/api/session", {}))
const id = Session.ID.create()
const parentID = parent === "self" ? id : Session.ID.create()
const error = yield* request(
"/api/experimental/session/import",
{ info: { ...template.data, id, parentID }, messages: [] },
404,
)
expect(error).toMatchObject({ _tag: "SessionNotFoundError", sessionID: parentID })
expect(yield* request(`/api/session/${id}`, undefined, 404)).toMatchObject({
_tag: "SessionNotFoundError",
sessionID: id,
})
}).pipe(Effect.scoped),
)
})