| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- import { describe, expect } from "bun:test"
- import { Effect, Layer, Schema } from "effect"
- import { Database } from "@opencode-ai/core/database/database"
- import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
- import { LayerNode } from "@opencode-ai/core/effect/layer-node"
- import { EventV2 } from "@opencode-ai/core/event"
- import { Location } from "@opencode-ai/core/location"
- import { ProjectV2 } from "@opencode-ai/core/project"
- import { ProjectTable } from "@opencode-ai/core/project/sql"
- import { AbsolutePath } from "@opencode-ai/core/schema"
- import { SessionV2 } from "@opencode-ai/core/session"
- import { SessionExecution } from "@opencode-ai/core/session/execution"
- import { SessionProjector } from "@opencode-ai/core/session/projector"
- 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(
- ProjectV2.Service,
- ProjectV2.Service.of({
- resolve: (directory) => Effect.succeed({ id: ProjectV2.ID.global, directory }),
- directories: () => Effect.succeed([]),
- commit: () => Effect.void,
- }),
- )
- const it = testEffect(
- AppNodeBuilder.build(
- LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node, SessionV2.node]),
- [
- [ProjectV2.node, projects],
- [SessionExecution.node, SessionExecution.noopLayer],
- ],
- ),
- )
- const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
- const GapEvent = EventV2.define({
- type: "test.session.history.gap",
- durable: { aggregate: "sessionID", version: 1 },
- schema: { sessionID: SessionV2.ID, value: Schema.String },
- })
- describe("SessionV2.history", () => {
- it.effect("returns an exhausted page for a migrated Session with no event sequence", () =>
- Effect.gen(function* () {
- const db = (yield* Database.Service).db
- const session = yield* SessionV2.Service
- const sessionID = SessionV2.ID.make("ses_empty_history")
- yield* db
- .insert(ProjectTable)
- .values({ id: ProjectV2.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
- .onConflictDoNothing()
- .run()
- yield* db
- .insert(SessionTable)
- .values({
- id: sessionID,
- project_id: ProjectV2.ID.global,
- slug: "empty-history",
- directory: "/project",
- title: "Empty history",
- version: "test",
- })
- .run()
- const first = yield* session.history({ sessionID, limit: 10 })
- expect(first).toEqual({ events: [], hasMore: false })
- }),
- )
- it.effect("treats after as an exclusive aggregate sequence", () =>
- Effect.gen(function* () {
- const session = yield* SessionV2.Service
- const created = yield* session.create({ location })
- yield* session.switchAgent({ sessionID: created.id, agent: "one" })
- yield* session.switchAgent({ sessionID: created.id, agent: "two" })
- const page = yield* session.history({ sessionID: created.id, after: 1, limit: 10 })
- expect(page.events.map((event) => event.durable?.seq)).toEqual([2])
- expect(page.hasMore).toBe(false)
- }),
- )
- it.effect("paginates public events in aggregate order across filtered gaps without duplicates", () =>
- Effect.gen(function* () {
- const session = yield* SessionV2.Service
- const events = yield* EventV2.Service
- const created = yield* session.create({ location })
- yield* session.switchAgent({ sessionID: created.id, agent: "one" })
- yield* events.publish(GapEvent, { sessionID: created.id, value: "filtered" })
- yield* session.switchAgent({ sessionID: created.id, agent: "two" })
- yield* session.switchAgent({ sessionID: created.id, agent: "three" })
- const first = yield* session.history({ sessionID: created.id, limit: 2 })
- const after = first.events.at(-1)?.durable?.seq
- const second = yield* session.history({
- sessionID: created.id,
- after,
- limit: 2,
- })
- const sequence = [...first.events, ...second.events].map((event) => event.durable?.seq)
- expect(first.hasMore).toBe(true)
- expect(second.hasMore).toBe(false)
- expect(sequence).toEqual([1, 3, 4])
- expect(new Set(sequence).size).toBe(sequence.length)
- }),
- )
- it.effect("includes events committed between pages", () =>
- Effect.gen(function* () {
- const session = yield* SessionV2.Service
- const created = yield* session.create({ location })
- yield* session.switchAgent({ sessionID: created.id, agent: "one" })
- yield* session.switchAgent({ sessionID: created.id, agent: "two" })
- const first = yield* session.history({ sessionID: created.id, limit: 1 })
- yield* session.switchAgent({ sessionID: created.id, agent: "later" })
- const second = yield* session.history({
- sessionID: created.id,
- after: first.events.at(-1)?.durable?.seq,
- limit: 10,
- })
- expect(first.hasMore).toBe(true)
- expect([...first.events, ...second.events].map((event) => event.durable?.seq)).toEqual([1, 2, 3])
- expect(second.hasMore).toBe(false)
- }),
- )
- it.effect("reports exhaustion for exact-limit and limit-plus-one pages", () =>
- Effect.gen(function* () {
- const session = yield* SessionV2.Service
- const created = yield* session.create({ location })
- yield* session.switchAgent({ sessionID: created.id, agent: "one" })
- yield* session.switchAgent({ sessionID: created.id, agent: "two" })
- const exact = yield* session.history({ sessionID: created.id, limit: 2 })
- const oneMore = yield* session.history({ sessionID: created.id, limit: 1 })
- const exhausted = yield* session.history({
- sessionID: created.id,
- after: oneMore.events.at(-1)?.durable?.seq,
- limit: 1,
- })
- expect(exact.events).toHaveLength(2)
- expect(exact.hasMore).toBe(false)
- expect(oneMore.events).toHaveLength(1)
- expect(oneMore.hasMore).toBe(true)
- expect(exhausted.events).toHaveLength(1)
- expect(exhausted.hasMore).toBe(false)
- }),
- )
- it.effect("fails with NotFoundError for a missing Session", () =>
- Effect.gen(function* () {
- const session = yield* SessionV2.Service
- const error = yield* session.history({ sessionID: SessionV2.ID.make("ses_missing"), limit: 10 }).pipe(Effect.flip)
- expect(error._tag).toBe("Session.NotFoundError")
- }),
- )
- })
|