global-session-list.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import { $ } from "bun"
  2. import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
  3. import { Effect } from "effect"
  4. import path from "path"
  5. import z from "zod"
  6. import { Instance } from "../../src/project/instance"
  7. import { Project } from "../../src/project/project"
  8. import { Session as SessionNs } from "../../src/session"
  9. import { Log } from "../../src/util/log"
  10. import { resetDatabase } from "../fixture/db"
  11. import { tmpdir } from "../fixture/fixture"
  12. import { RemoteSender } from "../../src/kilo-sessions/remote-sender" // kilocode_change
  13. // kilocode_change start
  14. beforeEach(() => {
  15. spyOn(RemoteSender, "create").mockReturnValue({ handle() {}, dispose() {} })
  16. })
  17. // kilocode_change end
  18. Log.init({ print: false })
  19. // kilocode_change start
  20. afterEach(async () => {
  21. mock.restore()
  22. await resetDatabase()
  23. })
  24. // kilocode_change end
  25. function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
  26. return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
  27. }
  28. const svc = {
  29. ...SessionNs,
  30. create(input?: SessionNs.CreateInput) {
  31. return run(SessionNs.Service.use((svc) => svc.create(input)))
  32. },
  33. setArchived(input: z.output<typeof SessionNs.SetArchivedInput>) {
  34. return run(SessionNs.Service.use((svc) => svc.setArchived(input)))
  35. },
  36. }
  37. describe("session.listGlobal", () => {
  38. test("lists sessions across projects with project metadata", async () => {
  39. await using first = await tmpdir({ git: true })
  40. await using second = await tmpdir({ git: true })
  41. const firstSession = await Instance.provide({
  42. directory: first.path,
  43. fn: async () => svc.create({ title: "first-session" }),
  44. })
  45. const secondSession = await Instance.provide({
  46. directory: second.path,
  47. fn: async () => svc.create({ title: "second-session" }),
  48. })
  49. const sessions = [...svc.listGlobal({ limit: 200 })]
  50. const ids = sessions.map((session) => session.id)
  51. expect(ids).toContain(firstSession.id)
  52. expect(ids).toContain(secondSession.id)
  53. const firstProject = Project.get(firstSession.projectID)
  54. const secondProject = Project.get(secondSession.projectID)
  55. const firstItem = sessions.find((session) => session.id === firstSession.id)
  56. const secondItem = sessions.find((session) => session.id === secondSession.id)
  57. expect(firstItem?.project?.id).toBe(firstProject?.id)
  58. expect(firstItem?.project?.worktree).toBe(firstProject?.worktree)
  59. expect(secondItem?.project?.id).toBe(secondProject?.id)
  60. expect(secondItem?.project?.worktree).toBe(secondProject?.worktree)
  61. })
  62. test("excludes archived sessions by default", async () => {
  63. await using tmp = await tmpdir({ git: true })
  64. const archived = await Instance.provide({
  65. directory: tmp.path,
  66. fn: async () => svc.create({ title: "archived-session" }),
  67. })
  68. await Instance.provide({
  69. directory: tmp.path,
  70. fn: async () => svc.setArchived({ sessionID: archived.id, time: Date.now() }),
  71. })
  72. const sessions = [...svc.listGlobal({ limit: 200 })]
  73. const ids = sessions.map((session) => session.id)
  74. expect(ids).not.toContain(archived.id)
  75. const allSessions = [...svc.listGlobal({ limit: 200, archived: true })]
  76. const allIds = allSessions.map((session) => session.id)
  77. expect(allIds).toContain(archived.id)
  78. })
  79. test("supports cursor pagination", async () => {
  80. await using tmp = await tmpdir({ git: true })
  81. const first = await Instance.provide({
  82. directory: tmp.path,
  83. fn: async () => svc.create({ title: "page-one" }),
  84. })
  85. await new Promise((resolve) => setTimeout(resolve, 5))
  86. const second = await Instance.provide({
  87. directory: tmp.path,
  88. fn: async () => svc.create({ title: "page-two" }),
  89. })
  90. const page = [...svc.listGlobal({ directory: tmp.path, limit: 1 })]
  91. expect(page.length).toBe(1)
  92. expect(page[0]!.id).toBe(second.id)
  93. const next = [...svc.listGlobal({ directory: tmp.path, limit: 10, cursor: page[0]!.time.updated })]
  94. const ids = next.map((session) => session.id)
  95. expect(ids).toContain(first.id)
  96. expect(ids).not.toContain(second.id)
  97. })
  98. // kilocode_change start - project-family filter across worktrees (stale .git/kilo project ID)
  99. test("filters by project family across worktrees when project IDs drift", async () => {
  100. await using first = await tmpdir({ git: true })
  101. await using second = await tmpdir({ git: true })
  102. const worktree = path.join(first.path, "..", path.basename(first.path) + "-worktree")
  103. try {
  104. await $`git worktree add ${worktree} -b test-branch-${Date.now()}`.cwd(first.path).quiet()
  105. // Create worktree session first so it computes its own project ID via rev-list
  106. const branch = await Instance.provide({
  107. directory: worktree,
  108. fn: async () => svc.create({ title: "worktree-session" }),
  109. })
  110. // Now write a stale project ID to .git/kilo — this overrides the root's cached ID
  111. await Bun.write(path.join(first.path, ".git", "kilo"), "stale-project-id")
  112. const root = await Instance.provide({
  113. directory: first.path,
  114. fn: async () => svc.create({ title: "root-session" }),
  115. })
  116. await Bun.file(path.join(first.path, ".git", "kilo")).delete()
  117. const other = await Instance.provide({
  118. directory: second.path,
  119. fn: async () => svc.create({ title: "other-session" }),
  120. })
  121. const sessions = [...svc.listGlobal({ projectID: root.projectID, roots: true, limit: 200 })]
  122. const ids = sessions.map((session) => session.id)
  123. expect(root.projectID).not.toBe(branch.projectID)
  124. expect(ids).toContain(root.id)
  125. expect(ids).toContain(branch.id)
  126. expect(ids).not.toContain(other.id)
  127. expect(sessions.find((session) => session.id === branch.id)?.directory).toBe(worktree)
  128. } finally {
  129. await $`git worktree remove ${worktree}`.cwd(first.path).quiet().nothrow()
  130. }
  131. })
  132. // kilocode_change end
  133. })