| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302 |
- import { afterEach, describe, expect } from "bun:test"
- import { Effect } from "effect"
- import { AppNodeBuilder } from "@kirincode-ai/core/effect/app-node-builder"
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import { Database } from "@kirincode-ai/core/database/database"
- import { SessionProjector } from "@kirincode-ai/core/session/projector"
- import { Session as SessionNs } from "@/session/session"
- import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture"
- import { mkdir } from "fs/promises"
- import path from "path"
- import { SessionTable } from "@kirincode-ai/core/session/sql"
- import { eq } from "drizzle-orm"
- import { testEffect } from "../lib/effect"
- import { RuntimeFlags } from "@/effect/runtime-flags"
- const layer = (experimentalWorkspaces: boolean) =>
- AppNodeBuilder.build(LayerNode.group([Database.node, SessionNs.node, SessionProjector.node]), [
- [RuntimeFlags.node, RuntimeFlags.layer({ experimentalWorkspaces })],
- ])
- const it = testEffect(layer(false))
- const itWorkspaces = testEffect(layer(true))
- const withSession = (input?: Parameters<SessionNs.Interface["create"]>[0]) =>
- Effect.acquireRelease(SessionNs.use.create(input), (created) =>
- SessionNs.Service.use((session) => session.remove(created.id).pipe(Effect.ignore)),
- )
- afterEach(async () => {
- await disposeAllInstances()
- })
- describe("session.list", () => {
- it.instance(
- "does not filter by directory when directory is omitted",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "kirincode"), { recursive: true }))
- yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "app"), { recursive: true }))
- const root = yield* withSession({ title: "root" })
- const parent = yield* withSession({ title: "parent" }).pipe(
- provideInstance(path.join(test.directory, "packages")),
- )
- const current = yield* withSession({ title: "current" }).pipe(
- provideInstance(path.join(test.directory, "packages", "kirincode")),
- )
- const sibling = yield* withSession({ title: "sibling" }).pipe(
- provideInstance(path.join(test.directory, "packages", "app")),
- )
- const ids = (yield* SessionNs.use.list()).map((session) => session.id)
- expect(ids).toContain(root.id)
- expect(ids).toContain(parent.id)
- expect(ids).toContain(current.id)
- expect(ids).toContain(sibling.id)
- }),
- { git: true },
- )
- it.instance(
- "filters by directory when directory is provided",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "kirincode"), { recursive: true }))
- yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "app"), { recursive: true }))
- const root = yield* withSession({ title: "root" })
- const parent = yield* withSession({ title: "parent" }).pipe(
- provideInstance(path.join(test.directory, "packages")),
- )
- const current = yield* withSession({ title: "current" }).pipe(
- provideInstance(path.join(test.directory, "packages", "kirincode")),
- )
- const sibling = yield* withSession({ title: "sibling" }).pipe(
- provideInstance(path.join(test.directory, "packages", "app")),
- )
- const ids = (yield* SessionNs.Service.use((session) =>
- session.list({ directory: path.join(test.directory, "packages", "kirincode") }),
- )).map((session) => session.id)
- expect(ids).not.toContain(root.id)
- expect(ids).not.toContain(parent.id)
- expect(ids).toContain(current.id)
- expect(ids).not.toContain(sibling.id)
- }),
- { git: true },
- )
- itWorkspaces.instance(
- "filters by directory when experimental workspaces are enabled",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "kirincode"), { recursive: true }))
- yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "app"), { recursive: true }))
- const current = yield* withSession({ title: "current" }).pipe(
- provideInstance(path.join(test.directory, "packages", "kirincode")),
- )
- const sibling = yield* withSession({ title: "sibling" }).pipe(
- provideInstance(path.join(test.directory, "packages", "app")),
- )
- const ids = (yield* SessionNs.Service.use((session) =>
- session.list({ directory: path.join(test.directory, "packages", "kirincode") }),
- )).map((session) => session.id)
- expect(ids).toContain(current.id)
- expect(ids).not.toContain(sibling.id)
- }),
- { git: true },
- )
- it.instance(
- "matches a session regardless of directory separator on Windows",
- () =>
- Effect.gen(function* () {
- if (process.platform !== "win32") return
- const test = yield* TestInstance
- const dir = path.join(test.directory, "packages", "kirincode")
- yield* Effect.promise(() => mkdir(dir, { recursive: true }))
- const created = yield* withSession({ title: "separator" }).pipe(provideInstance(dir))
- // A forward-slash query (e.g. from the SDK/HTTP layer) must still find it —
- // this is the regression: backslash-stored vs forward-slash-queried.
- const forwardIDs = (yield* SessionNs.Service.use((session) =>
- session.list({ directory: dir.replaceAll("\\", "/") }),
- )).map((session) => session.id)
- expect(forwardIDs).toContain(created.id)
- // The native form must keep matching too.
- const nativeIDs = (yield* SessionNs.Service.use((session) => session.list({ directory: dir }))).map(
- (session) => session.id,
- )
- expect(nativeIDs).toContain(created.id)
- }),
- { git: true },
- )
- it.instance(
- "filters by path and ignores directory when path is provided",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* Effect.promise(() =>
- mkdir(path.join(test.directory, "packages", "kirincode", "src", "deep"), { recursive: true }),
- )
- yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "app"), { recursive: true }))
- const parent = yield* withSession({ title: "parent" }).pipe(
- provideInstance(path.join(test.directory, "packages", "kirincode")),
- )
- const current = yield* withSession({ title: "current" }).pipe(
- provideInstance(path.join(test.directory, "packages", "kirincode", "src")),
- )
- const deeper = yield* withSession({ title: "deeper" }).pipe(
- provideInstance(path.join(test.directory, "packages", "kirincode", "src", "deep")),
- )
- const sibling = yield* withSession({ title: "sibling" }).pipe(
- provideInstance(path.join(test.directory, "packages", "app")),
- )
- const pathIDs = (yield* SessionNs.Service.use((session) =>
- session.list({
- directory: path.join(test.directory, "packages", "app"),
- path: "packages/kirincode/src",
- }),
- )).map((session) => session.id)
- expect(pathIDs).not.toContain(parent.id)
- expect(pathIDs).toContain(current.id)
- expect(pathIDs).toContain(deeper.id)
- expect(pathIDs).not.toContain(sibling.id)
- if (process.platform === "win32") {
- const windowsPathIDs = (yield* SessionNs.Service.use((session) =>
- session.list({ path: "packages\\opencode\\src" }),
- )).map((session) => session.id)
- expect(windowsPathIDs).toContain(current.id)
- expect(windowsPathIDs).toContain(deeper.id)
- }
- }),
- { git: true },
- )
- it.instance(
- "falls back to directory when filtering legacy sessions without path",
- () =>
- Effect.gen(function* () {
- const test = yield* TestInstance
- yield* Effect.promise(() =>
- mkdir(path.join(test.directory, "packages", "kirincode", "src"), { recursive: true }),
- )
- yield* Effect.promise(() => mkdir(path.join(test.directory, "packages", "app"), { recursive: true }))
- const current = yield* withSession({ title: "legacy-current" }).pipe(
- provideInstance(path.join(test.directory, "packages", "kirincode", "src")),
- )
- const sibling = yield* withSession({ title: "legacy-sibling" }).pipe(
- provideInstance(path.join(test.directory, "packages", "app")),
- )
- const { db } = yield* Database.Service
- yield* db
- .update(SessionTable)
- .set({ path: null })
- .where(eq(SessionTable.id, current.id))
- .run()
- .pipe(Effect.orDie)
- yield* db
- .update(SessionTable)
- .set({ path: null })
- .where(eq(SessionTable.id, sibling.id))
- .run()
- .pipe(Effect.orDie)
- const pathIDs = (yield* SessionNs.Service.use((session) =>
- session.list({
- directory: path.join(test.directory, "packages", "kirincode", "src"),
- path: "packages/kirincode/src",
- }),
- )).map((session) => session.id)
- expect(pathIDs).toContain(current.id)
- expect(pathIDs).not.toContain(sibling.id)
- }),
- { git: true },
- )
- it.instance(
- "filters root sessions",
- () =>
- Effect.gen(function* () {
- const root = yield* withSession({ title: "root-session" })
- const child = yield* withSession({ title: "child-session", parentID: root.id })
- const sessions = yield* SessionNs.use.list({ roots: true })
- const ids = sessions.map((session) => session.id)
- expect(ids).toContain(root.id)
- expect(ids).not.toContain(child.id)
- }),
- { git: true },
- )
- it.instance(
- "filters by start time",
- () =>
- Effect.gen(function* () {
- yield* withSession({ title: "new-session" })
- const sessions = yield* SessionNs.Service.use((session) => session.list({ start: Date.now() + 86400000 }))
- expect(sessions.length).toBe(0)
- }),
- { git: true },
- )
- it.instance(
- "filters by search term",
- () =>
- Effect.gen(function* () {
- yield* withSession({ title: "unique-search-term-abc" })
- yield* withSession({ title: "other-session-xyz" })
- const sessions = yield* SessionNs.use.list({ search: "unique-search" })
- const titles = sessions.map((session) => session.title)
- expect(titles).toContain("unique-search-term-abc")
- expect(titles).not.toContain("other-session-xyz")
- }),
- { git: true },
- )
- it.instance(
- "respects limit parameter",
- () =>
- Effect.gen(function* () {
- yield* withSession({ title: "session-1" })
- yield* withSession({ title: "session-2" })
- yield* withSession({ title: "session-3" })
- const sessions = yield* SessionNs.use.list({ limit: 2 })
- expect(sessions.length).toBe(2)
- }),
- { git: true },
- )
- it.instance(
- "includes metadata in listed sessions",
- () =>
- Effect.gen(function* () {
- const meta = { source: "sdk", trace: { id: "abc" } }
- const created = yield* withSession({ title: "meta-session", metadata: meta })
- const listed = (yield* SessionNs.Service.use((session) => session.list({ search: "meta-session" }))).find(
- (item) => item.id === created.id,
- )
- expect(listed?.metadata).toEqual(meta)
- }),
- { git: true },
- )
- })
|