From 4637ea75993d15c571a13d663b9f65c059c41e17 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 14 Apr 2026 12:42:51 -0400 Subject: [PATCH] refactor(session): remove async facade exports (#22396) --- packages/opencode/script/seed-e2e.ts | 53 ++--- packages/opencode/src/cli/cmd/debug/agent.ts | 82 +++---- packages/opencode/src/cli/cmd/export.ts | 14 +- packages/opencode/src/cli/cmd/github.ts | 22 +- packages/opencode/src/cli/cmd/session.ts | 5 +- packages/opencode/src/cli/cmd/stats.ts | 5 +- .../src/server/instance/middleware.ts | 2 +- .../opencode/src/server/instance/session.ts | 86 ++++---- packages/opencode/src/server/instance/tui.ts | 3 +- packages/opencode/src/session/index.ts | 89 +++----- packages/opencode/src/share/session.ts | 4 +- .../test/server/global-session-list.test.ts | 42 ++-- .../test/server/session-actions.test.ts | 22 +- .../opencode/test/server/session-list.test.ts | 46 ++-- .../test/server/session-messages.test.ts | 43 +++- .../test/server/session-select.test.ts | 22 +- .../opencode/test/session/compaction.test.ts | 161 ++++++++------ .../test/session/messages-pagination.test.ts | 203 ++++++++++-------- .../opencode/test/session/session.test.ts | 76 ++++--- 19 files changed, 557 insertions(+), 423 deletions(-) diff --git a/packages/opencode/script/seed-e2e.ts b/packages/opencode/script/seed-e2e.ts index ef61176339..2b89df838c 100644 --- a/packages/opencode/script/seed-e2e.ts +++ b/packages/opencode/script/seed-e2e.ts @@ -33,30 +33,35 @@ const seed = async () => { }), ) - const session = await Session.create({ title }) - const messageID = MessageID.ascending() - const partID = PartID.ascending() - const message = { - id: messageID, - sessionID: session.id, - role: "user" as const, - time: { created: now }, - agent: "build", - model: { - providerID: ProviderID.make(providerID), - modelID: ModelID.make(modelID), - }, - } - const part = { - id: partID, - sessionID: session.id, - messageID, - type: "text" as const, - text, - time: { start: now }, - } - await Session.updateMessage(message) - await Session.updatePart(part) + await AppRuntime.runPromise( + Effect.gen(function* () { + const session = yield* Session.Service + const result = yield* session.create({ title }) + const messageID = MessageID.ascending() + const partID = PartID.ascending() + const message = { + id: messageID, + sessionID: result.id, + role: "user" as const, + time: { created: now }, + agent: "build", + model: { + providerID: ProviderID.make(providerID), + modelID: ModelID.make(modelID), + }, + } + const part = { + id: partID, + sessionID: result.id, + messageID, + type: "text" as const, + text, + time: { start: now }, + } + yield* session.updateMessage(message) + yield* session.updatePart(part) + }), + ) await AppRuntime.runPromise( Project.Service.use((svc) => svc.update({ projectID: Instance.project.id, name: "E2E Project" })), ) diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index 51de43f671..ea45cde664 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -123,45 +123,49 @@ function parseToolParams(input?: string) { } async function createToolContext(agent: Agent.Info) { - const session = await Session.create({ title: `Debug tool run (${agent.name})` }) - const messageID = MessageID.ascending() - const model = - agent.model ?? - (await AppRuntime.runPromise( - Effect.gen(function* () { - const provider = yield* Provider.Service - return yield* provider.defaultModel() - }), - )) - const now = Date.now() - const message: MessageV2.Assistant = { - id: messageID, - sessionID: session.id, - role: "assistant", - time: { - created: now, - }, - parentID: messageID, - modelID: model.modelID, - providerID: model.providerID, - mode: "debug", - agent: agent.name, - path: { - cwd: Instance.directory, - root: Instance.worktree, - }, - cost: 0, - tokens: { - input: 0, - output: 0, - reasoning: 0, - cache: { - read: 0, - write: 0, - }, - }, - } - await Session.updateMessage(message) + const { session, messageID } = await AppRuntime.runPromise( + Effect.gen(function* () { + const session = yield* Session.Service + const result = yield* session.create({ title: `Debug tool run (${agent.name})` }) + const messageID = MessageID.ascending() + const model = agent.model + ? agent.model + : yield* Effect.gen(function* () { + const provider = yield* Provider.Service + return yield* provider.defaultModel() + }) + const now = Date.now() + const message: MessageV2.Assistant = { + id: messageID, + sessionID: result.id, + role: "assistant", + time: { + created: now, + }, + parentID: messageID, + modelID: model.modelID, + providerID: model.providerID, + mode: "debug", + agent: agent.name, + path: { + cwd: Instance.directory, + root: Instance.worktree, + }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { + read: 0, + write: 0, + }, + }, + } + yield* session.updateMessage(message) + return { session: result, messageID } + }), + ) const ruleset = Permission.merge(agent.permission, session.permission ?? []) diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts index 4088b4818d..cd2637722b 100644 --- a/packages/opencode/src/cli/cmd/export.ts +++ b/packages/opencode/src/cli/cmd/export.ts @@ -6,6 +6,8 @@ import { bootstrap } from "../bootstrap" import { UI } from "../ui" import * as prompts from "@clack/prompts" import { EOL } from "os" +import { AppRuntime } from "@/effect/app-runtime" +import { Effect } from "effect" export const ExportCommand = cmd({ command: "export [sessionID]", @@ -67,8 +69,16 @@ export const ExportCommand = cmd({ } try { - const sessionInfo = await Session.get(sessionID!) - const messages = await Session.messages({ sessionID: sessionInfo.id }) + const { sessionInfo, messages } = await AppRuntime.runPromise( + Effect.gen(function* () { + const session = yield* Session.Service + const sessionInfo = yield* session.get(sessionID!) + return { + sessionInfo, + messages: yield* session.messages({ sessionID: sessionInfo.id }), + } + }), + ) const exportData = { info: sessionInfo, diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts index fd375f55f9..074d9e5185 100644 --- a/packages/opencode/src/cli/cmd/github.ts +++ b/packages/opencode/src/cli/cmd/github.ts @@ -552,15 +552,19 @@ export const GithubRunCommand = cmd({ // Setup opencode session const repoData = await fetchRepo() - session = await Session.create({ - permission: [ - { - permission: "question", - action: "deny", - pattern: "*", - }, - ], - }) + session = await AppRuntime.runPromise( + Session.Service.use((svc) => + svc.create({ + permission: [ + { + permission: "question", + action: "deny", + pattern: "*", + }, + ], + }), + ), + ) subscribeSessionEvents() shareId = await (async () => { if (share === false) return diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 8acd7480c9..6f79e726fa 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -11,6 +11,7 @@ import { Process } from "../../util/process" import { EOL } from "os" import path from "path" import { which } from "../../util/which" +import { AppRuntime } from "@/effect/app-runtime" function pagerCmd(): string[] { const lessOptions = ["-R", "-S"] @@ -60,12 +61,12 @@ export const SessionDeleteCommand = cmd({ await bootstrap(process.cwd(), async () => { const sessionID = SessionID.make(args.sessionID) try { - await Session.get(sessionID) + await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID))) } catch { UI.error(`Session not found: ${args.sessionID}`) process.exit(1) } - await Session.remove(sessionID) + await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(sessionID))) UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL) }) }, diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts index 04c1fe2ebc..527a6ac952 100644 --- a/packages/opencode/src/cli/cmd/stats.ts +++ b/packages/opencode/src/cli/cmd/stats.ts @@ -6,6 +6,7 @@ import { Database } from "../../storage/db" import { SessionTable } from "../../session/session.sql" import { Project } from "../../project/project" import { Instance } from "../../project/instance" +import { AppRuntime } from "@/effect/app-runtime" interface SessionStats { totalSessions: number @@ -167,7 +168,9 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin const batch = filteredSessions.slice(i, i + BATCH_SIZE) const batchPromises = batch.map(async (session) => { - const messages = await Session.messages({ sessionID: session.id }) + const messages = await AppRuntime.runPromise( + Session.Service.use((svc) => svc.messages({ sessionID: session.id })), + ) let sessionCost = 0 let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } diff --git a/packages/opencode/src/server/instance/middleware.ts b/packages/opencode/src/server/instance/middleware.ts index 868131eb82..9155ad451b 100644 --- a/packages/opencode/src/server/instance/middleware.ts +++ b/packages/opencode/src/server/instance/middleware.ts @@ -41,7 +41,7 @@ async function getSessionWorkspace(url: URL) { const id = getSessionID(url) if (!id) return null - const session = await Session.get(id).catch(() => undefined) + const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(id))).catch(() => undefined) return session?.workspaceID } diff --git a/packages/opencode/src/server/instance/session.ts b/packages/opencode/src/server/instance/session.ts index 663976eb40..e443a6ddd2 100644 --- a/packages/opencode/src/server/instance/session.ts +++ b/packages/opencode/src/server/instance/session.ts @@ -121,12 +121,12 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: Session.get.schema, + sessionID: Session.GetInput, }), ), async (c) => { const sessionID = c.req.valid("param").sessionID - const session = await Session.get(sessionID) + const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID))) return c.json(session) }, ) @@ -152,12 +152,12 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: Session.children.schema, + sessionID: Session.ChildrenInput, }), ), async (c) => { const sessionID = c.req.valid("param").sessionID - const session = await Session.children(sessionID) + const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.children(sessionID))) return c.json(session) }, ) @@ -209,7 +209,7 @@ export const SessionRoutes = lazy(() => }, }, }), - validator("json", Session.create.schema), + validator("json", Session.CreateInput), async (c) => { const body = c.req.valid("json") ?? {} const session = await AppRuntime.runPromise(SessionShare.Service.use((svc) => svc.create(body))) @@ -237,12 +237,12 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: Session.remove.schema, + sessionID: Session.RemoveInput, }), ), async (c) => { const sessionID = c.req.valid("param").sessionID - await Session.remove(sessionID) + await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(sessionID))) return c.json(true) }, ) @@ -285,22 +285,27 @@ export const SessionRoutes = lazy(() => async (c) => { const sessionID = c.req.valid("param").sessionID const updates = c.req.valid("json") - const current = await Session.get(sessionID) + const session = await AppRuntime.runPromise( + Effect.gen(function* () { + const session = yield* Session.Service + const current = yield* session.get(sessionID) - if (updates.title !== undefined) { - await Session.setTitle({ sessionID, title: updates.title }) - } - if (updates.permission !== undefined) { - await Session.setPermission({ - sessionID, - permission: Permission.merge(current.permission ?? [], updates.permission), - }) - } - if (updates.time?.archived !== undefined) { - await Session.setArchived({ sessionID, time: updates.time.archived }) - } + if (updates.title !== undefined) { + yield* session.setTitle({ sessionID, title: updates.title }) + } + if (updates.permission !== undefined) { + yield* session.setPermission({ + sessionID, + permission: Permission.merge(current.permission ?? [], updates.permission), + }) + } + if (updates.time?.archived !== undefined) { + yield* session.setArchived({ sessionID, time: updates.time.archived }) + } - const session = await Session.get(sessionID) + return yield* session.get(sessionID) + }), + ) return c.json(session) }, ) @@ -375,14 +380,14 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: Session.fork.schema.shape.sessionID, + sessionID: Session.ForkInput.shape.sessionID, }), ), - validator("json", Session.fork.schema.omit({ sessionID: true })), + validator("json", Session.ForkInput.omit({ sessionID: true })), async (c) => { const sessionID = c.req.valid("param").sessionID const body = c.req.valid("json") - const result = await Session.fork({ ...body, sessionID }) + const result = await AppRuntime.runPromise(Session.Service.use((svc) => svc.fork({ ...body, sessionID }))) return c.json(result) }, ) @@ -661,15 +666,14 @@ export const SessionRoutes = lazy(() => async (c) => { const query = c.req.valid("query") const sessionID = c.req.valid("param").sessionID - if (query.limit === undefined) { - await Session.get(sessionID) - const messages = await Session.messages({ sessionID }) - return c.json(messages) - } - - if (query.limit === 0) { - await Session.get(sessionID) - const messages = await Session.messages({ sessionID }) + if (query.limit === undefined || query.limit === 0) { + const messages = await AppRuntime.runPromise( + Effect.gen(function* () { + const session = yield* Session.Service + yield* session.get(sessionID) + return yield* session.messages({ sessionID }) + }), + ) return c.json(messages) } @@ -797,11 +801,15 @@ export const SessionRoutes = lazy(() => ), async (c) => { const params = c.req.valid("param") - await Session.removePart({ - sessionID: params.sessionID, - messageID: params.messageID, - partID: params.partID, - }) + await AppRuntime.runPromise( + Session.Service.use((svc) => + svc.removePart({ + sessionID: params.sessionID, + messageID: params.messageID, + partID: params.partID, + }), + ), + ) return c.json(true) }, ) @@ -839,7 +847,7 @@ export const SessionRoutes = lazy(() => `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`, ) } - const part = await Session.updatePart(body) + const part = await AppRuntime.runPromise(Session.Service.use((svc) => svc.updatePart(body))) return c.json(part) }, ) diff --git a/packages/opencode/src/server/instance/tui.ts b/packages/opencode/src/server/instance/tui.ts index 8650a0cccf..13f150655b 100644 --- a/packages/opencode/src/server/instance/tui.ts +++ b/packages/opencode/src/server/instance/tui.ts @@ -4,6 +4,7 @@ import z from "zod" import { Bus } from "../../bus" import { Session } from "../../session" import { TuiEvent } from "@/cli/cmd/tui/event" +import { AppRuntime } from "@/effect/app-runtime" import { AsyncQueue } from "../../util/queue" import { errors } from "../error" import { lazy } from "../../util/lazy" @@ -370,7 +371,7 @@ export const TuiRoutes = lazy(() => validator("json", TuiEvent.SessionSelect.properties), async (c) => { const { sessionID } = c.req.valid("json") - await Session.get(sessionID) + await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID))) await Bus.publish(TuiEvent.SessionSelect, { sessionID }) return c.json(true) }, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index b43b724a00..aa3404ad75 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -19,7 +19,6 @@ import { updateSchema } from "../util/update-schema" import { MessageV2 } from "./message-v2" import { Instance } from "../project/instance" import { InstanceState } from "@/effect/instance-state" -import { fn } from "@/util/fn" import { Snapshot } from "@/snapshot" import { ProjectID } from "../project/schema" import { WorkspaceID } from "../control-plane/schema" @@ -29,7 +28,6 @@ import type { Provider } from "@/provider/provider" import { Permission } from "@/permission" import { Global } from "@/global" import { Effect, Layer, Option, Context } from "effect" -import { makeRuntime } from "@/effect/run-service" export namespace Session { const log = Log.create({ service: "session" }) @@ -179,6 +177,30 @@ export namespace Session { }) export type GlobalInfo = z.output + export const CreateInput = z + .object({ + parentID: SessionID.zod.optional(), + title: z.string().optional(), + permission: Info.shape.permission, + workspaceID: WorkspaceID.zod.optional(), + }) + .optional() + export type CreateInput = z.output + + export const ForkInput = z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() }) + export const GetInput = SessionID.zod + export const ChildrenInput = SessionID.zod + export const RemoveInput = SessionID.zod + export const SetTitleInput = z.object({ sessionID: SessionID.zod, title: z.string() }) + export const SetArchivedInput = z.object({ sessionID: SessionID.zod, time: z.number().optional() }) + export const SetPermissionInput = z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }) + export const SetRevertInput = z.object({ + sessionID: SessionID.zod, + revert: Info.shape.revert, + summary: Info.shape.summary, + }) + export const MessagesInput = z.object({ sessionID: SessionID.zod, limit: z.number().optional() }) + export const Event = { Created: SyncEvent.define({ type: "session.created", @@ -682,48 +704,6 @@ export namespace Session { export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Storage.defaultLayer)) - const { runPromise } = makeRuntime(Service, defaultLayer) - - export const create = fn( - z - .object({ - parentID: SessionID.zod.optional(), - title: z.string().optional(), - permission: Info.shape.permission, - workspaceID: WorkspaceID.zod.optional(), - }) - .optional(), - (input) => runPromise((svc) => svc.create(input)), - ) - - export const fork = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() }), (input) => - runPromise((svc) => svc.fork(input)), - ) - - export const get = fn(SessionID.zod, (id) => runPromise((svc) => svc.get(id))) - - export const setTitle = fn(z.object({ sessionID: SessionID.zod, title: z.string() }), (input) => - runPromise((svc) => svc.setTitle(input)), - ) - - export const setArchived = fn(z.object({ sessionID: SessionID.zod, time: z.number().optional() }), (input) => - runPromise((svc) => svc.setArchived(input)), - ) - - export const setPermission = fn(z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }), (input) => - runPromise((svc) => svc.setPermission(input)), - ) - - export const setRevert = fn( - z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }), - (input) => - runPromise((svc) => svc.setRevert({ sessionID: input.sessionID, revert: input.revert, summary: input.summary })), - ) - - export const messages = fn(z.object({ sessionID: SessionID.zod, limit: z.number().optional() }), (input) => - runPromise((svc) => svc.messages(input)), - ) - export function* list(input?: { directory?: string workspaceID?: WorkspaceID @@ -835,25 +815,4 @@ export namespace Session { yield { ...fromRow(row), project } } } - - export const children = fn(SessionID.zod, (id) => runPromise((svc) => svc.children(id))) - export const remove = fn(SessionID.zod, (id) => runPromise((svc) => svc.remove(id))) - export async function updateMessage(msg: T): Promise { - MessageV2.Info.parse(msg) - return runPromise((svc) => svc.updateMessage(msg)) - } - - export const removeMessage = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod }), (input) => - runPromise((svc) => svc.removeMessage(input)), - ) - - export const removePart = fn( - z.object({ sessionID: SessionID.zod, messageID: MessageID.zod, partID: PartID.zod }), - (input) => runPromise((svc) => svc.removePart(input)), - ) - - export async function updatePart(part: T): Promise { - MessageV2.Part.parse(part) - return runPromise((svc) => svc.updatePart(part)) - } } diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index 0030e3c840..08210de8a1 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -8,7 +8,7 @@ import { ShareNext } from "./share-next" export namespace SessionShare { export interface Interface { - readonly create: (input?: Parameters[0]) => Effect.Effect + readonly create: (input?: Session.CreateInput) => Effect.Effect readonly share: (sessionID: SessionID) => Effect.Effect<{ url: string }, unknown> readonly unshare: (sessionID: SessionID) => Effect.Effect } @@ -38,7 +38,7 @@ export namespace SessionShare { yield* Effect.sync(() => SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } })) }) - const create = Effect.fn("SessionShare.create")(function* (input?: Parameters[0]) { + const create = Effect.fn("SessionShare.create")(function* (input?: Session.CreateInput) { const result = yield* session.create(input) if (result.parentID) return result const conf = yield* cfg.get() diff --git a/packages/opencode/test/server/global-session-list.test.ts b/packages/opencode/test/server/global-session-list.test.ts index 05d6de04b1..a1e374b4f7 100644 --- a/packages/opencode/test/server/global-session-list.test.ts +++ b/packages/opencode/test/server/global-session-list.test.ts @@ -1,27 +1,43 @@ import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import z from "zod" import { Instance } from "../../src/project/instance" import { Project } from "../../src/project/project" -import { Session } from "../../src/session" +import { Session as SessionNs } from "../../src/session" import { Log } from "../../src/util/log" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) -describe("Session.listGlobal", () => { +function run(fx: Effect.Effect) { + return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) +} + +const svc = { + ...SessionNs, + create(input?: SessionNs.CreateInput) { + return run(SessionNs.Service.use((svc) => svc.create(input))) + }, + setArchived(input: z.output) { + return run(SessionNs.Service.use((svc) => svc.setArchived(input))) + }, +} + +describe("session.listGlobal", () => { test("lists sessions across projects with project metadata", async () => { await using first = await tmpdir({ git: true }) await using second = await tmpdir({ git: true }) const firstSession = await Instance.provide({ directory: first.path, - fn: async () => Session.create({ title: "first-session" }), + fn: async () => svc.create({ title: "first-session" }), }) const secondSession = await Instance.provide({ directory: second.path, - fn: async () => Session.create({ title: "second-session" }), + fn: async () => svc.create({ title: "second-session" }), }) - const sessions = [...Session.listGlobal({ limit: 200 })] + const sessions = [...svc.listGlobal({ limit: 200 })] const ids = sessions.map((session) => session.id) expect(ids).toContain(firstSession.id) @@ -44,20 +60,20 @@ describe("Session.listGlobal", () => { const archived = await Instance.provide({ directory: tmp.path, - fn: async () => Session.create({ title: "archived-session" }), + fn: async () => svc.create({ title: "archived-session" }), }) await Instance.provide({ directory: tmp.path, - fn: async () => Session.setArchived({ sessionID: archived.id, time: Date.now() }), + fn: async () => svc.setArchived({ sessionID: archived.id, time: Date.now() }), }) - const sessions = [...Session.listGlobal({ limit: 200 })] + const sessions = [...svc.listGlobal({ limit: 200 })] const ids = sessions.map((session) => session.id) expect(ids).not.toContain(archived.id) - const allSessions = [...Session.listGlobal({ limit: 200, archived: true })] + const allSessions = [...svc.listGlobal({ limit: 200, archived: true })] const allIds = allSessions.map((session) => session.id) expect(allIds).toContain(archived.id) @@ -68,19 +84,19 @@ describe("Session.listGlobal", () => { const first = await Instance.provide({ directory: tmp.path, - fn: async () => Session.create({ title: "page-one" }), + fn: async () => svc.create({ title: "page-one" }), }) await new Promise((resolve) => setTimeout(resolve, 5)) const second = await Instance.provide({ directory: tmp.path, - fn: async () => Session.create({ title: "page-two" }), + fn: async () => svc.create({ title: "page-two" }), }) - const page = [...Session.listGlobal({ directory: tmp.path, limit: 1 })] + const page = [...svc.listGlobal({ directory: tmp.path, limit: 1 })] expect(page.length).toBe(1) expect(page[0].id).toBe(second.id) - const next = [...Session.listGlobal({ directory: tmp.path, limit: 10, cursor: page[0].time.updated })] + const next = [...svc.listGlobal({ directory: tmp.path, limit: 10, cursor: page[0].time.updated })] const ids = next.map((session) => session.id) expect(ids).toContain(first.id) diff --git a/packages/opencode/test/server/session-actions.test.ts b/packages/opencode/test/server/session-actions.test.ts index da65434acc..301691ae2f 100644 --- a/packages/opencode/test/server/session-actions.test.ts +++ b/packages/opencode/test/server/session-actions.test.ts @@ -1,12 +1,28 @@ import { afterEach, describe, expect, mock, test } from "bun:test" +import { Effect } from "effect" import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" -import { Session } from "../../src/session" +import { Session as SessionNs } from "../../src/session" +import type { SessionID } from "../../src/session/schema" import { Log } from "../../src/util/log" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) +function run(fx: Effect.Effect) { + return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) +} + +const svc = { + ...SessionNs, + create(input?: SessionNs.CreateInput) { + return run(SessionNs.Service.use((svc) => svc.create(input))) + }, + remove(id: SessionID) { + return run(SessionNs.Service.use((svc) => svc.remove(id))) + }, +} + afterEach(async () => { mock.restore() await Instance.disposeAll() @@ -18,7 +34,7 @@ describe("session action routes", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const app = Server.Default().app const res = await app.request(`/session/${session.id}/abort`, { method: "POST" }) @@ -26,7 +42,7 @@ describe("session action routes", () => { expect(res.status).toBe(200) expect(await res.json()).toBe(true) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 933b5b5b5a..8c86dc2f06 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -1,30 +1,42 @@ import { afterEach, describe, expect, test } from "bun:test" +import { Effect } from "effect" import { Instance } from "../../src/project/instance" -import { Session } from "../../src/session" +import { Session as SessionNs } from "../../src/session" import { Log } from "../../src/util/log" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) +function run(fx: Effect.Effect) { + return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) +} + +const svc = { + ...SessionNs, + create(input?: SessionNs.CreateInput) { + return run(SessionNs.Service.use((svc) => svc.create(input))) + }, +} + afterEach(async () => { await Instance.disposeAll() }) -describe("Session.list", () => { +describe("session.list", () => { test("filters by directory", async () => { await using tmp = await tmpdir({ git: true }) await Instance.provide({ directory: tmp.path, fn: async () => { - const first = await Session.create({}) + const first = await svc.create({}) await using other = await tmpdir({ git: true }) const second = await Instance.provide({ directory: other.path, - fn: async () => Session.create({}), + fn: async () => svc.create({}), }) - const sessions = [...Session.list({ directory: tmp.path })] + const sessions = [...svc.list({ directory: tmp.path })] const ids = sessions.map((s) => s.id) expect(ids).toContain(first.id) @@ -38,10 +50,10 @@ describe("Session.list", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const root = await Session.create({ title: "root-session" }) - const child = await Session.create({ title: "child-session", parentID: root.id }) + const root = await svc.create({ title: "root-session" }) + const child = await svc.create({ title: "child-session", parentID: root.id }) - const sessions = [...Session.list({ roots: true })] + const sessions = [...svc.list({ roots: true })] const ids = sessions.map((s) => s.id) expect(ids).toContain(root.id) @@ -55,10 +67,10 @@ describe("Session.list", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({ title: "new-session" }) + const session = await svc.create({ title: "new-session" }) const futureStart = Date.now() + 86400000 - const sessions = [...Session.list({ start: futureStart })] + const sessions = [...svc.list({ start: futureStart })] expect(sessions.length).toBe(0) }, }) @@ -69,10 +81,10 @@ describe("Session.list", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await Session.create({ title: "unique-search-term-abc" }) - await Session.create({ title: "other-session-xyz" }) + await svc.create({ title: "unique-search-term-abc" }) + await svc.create({ title: "other-session-xyz" }) - const sessions = [...Session.list({ search: "unique-search" })] + const sessions = [...svc.list({ search: "unique-search" })] const titles = sessions.map((s) => s.title) expect(titles).toContain("unique-search-term-abc") @@ -86,11 +98,11 @@ describe("Session.list", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - await Session.create({ title: "session-1" }) - await Session.create({ title: "session-2" }) - await Session.create({ title: "session-3" }) + await svc.create({ title: "session-1" }) + await svc.create({ title: "session-2" }) + await svc.create({ title: "session-3" }) - const sessions = [...Session.list({ limit: 2 })] + const sessions = [...svc.list({ limit: 2 })] expect(sessions.length).toBe(2) }, }) diff --git a/packages/opencode/test/server/session-messages.test.ts b/packages/opencode/test/server/session-messages.test.ts index fac3368376..24ee6a1b43 100644 --- a/packages/opencode/test/server/session-messages.test.ts +++ b/packages/opencode/test/server/session-messages.test.ts @@ -1,7 +1,8 @@ import { afterEach, describe, expect, test } from "bun:test" +import { Effect } from "effect" import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" -import { Session } from "../../src/session" +import { Session as SessionNs } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { Log } from "../../src/util/log" @@ -9,6 +10,26 @@ import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) +function run(fx: Effect.Effect) { + return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) +} + +const svc = { + ...SessionNs, + create(input?: SessionNs.CreateInput) { + return run(SessionNs.Service.use((svc) => svc.create(input))) + }, + remove(id: SessionID) { + return run(SessionNs.Service.use((svc) => svc.remove(id))) + }, + updateMessage(msg: T) { + return run(SessionNs.Service.use((svc) => svc.updateMessage(msg))) + }, + updatePart(part: T) { + return run(SessionNs.Service.use((svc) => svc.updatePart(part))) + }, +} + afterEach(async () => { await Instance.disposeAll() }) @@ -30,7 +51,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D for (let i = 0; i < count; i++) { const id = MessageID.ascending() ids.push(id) - await Session.updateMessage({ + await svc.updateMessage({ id, sessionID, role: "user", @@ -40,7 +61,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D tools: {}, mode: "", } as unknown as MessageV2.Info) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID, messageID: id, @@ -58,7 +79,7 @@ describe("session messages endpoint", () => { Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 5) const app = Server.Default().app @@ -75,7 +96,7 @@ describe("session messages endpoint", () => { const bBody = (await b.json()) as MessageV2.WithParts[] expect(bBody.map((item) => item.info.id)).toEqual(ids.slice(-4, -2)) - await Session.remove(session.id) + await svc.remove(session.id) }, }), ) @@ -87,7 +108,7 @@ describe("session messages endpoint", () => { Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 3) const app = Server.Default().app @@ -96,7 +117,7 @@ describe("session messages endpoint", () => { const body = (await res.json()) as MessageV2.WithParts[] expect(body.map((item) => item.info.id)).toEqual(ids) - await Session.remove(session.id) + await svc.remove(session.id) }, }), ) @@ -108,7 +129,7 @@ describe("session messages endpoint", () => { Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const app = Server.Default().app const bad = await app.request(`/session/${session.id}/message?limit=2&before=bad`) @@ -117,7 +138,7 @@ describe("session messages endpoint", () => { const miss = await app.request(`/session/ses_missing/message?limit=2`) expect(miss.status).toBe(404) - await Session.remove(session.id) + await svc.remove(session.id) }, }), ) @@ -129,7 +150,7 @@ describe("session messages endpoint", () => { Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) await fill(session.id, 520) const app = Server.Default().app @@ -138,7 +159,7 @@ describe("session messages endpoint", () => { const body = (await res.json()) as MessageV2.WithParts[] expect(body).toHaveLength(510) - await Session.remove(session.id) + await svc.remove(session.id) }, }), ) diff --git a/packages/opencode/test/server/session-select.test.ts b/packages/opencode/test/server/session-select.test.ts index 7558b4a6b6..12552538da 100644 --- a/packages/opencode/test/server/session-select.test.ts +++ b/packages/opencode/test/server/session-select.test.ts @@ -1,5 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test" -import { Session } from "../../src/session" +import { Effect } from "effect" +import { Session as SessionNs } from "../../src/session" +import type { SessionID } from "../../src/session/schema" import { Log } from "../../src/util/log" import { Instance } from "../../src/project/instance" import { Server } from "../../src/server/server" @@ -7,6 +9,20 @@ import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) +function run(fx: Effect.Effect) { + return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) +} + +const svc = { + ...SessionNs, + create(input?: SessionNs.CreateInput) { + return run(SessionNs.Service.use((svc) => svc.create(input))) + }, + remove(id: SessionID) { + return run(SessionNs.Service.use((svc) => svc.remove(id))) + }, +} + afterEach(async () => { await Instance.disposeAll() }) @@ -18,7 +34,7 @@ describe("tui.selectSession endpoint", () => { directory: tmp.path, fn: async () => { // #given - const session = await Session.create({}) + const session = await svc.create({}) // #when const app = Server.Default().app @@ -33,7 +49,7 @@ describe("tui.selectSession endpoint", () => { const body = await response.json() expect(body).toBe(true) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 42511d2118..ddfe859113 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -3,6 +3,7 @@ import { APICallError } from "ai" import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect" import * as Stream from "effect/Stream" import path from "path" +import z from "zod" import { Bus } from "../../src/bus" import { Config } from "../../src/config/config" import { Agent } from "../../src/agent/agent" @@ -14,7 +15,7 @@ import { Log } from "../../src/util/log" import { Permission } from "../../src/permission" import { Plugin } from "../../src/plugin" import { provideTmpdirInstance, tmpdir } from "../fixture/fixture" -import { Session } from "../../src/session" +import { Session as SessionNs } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" @@ -29,6 +30,26 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" Log.init({ print: false }) +function run(fx: Effect.Effect) { + return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) +} + +const svc = { + ...SessionNs, + create(input?: SessionNs.CreateInput) { + return run(SessionNs.Service.use((svc) => svc.create(input))) + }, + messages(input: z.output) { + return run(SessionNs.Service.use((svc) => svc.messages(input))) + }, + updateMessage(msg: T) { + return run(SessionNs.Service.use((svc) => svc.updateMessage(msg))) + }, + updatePart(part: T) { + return run(SessionNs.Service.use((svc) => svc.updatePart(part))) + }, +} + const summary = Layer.succeed( SessionSummary.Service, SessionSummary.Service.of({ @@ -80,7 +101,7 @@ function createModel(opts: { const wide = () => ProviderTest.fake({ model: createModel({ context: 100_000, output: 32_000 }) }) async function user(sessionID: SessionID, text: string) { - const msg = await Session.updateMessage({ + const msg = await svc.updateMessage({ id: MessageID.ascending(), role: "user", sessionID, @@ -88,7 +109,7 @@ async function user(sessionID: SessionID, text: string) { model: ref, time: { created: Date.now() }, }) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), messageID: msg.id, sessionID, @@ -119,12 +140,12 @@ async function assistant(sessionID: SessionID, parentID: MessageID, root: string time: { created: Date.now() }, finish: "end_turn", } - await Session.updateMessage(msg) + await svc.updateMessage(msg) return msg } async function tool(sessionID: SessionID, messageID: MessageID, tool: string, output: string) { - return Session.updatePart({ + return svc.updatePart({ id: PartID.ascending(), messageID, sessionID, @@ -171,7 +192,7 @@ function runtime(result: "continue" | "compact", plugin = Plugin.defaultLayer, p return ManagedRuntime.make( Layer.mergeAll(SessionCompaction.layer, bus).pipe( Layer.provide(provider.layer), - Layer.provide(Session.defaultLayer), + Layer.provide(SessionNs.defaultLayer), Layer.provide(layer(result)), Layer.provide(Agent.defaultLayer), Layer.provide(plugin), @@ -191,9 +212,9 @@ const deps = Layer.mergeAll( ) const env = Layer.mergeAll( - Session.defaultLayer, + SessionNs.defaultLayer, CrossSpawnSpawner.defaultLayer, - SessionCompaction.layer.pipe(Layer.provide(Session.defaultLayer), Layer.provideMerge(deps)), + SessionCompaction.layer.pipe(Layer.provide(SessionNs.defaultLayer), Layer.provideMerge(deps)), ) const it = testEffect(env) @@ -227,7 +248,7 @@ function liveRuntime(layer: Layer.Layer, provider = ProviderTest.fa return ManagedRuntime.make( Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus, status).pipe( Layer.provide(provider.layer), - Layer.provide(Session.defaultLayer), + Layer.provide(SessionNs.defaultLayer), Layer.provide(Snapshot.defaultLayer), Layer.provide(layer), Layer.provide(Permission.defaultLayer), @@ -467,9 +488,9 @@ describe("session.compaction.create", () => { provideTmpdirInstance(() => Effect.gen(function* () { const compact = yield* SessionCompaction.Service - const session = yield* Session.Service + const ssn = yield* SessionNs.Service - const info = yield* session.create({}) + const info = yield* ssn.create({}) yield* compact.create({ sessionID: info.id, @@ -479,7 +500,7 @@ describe("session.compaction.create", () => { overflow: true, }) - const msgs = yield* session.messages({ sessionID: info.id }) + const msgs = yield* ssn.messages({ sessionID: info.id }) expect(msgs).toHaveLength(1) expect(msgs[0].info.role).toBe("user") expect(msgs[0].parts).toHaveLength(1) @@ -499,9 +520,9 @@ describe("session.compaction.prune", () => { provideTmpdirInstance((dir) => Effect.gen(function* () { const compact = yield* SessionCompaction.Service - const session = yield* Session.Service - const info = yield* session.create({}) - const a = yield* session.updateMessage({ + const ssn = yield* SessionNs.Service + const info = yield* ssn.create({}) + const a = yield* ssn.updateMessage({ id: MessageID.ascending(), role: "user", sessionID: info.id, @@ -509,7 +530,7 @@ describe("session.compaction.prune", () => { model: ref, time: { created: Date.now() }, }) - yield* session.updatePart({ + yield* ssn.updatePart({ id: PartID.ascending(), messageID: a.id, sessionID: info.id, @@ -536,8 +557,8 @@ describe("session.compaction.prune", () => { time: { created: Date.now() }, finish: "end_turn", } - yield* session.updateMessage(b) - yield* session.updatePart({ + yield* ssn.updateMessage(b) + yield* ssn.updatePart({ id: PartID.ascending(), messageID: b.id, sessionID: info.id, @@ -554,7 +575,7 @@ describe("session.compaction.prune", () => { }, }) for (const text of ["second", "third"]) { - const msg = yield* session.updateMessage({ + const msg = yield* ssn.updateMessage({ id: MessageID.ascending(), role: "user", sessionID: info.id, @@ -562,7 +583,7 @@ describe("session.compaction.prune", () => { model: ref, time: { created: Date.now() }, }) - yield* session.updatePart({ + yield* ssn.updatePart({ id: PartID.ascending(), messageID: msg.id, sessionID: info.id, @@ -573,7 +594,7 @@ describe("session.compaction.prune", () => { yield* compact.prune({ sessionID: info.id }) - const msgs = yield* session.messages({ sessionID: info.id }) + const msgs = yield* ssn.messages({ sessionID: info.id }) const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool") expect(part?.type).toBe("tool") expect(part?.state.status).toBe("completed") @@ -589,9 +610,9 @@ describe("session.compaction.prune", () => { provideTmpdirInstance((dir) => Effect.gen(function* () { const compact = yield* SessionCompaction.Service - const session = yield* Session.Service - const info = yield* session.create({}) - const a = yield* session.updateMessage({ + const ssn = yield* SessionNs.Service + const info = yield* ssn.create({}) + const a = yield* ssn.updateMessage({ id: MessageID.ascending(), role: "user", sessionID: info.id, @@ -599,7 +620,7 @@ describe("session.compaction.prune", () => { model: ref, time: { created: Date.now() }, }) - yield* session.updatePart({ + yield* ssn.updatePart({ id: PartID.ascending(), messageID: a.id, sessionID: info.id, @@ -626,8 +647,8 @@ describe("session.compaction.prune", () => { time: { created: Date.now() }, finish: "end_turn", } - yield* session.updateMessage(b) - yield* session.updatePart({ + yield* ssn.updateMessage(b) + yield* ssn.updatePart({ id: PartID.ascending(), messageID: b.id, sessionID: info.id, @@ -644,7 +665,7 @@ describe("session.compaction.prune", () => { }, }) for (const text of ["second", "third"]) { - const msg = yield* session.updateMessage({ + const msg = yield* ssn.updateMessage({ id: MessageID.ascending(), role: "user", sessionID: info.id, @@ -652,7 +673,7 @@ describe("session.compaction.prune", () => { model: ref, time: { created: Date.now() }, }) - yield* session.updatePart({ + yield* ssn.updatePart({ id: PartID.ascending(), messageID: msg.id, sessionID: info.id, @@ -663,7 +684,7 @@ describe("session.compaction.prune", () => { yield* compact.prune({ sessionID: info.id }) - const msgs = yield* session.messages({ sessionID: info.id }) + const msgs = yield* ssn.messages({ sessionID: info.id }) const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool") expect(part?.type).toBe("tool") if (part?.type === "tool" && part.state.status === "completed") { @@ -680,12 +701,12 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const msg = await user(session.id, "hello") const reply = await assistant(session.id, msg.id, tmp.path) const rt = runtime("continue") try { - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) await expect( rt.runPromise( SessionCompaction.Service.use((svc) => @@ -710,9 +731,9 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const msg = await user(session.id, "hello") - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) const done = defer() let seen = false const rt = runtime("continue", Plugin.defaultLayer, wide()) @@ -760,11 +781,11 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const msg = await user(session.id, "hello") const rt = runtime("compact", Plugin.defaultLayer, wide()) try { - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) const result = await rt.runPromise( SessionCompaction.Service.use((svc) => svc.process({ @@ -776,7 +797,7 @@ describe("session.compaction.process", () => { ), ) - const summary = (await Session.messages({ sessionID: session.id })).find( + const summary = (await svc.messages({ sessionID: session.id })).find( (msg) => msg.info.role === "assistant" && msg.info.summary, ) @@ -798,11 +819,11 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const msg = await user(session.id, "hello") const rt = runtime("continue", Plugin.defaultLayer, wide()) try { - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) const result = await rt.runPromise( SessionCompaction.Service.use((svc) => svc.process({ @@ -814,7 +835,7 @@ describe("session.compaction.process", () => { ), ) - const all = await Session.messages({ sessionID: session.id }) + const all = await svc.messages({ sessionID: session.id }) const last = all.at(-1) expect(result).toBe("continue") @@ -838,11 +859,11 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const msg = await user(session.id, "hello") const rt = runtime("continue", autocontinue(false), wide()) try { - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) const result = await rt.runPromise( SessionCompaction.Service.use((svc) => svc.process({ @@ -854,7 +875,7 @@ describe("session.compaction.process", () => { ), ) - const all = await Session.messages({ sessionID: session.id }) + const all = await svc.messages({ sessionID: session.id }) const last = all.at(-1) expect(result).toBe("continue") @@ -881,10 +902,10 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) await user(session.id, "root") const replay = await user(session.id, "image") - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), messageID: replay.id, sessionID: session.id, @@ -896,7 +917,7 @@ describe("session.compaction.process", () => { const msg = await user(session.id, "current") const rt = runtime("continue", Plugin.defaultLayer, wide()) try { - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) const result = await rt.runPromise( SessionCompaction.Service.use((svc) => svc.process({ @@ -909,7 +930,7 @@ describe("session.compaction.process", () => { ), ) - const last = (await Session.messages({ sessionID: session.id })).at(-1) + const last = (await svc.messages({ sessionID: session.id })).at(-1) expect(result).toBe("continue") expect(last?.info.role).toBe("user") @@ -929,13 +950,13 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) await user(session.id, "earlier") const msg = await user(session.id, "current") const rt = runtime("continue", Plugin.defaultLayer, wide()) try { - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) const result = await rt.runPromise( SessionCompaction.Service.use((svc) => svc.process({ @@ -948,7 +969,7 @@ describe("session.compaction.process", () => { ), ) - const last = (await Session.messages({ sessionID: session.id })).at(-1) + const last = (await svc.messages({ sessionID: session.id })).at(-1) expect(result).toBe("continue") expect(last?.info.role).toBe("user") @@ -989,9 +1010,9 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const msg = await user(session.id, "hello") - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) const abort = new AbortController() const rt = liveRuntime(stub.layer, wide()) let off: (() => void) | undefined @@ -1063,9 +1084,9 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const msg = await user(session.id, "hello") - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) const abort = new AbortController() const rt = runtime("continue", plugin(ready), wide()) let run: Promise<"continue" | "stop"> | undefined @@ -1100,7 +1121,7 @@ describe("session.compaction.process", () => { abort.abort() expect(await run).toBe("stop") - const all = await Session.messages({ sessionID: session.id }) + const all = await svc.messages({ sessionID: session.id }) expect(all.some((msg) => msg.info.role === "assistant" && msg.info.summary)).toBe(false) } finally { abort.abort() @@ -1165,11 +1186,11 @@ describe("session.compaction.process", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const msg = await user(session.id, "hello") const rt = liveRuntime(stub.layer, wide()) try { - const msgs = await Session.messages({ sessionID: session.id }) + const msgs = await svc.messages({ sessionID: session.id }) await rt.runPromise( SessionCompaction.Service.use((svc) => svc.process({ @@ -1181,7 +1202,7 @@ describe("session.compaction.process", () => { ), ) - const summary = (await Session.messages({ sessionID: session.id })).find( + const summary = (await svc.messages({ sessionID: session.id })).find( (item) => item.info.role === "assistant" && item.info.summary, ) @@ -1211,10 +1232,10 @@ describe("util.token.estimate", () => { }) }) -describe("session.getUsage", () => { +describe("SessionNs.getUsage", () => { test("normalizes standard usage to token format", () => { const model = createModel({ context: 100_000, output: 32_000 }) - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 1000, @@ -1241,7 +1262,7 @@ describe("session.getUsage", () => { test("extracts cached tokens to cache.read", () => { const model = createModel({ context: 100_000, output: 32_000 }) - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 1000, @@ -1265,7 +1286,7 @@ describe("session.getUsage", () => { test("handles anthropic cache write metadata", () => { const model = createModel({ context: 100_000, output: 32_000 }) - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 1000, @@ -1294,7 +1315,7 @@ describe("session.getUsage", () => { test("subtracts cached tokens for anthropic provider", () => { const model = createModel({ context: 100_000, output: 32_000 }) // AI SDK v6 normalizes inputTokens to include cached tokens for all providers - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 1000, @@ -1321,7 +1342,7 @@ describe("session.getUsage", () => { test("separates reasoning tokens from output tokens", () => { const model = createModel({ context: 100_000, output: 32_000 }) - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 1000, @@ -1355,7 +1376,7 @@ describe("session.getUsage", () => { cache: { read: 0, write: 0 }, }, }) - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 0, @@ -1380,7 +1401,7 @@ describe("session.getUsage", () => { test("handles undefined optional values gracefully", () => { const model = createModel({ context: 100_000, output: 32_000 }) - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 0, @@ -1416,7 +1437,7 @@ describe("session.getUsage", () => { cache: { read: 0.3, write: 3.75 }, }, }) - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 1_000_000, @@ -1457,7 +1478,7 @@ describe("session.getUsage", () => { }, } if (npm === "@ai-sdk/amazon-bedrock") { - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage, metadata: { @@ -1478,7 +1499,7 @@ describe("session.getUsage", () => { return } - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage, metadata: { @@ -1499,7 +1520,7 @@ describe("session.getUsage", () => { test("extracts cache write tokens from vertex metadata key", () => { const model = createModel({ context: 100_000, output: 32_000, npm: "@ai-sdk/google-vertex/anthropic" }) - const result = Session.getUsage({ + const result = SessionNs.getUsage({ model, usage: { inputTokens: 1000, diff --git a/packages/opencode/test/session/messages-pagination.test.ts b/packages/opencode/test/session/messages-pagination.test.ts index bb9df6aea3..668918ec83 100644 --- a/packages/opencode/test/session/messages-pagination.test.ts +++ b/packages/opencode/test/session/messages-pagination.test.ts @@ -1,7 +1,8 @@ import { describe, expect, test } from "bun:test" +import { Effect } from "effect" import path from "path" import { Instance } from "../../src/project/instance" -import { Session } from "../../src/session" +import { Session as SessionNs } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" import { MessageID, PartID, type SessionID } from "../../src/session/schema" import { ModelID, ProviderID } from "../../src/provider/schema" @@ -10,12 +11,32 @@ import { Log } from "../../src/util/log" const root = path.join(__dirname, "../..") Log.init({ print: false }) +function run(fx: Effect.Effect) { + return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer))) +} + +const svc = { + ...SessionNs, + create(input?: SessionNs.CreateInput) { + return run(SessionNs.Service.use((svc) => svc.create(input))) + }, + remove(id: SessionID) { + return run(SessionNs.Service.use((svc) => svc.remove(id))) + }, + updateMessage(msg: T) { + return run(SessionNs.Service.use((svc) => svc.updateMessage(msg))) + }, + updatePart(part: T) { + return run(SessionNs.Service.use((svc) => svc.updatePart(part))) + }, +} + async function fill(sessionID: SessionID, count: number, time = (i: number) => Date.now() + i) { const ids = [] as MessageID[] for (let i = 0; i < count; i++) { const id = MessageID.ascending() ids.push(id) - await Session.updateMessage({ + await svc.updateMessage({ id, sessionID, role: "user", @@ -25,7 +46,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D tools: {}, mode: "", } as unknown as MessageV2.Info) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID, messageID: id, @@ -38,7 +59,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D async function addUser(sessionID: SessionID, text?: string) { const id = MessageID.ascending() - await Session.updateMessage({ + await svc.updateMessage({ id, sessionID, role: "user", @@ -49,7 +70,7 @@ async function addUser(sessionID: SessionID, text?: string) { mode: "", } as unknown as MessageV2.Info) if (text) { - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID, messageID: id, @@ -66,7 +87,7 @@ async function addAssistant( opts?: { summary?: boolean; finish?: string; error?: MessageV2.Assistant["error"] }, ) { const id = MessageID.ascending() - await Session.updateMessage({ + await svc.updateMessage({ id, sessionID, role: "assistant", @@ -87,7 +108,7 @@ async function addAssistant( } async function addCompactionPart(sessionID: SessionID, messageID: MessageID) { - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID, messageID, @@ -101,14 +122,14 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) await fill(session.id, 2) const result = MessageV2.page({ sessionID: session.id, limit: 10 }) expect(result).toBeDefined() expect(result.items).toBeArray() - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -117,7 +138,7 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 6) const a = MessageV2.page({ sessionID: session.id, limit: 2 }) @@ -136,7 +157,7 @@ describe("MessageV2.page", () => { expect(c.more).toBe(false) expect(c.cursor).toBeUndefined() - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -145,13 +166,13 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 4) const result = MessageV2.page({ sessionID: session.id, limit: 4 }) expect(result.items.map((item) => item.info.id)).toEqual(ids) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -160,14 +181,14 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const result = MessageV2.page({ sessionID: session.id, limit: 10 }) expect(result.items).toEqual([]) expect(result.more).toBe(false) expect(result.cursor).toBeUndefined() - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -186,7 +207,7 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 3) const result = MessageV2.page({ sessionID: session.id, limit: 3 }) @@ -194,7 +215,7 @@ describe("MessageV2.page", () => { expect(result.more).toBe(false) expect(result.cursor).toBeUndefined() - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -203,7 +224,7 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 5) const result = MessageV2.page({ sessionID: session.id, limit: 1 }) @@ -211,7 +232,7 @@ describe("MessageV2.page", () => { expect(result.items[0].info.id).toBe(ids[ids.length - 1]) expect(result.more).toBe(true) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -220,10 +241,10 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const [id] = await fill(session.id, 1) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID: session.id, messageID: id, @@ -235,7 +256,7 @@ describe("MessageV2.page", () => { expect(result.items).toHaveLength(1) expect(result.items[0].parts).toHaveLength(2) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -244,7 +265,7 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 4, (i) => 1000.5 + i) const a = MessageV2.page({ sessionID: session.id, limit: 2 }) @@ -253,7 +274,7 @@ describe("MessageV2.page", () => { expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2)) expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2)) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -262,7 +283,7 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 4, () => 1000) const a = MessageV2.page({ sessionID: session.id, limit: 2 }) @@ -273,7 +294,7 @@ describe("MessageV2.page", () => { expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2)) expect(b.more).toBe(false) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -282,8 +303,8 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const a = await Session.create({}) - const b = await Session.create({}) + const a = await svc.create({}) + const b = await svc.create({}) await fill(a.id, 3) await fill(b.id, 2) @@ -294,8 +315,8 @@ describe("MessageV2.page", () => { expect(resultA.items.every((item) => item.info.sessionID === a.id)).toBe(true) expect(resultB.items.every((item) => item.info.sessionID === b.id)).toBe(true) - await Session.remove(a.id) - await Session.remove(b.id) + await svc.remove(a.id) + await svc.remove(b.id) }, }) }) @@ -304,7 +325,7 @@ describe("MessageV2.page", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 10) const result = MessageV2.page({ sessionID: session.id, limit: 100 }) @@ -313,7 +334,7 @@ describe("MessageV2.page", () => { expect(result.more).toBe(false) expect(result.cursor).toBeUndefined() - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -324,13 +345,13 @@ describe("MessageV2.stream", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 5) const items = Array.from(MessageV2.stream(session.id)) expect(items.map((item) => item.info.id)).toEqual(ids.slice().reverse()) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -339,12 +360,12 @@ describe("MessageV2.stream", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const items = Array.from(MessageV2.stream(session.id)) expect(items).toHaveLength(0) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -353,14 +374,14 @@ describe("MessageV2.stream", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 1) const items = Array.from(MessageV2.stream(session.id)) expect(items).toHaveLength(1) expect(items[0].info.id).toBe(ids[0]) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -369,7 +390,7 @@ describe("MessageV2.stream", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) await fill(session.id, 3) const items = Array.from(MessageV2.stream(session.id)) @@ -378,7 +399,7 @@ describe("MessageV2.stream", () => { expect(item.parts[0].type).toBe("text") } - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -387,7 +408,7 @@ describe("MessageV2.stream", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 60) const items = Array.from(MessageV2.stream(session.id)) @@ -395,7 +416,7 @@ describe("MessageV2.stream", () => { expect(items[0].info.id).toBe(ids[ids.length - 1]) expect(items[59].info.id).toBe(ids[0]) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -404,7 +425,7 @@ describe("MessageV2.stream", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) await fill(session.id, 1) const gen = MessageV2.stream(session.id) @@ -414,7 +435,7 @@ describe("MessageV2.stream", () => { expect(first).toHaveProperty("done") expect(first.done).toBe(false) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -425,7 +446,7 @@ describe("MessageV2.parts", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const [id] = await fill(session.id, 1) const result = MessageV2.parts(id) @@ -433,7 +454,7 @@ describe("MessageV2.parts", () => { expect(result[0].type).toBe("text") expect((result[0] as MessageV2.TextPart).text).toBe("m0") - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -442,13 +463,13 @@ describe("MessageV2.parts", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const id = await addUser(session.id) const result = MessageV2.parts(id) expect(result).toEqual([]) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -457,17 +478,17 @@ describe("MessageV2.parts", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const [id] = await fill(session.id, 1) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID: session.id, messageID: id, type: "text", text: "second", }) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID: session.id, messageID: id, @@ -481,7 +502,7 @@ describe("MessageV2.parts", () => { expect((result[1] as MessageV2.TextPart).text).toBe("second") expect((result[2] as MessageV2.TextPart).text).toBe("third") - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -490,7 +511,7 @@ describe("MessageV2.parts", () => { await Instance.provide({ directory: root, fn: async () => { - await Session.create({}) + await svc.create({}) const result = MessageV2.parts(MessageID.ascending()) expect(result).toEqual([]) }, @@ -501,14 +522,14 @@ describe("MessageV2.parts", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const [id] = await fill(session.id, 1) const result = MessageV2.parts(id) expect(result[0].sessionID).toBe(session.id) expect(result[0].messageID).toBe(id) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -519,7 +540,7 @@ describe("MessageV2.get", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const [id] = await fill(session.id, 1) const result = MessageV2.get({ sessionID: session.id, messageID: id }) @@ -529,7 +550,7 @@ describe("MessageV2.get", () => { expect(result.parts).toHaveLength(1) expect((result.parts[0] as MessageV2.TextPart).text).toBe("m0") - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -538,13 +559,13 @@ describe("MessageV2.get", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) expect(() => MessageV2.get({ sessionID: session.id, messageID: MessageID.ascending() })).toThrow( "NotFoundError", ) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -553,16 +574,16 @@ describe("MessageV2.get", () => { await Instance.provide({ directory: root, fn: async () => { - const a = await Session.create({}) - const b = await Session.create({}) + const a = await svc.create({}) + const b = await svc.create({}) const [id] = await fill(a.id, 1) expect(() => MessageV2.get({ sessionID: b.id, messageID: id })).toThrow("NotFoundError") const result = MessageV2.get({ sessionID: a.id, messageID: id }) expect(result.info.id).toBe(id) - await Session.remove(a.id) - await Session.remove(b.id) + await svc.remove(a.id) + await svc.remove(b.id) }, }) }) @@ -571,10 +592,10 @@ describe("MessageV2.get", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const [id] = await fill(session.id, 1) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID: session.id, messageID: id, @@ -585,7 +606,7 @@ describe("MessageV2.get", () => { const result = MessageV2.get({ sessionID: session.id, messageID: id }) expect(result.parts).toHaveLength(2) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -594,11 +615,11 @@ describe("MessageV2.get", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const uid = await addUser(session.id, "hello") const aid = await addAssistant(session.id, uid) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID: session.id, messageID: aid, @@ -611,7 +632,7 @@ describe("MessageV2.get", () => { expect(result.parts).toHaveLength(1) expect((result.parts[0] as MessageV2.TextPart).text).toBe("response") - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -620,14 +641,14 @@ describe("MessageV2.get", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const id = await addUser(session.id) const result = MessageV2.get({ sessionID: session.id, messageID: id }) expect(result.info.id).toBe(id) expect(result.parts).toEqual([]) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -638,7 +659,7 @@ describe("MessageV2.filterCompacted", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 5) const result = MessageV2.filterCompacted(MessageV2.stream(session.id)) @@ -646,7 +667,7 @@ describe("MessageV2.filterCompacted", () => { // reversed from newest-first to chronological expect(result.map((item) => item.info.id)).toEqual(ids) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -655,13 +676,13 @@ describe("MessageV2.filterCompacted", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) // Chronological: u1(+compaction part), a1(summary, parentID=u1), u2, a2 // Stream (newest first): a2, u2, a1(adds u1 to completed), u1(in completed + compaction) -> break const u1 = await addUser(session.id, "first question") const a1 = await addAssistant(session.id, u1, { summary: true, finish: "end_turn" }) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID: session.id, messageID: a1, @@ -672,7 +693,7 @@ describe("MessageV2.filterCompacted", () => { const u2 = await addUser(session.id, "new question") const a2 = await addAssistant(session.id, u2) - await Session.updatePart({ + await svc.updatePart({ id: PartID.ascending(), sessionID: session.id, messageID: a2, @@ -685,7 +706,7 @@ describe("MessageV2.filterCompacted", () => { expect(result[0].info.id).toBe(u1) expect(result.length).toBe(4) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -699,7 +720,7 @@ describe("MessageV2.filterCompacted", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const u1 = await addUser(session.id, "hello") await addCompactionPart(session.id, u1) @@ -708,7 +729,7 @@ describe("MessageV2.filterCompacted", () => { const result = MessageV2.filterCompacted(MessageV2.stream(session.id)) expect(result).toHaveLength(2) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -717,7 +738,7 @@ describe("MessageV2.filterCompacted", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const u1 = await addUser(session.id, "hello") await addCompactionPart(session.id, u1) @@ -733,7 +754,7 @@ describe("MessageV2.filterCompacted", () => { // Error assistant doesn't add to completed, so compaction boundary never triggers expect(result).toHaveLength(3) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -742,7 +763,7 @@ describe("MessageV2.filterCompacted", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const u1 = await addUser(session.id, "hello") await addCompactionPart(session.id, u1) @@ -754,7 +775,7 @@ describe("MessageV2.filterCompacted", () => { const result = MessageV2.filterCompacted(MessageV2.stream(session.id)) expect(result).toHaveLength(3) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -808,7 +829,7 @@ describe("MessageV2 consistency", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) await fill(session.id, 3) const paged = MessageV2.page({ sessionID: session.id, limit: 10 }) @@ -818,7 +839,7 @@ describe("MessageV2 consistency", () => { expect(got.parts).toEqual(item.parts) } - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -827,14 +848,14 @@ describe("MessageV2 consistency", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const [id] = await fill(session.id, 1) const got = MessageV2.get({ sessionID: session.id, messageID: id }) const standalone = MessageV2.parts(id) expect(got.parts).toEqual(standalone) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -843,7 +864,7 @@ describe("MessageV2 consistency", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) await fill(session.id, 7) const streamed = Array.from(MessageV2.stream(session.id)) @@ -861,7 +882,7 @@ describe("MessageV2 consistency", () => { expect(streamed.map((m) => m.info.id)).toEqual(paged.map((m) => m.info.id)) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) @@ -870,7 +891,7 @@ describe("MessageV2 consistency", () => { await Instance.provide({ directory: root, fn: async () => { - const session = await Session.create({}) + const session = await svc.create({}) const ids = await fill(session.id, 4) const filtered = MessageV2.filterCompacted(MessageV2.stream(session.id)) @@ -878,7 +899,7 @@ describe("MessageV2 consistency", () => { expect(filtered.map((m) => m.info.id)).toEqual(all.map((m) => m.info.id)) - await Session.remove(session.id) + await svc.remove(session.id) }, }) }) diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index 75c74002a7..15132a2701 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -1,43 +1,62 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { Session } from "../../src/session" +import { Session as SessionNs } from "../../src/session" import { Bus } from "../../src/bus" import { Log } from "../../src/util/log" import { Instance } from "../../src/project/instance" import { MessageV2 } from "../../src/session/message-v2" -import { MessageID, PartID } from "../../src/session/schema" +import { MessageID, PartID, type SessionID } from "../../src/session/schema" +import { AppRuntime } from "../../src/effect/app-runtime" import { tmpdir } from "../fixture/fixture" const projectRoot = path.join(__dirname, "../..") Log.init({ print: false }) +function create(input?: SessionNs.CreateInput) { + return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create(input))) +} + +function get(id: SessionID) { + return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.get(id))) +} + +function remove(id: SessionID) { + return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.remove(id))) +} + +function updateMessage(msg: T) { + return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.updateMessage(msg))) +} + +function updatePart(part: T) { + return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.updatePart(part))) +} + describe("session.created event", () => { test("should emit session.created event when session is created", async () => { await Instance.provide({ directory: projectRoot, fn: async () => { let eventReceived = false - let receivedInfo: Session.Info | undefined + let receivedInfo: SessionNs.Info | undefined - const unsub = Bus.subscribe(Session.Event.Created, (event) => { + const unsub = Bus.subscribe(SessionNs.Event.Created, (event) => { eventReceived = true - receivedInfo = event.properties.info as Session.Info + receivedInfo = event.properties.info as SessionNs.Info }) - const session = await Session.create({}) - + const info = await create({}) await new Promise((resolve) => setTimeout(resolve, 100)) - unsub() expect(eventReceived).toBe(true) expect(receivedInfo).toBeDefined() - expect(receivedInfo?.id).toBe(session.id) - expect(receivedInfo?.projectID).toBe(session.projectID) - expect(receivedInfo?.directory).toBe(session.directory) - expect(receivedInfo?.title).toBe(session.title) + expect(receivedInfo?.id).toBe(info.id) + expect(receivedInfo?.projectID).toBe(info.projectID) + expect(receivedInfo?.directory).toBe(info.directory) + expect(receivedInfo?.title).toBe(info.title) - await Session.remove(session.id) + await remove(info.id) }, }) }) @@ -48,18 +67,16 @@ describe("session.created event", () => { fn: async () => { const events: string[] = [] - const unsubCreated = Bus.subscribe(Session.Event.Created, () => { + const unsubCreated = Bus.subscribe(SessionNs.Event.Created, () => { events.push("created") }) - const unsubUpdated = Bus.subscribe(Session.Event.Updated, () => { + const unsubUpdated = Bus.subscribe(SessionNs.Event.Updated, () => { events.push("updated") }) - const session = await Session.create({}) - + const info = await create({}) await new Promise((resolve) => setTimeout(resolve, 100)) - unsubCreated() unsubUpdated() @@ -67,7 +84,7 @@ describe("session.created event", () => { expect(events).toContain("updated") expect(events.indexOf("created")).toBeLessThan(events.indexOf("updated")) - await Session.remove(session.id) + await remove(info.id) }, }) }) @@ -80,12 +97,12 @@ describe("step-finish token propagation via Bus event", () => { await Instance.provide({ directory: projectRoot, fn: async () => { - const session = await Session.create({}) + const info = await create({}) const messageID = MessageID.ascending() - await Session.updateMessage({ + await updateMessage({ id: messageID, - sessionID: session.id, + sessionID: info.id, role: "user", time: { created: Date.now() }, agent: "user", @@ -110,15 +127,14 @@ describe("step-finish token propagation via Bus event", () => { const partInput = { id: PartID.ascending(), messageID, - sessionID: session.id, + sessionID: info.id, type: "step-finish" as const, reason: "stop", cost: 0.005, tokens, } - await Session.updatePart(partInput) - + await updatePart(partInput) await new Promise((resolve) => setTimeout(resolve, 100)) expect(received).toBeDefined() @@ -134,7 +150,7 @@ describe("step-finish token propagation via Bus event", () => { expect(received).not.toBe(partInput) unsub() - await Session.remove(session.id) + await remove(info.id) }, }) }, @@ -146,17 +162,17 @@ describe("Session", () => { test("remove works without an instance", async () => { await using tmp = await tmpdir({ git: true }) - const session = await Instance.provide({ + const info = await Instance.provide({ directory: tmp.path, - fn: async () => Session.create({ title: "remove-without-instance" }), + fn: () => create({ title: "remove-without-instance" }), }) await expect(async () => { - await Session.remove(session.id) + await remove(info.id) }).not.toThrow() let missing = false - await Session.get(session.id).catch(() => { + await get(info.id).catch(() => { missing = true })