| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- import { describe, expect } from "bun:test"
- import { Effect, Fiber, Layer, Schema, Stream } from "effect"
- import { Database } from "@opencode-ai/core/database/database"
- import { Agent } from "@opencode-ai/core/agent"
- import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
- import { LayerNode } from "@opencode-ai/util/effect/layer-node"
- import { Bus } from "@opencode-ai/core/bus"
- import { Event } from "@opencode-ai/schema/event"
- import { Location } from "@opencode-ai/core/location"
- import { Project } from "@opencode-ai/core/project"
- import { ProjectTable } from "@opencode-ai/core/project/sql"
- import { AbsolutePath } from "@opencode-ai/core/schema"
- import { Session } from "@opencode-ai/core/session"
- import { SessionProjector } from "@opencode-ai/core/session/projector"
- import { SessionExecution } from "@opencode-ai/core/session/execution"
- import { SessionStore } from "@opencode-ai/core/session/store"
- import { SessionTable } from "@opencode-ai/core/session/sql"
- import { testEffect } from "./lib/effect"
- const projects = Layer.succeed(
- Project.Service,
- Project.Service.of({
- list: () => Effect.succeed([]),
- resolve: (directory) => Effect.succeed({ id: Project.ID.global, directory, canonical: directory }),
- directories: () => Effect.succeed([]),
- commit: () => Effect.void,
- }),
- )
- const it = testEffect(
- AppNodeBuilder.build(
- LayerNode.group([Database.node, Bus.node, SessionProjector.node, SessionStore.node, Session.node]),
- [
- [Project.node, projects],
- [SessionExecution.node, SessionExecution.noopLayer],
- ],
- ),
- )
- const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
- describe("Session.log", () => {
- it.effect("replays public session events and marks synced at the aggregate watermark", () =>
- Effect.gen(function* () {
- const session = yield* Session.Service
- const bus = yield* Bus.Service
- const created = yield* session.create({ location })
- yield* session.rename({ sessionID: created.id, title: "session.renamed" })
- const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id })))
- const watermark = (yield* bus.sequences([created.id])).get(created.id)
- // Session creation commits a non-public durable event, so the marker's
- // seq covers more of the aggregate than the public events emitted.
- expect(items.map((item) => item.type)).toEqual(["session.renamed", "log.synced"])
- expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: watermark })
- }),
- )
- it.effect("continues with live public events when following", () =>
- Effect.gen(function* () {
- const session = yield* Session.Service
- const created = yield* session.create({ location })
- const fiber = yield* session
- .log({ sessionID: created.id, follow: true })
- .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
- yield* Effect.yieldNow
- yield* session.rename({ sessionID: created.id, title: "renamed live" })
- const items = Array.from(yield* Fiber.join(fiber))
- expect(items.map((item) => item.type)).toEqual(["log.synced", "session.renamed"])
- }),
- )
- it.effect("fails with NotFound for an unknown session", () =>
- Effect.gen(function* () {
- const session = yield* Session.Service
- const error = yield* Effect.flip(Stream.runCollect(session.log({ sessionID: Session.ID.create() })))
- expect(error._tag).toBe("Session.NotFoundError")
- }),
- )
- it.effect("reads across undecodable gaps in aggregate order and marks the true log position", () =>
- Effect.gen(function* () {
- const GapEvent = Bus.durable({
- type: "test.session.log.gap",
- durable: { aggregate: "sessionID", version: 1 },
- schema: { sessionID: Session.ID, value: Schema.String },
- })
- const session = yield* Session.Service
- const bus = yield* Bus.Service
- const created = yield* session.create({ location })
- yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("one") })
- // Not in the durable manifest, so reads must skip it without failing.
- yield* bus.publish(GapEvent, { sessionID: created.id, value: "filtered" })
- yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("two") })
- yield* session.switchAgent({ sessionID: created.id, agent: Agent.ID.make("three") })
- const items = Array.from(yield* Stream.runCollect(session.log({ sessionID: created.id, after: 1 })))
- expect(
- items.map((item): number | string | undefined => (Bus.isSynced(item) ? item.type : item.durable?.seq)),
- ).toEqual([3, 4, "log.synced"])
- expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID: created.id, seq: Event.Seq.make(4) })
- }),
- )
- it.effect("completes with a bare synced marker for a migrated Session with no event sequence", () =>
- Effect.gen(function* () {
- const db = (yield* Database.Service).db
- const session = yield* Session.Service
- const sessionID = Session.ID.make("ses_empty_log")
- yield* db
- .insert(ProjectTable)
- .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
- .onConflictDoNothing()
- .run()
- yield* db
- .insert(SessionTable)
- .values({
- id: sessionID,
- project_id: Project.ID.global,
- slug: "empty-log",
- directory: "/project",
- title: "Empty log",
- version: "test",
- })
- .run()
- const items = Array.from(yield* Stream.runCollect(session.log({ sessionID })))
- expect(items).toEqual([{ type: "log.synced", aggregateID: sessionID }])
- }),
- )
- })
|