experimental-session-list.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. // kilocode_change - new file
  2. import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
  3. import { $ } from "bun"
  4. import path from "path"
  5. import { Config } from "../../src/config/config"
  6. import { Instance } from "../../src/project/instance"
  7. import { Log } from "../../src/util/log"
  8. import { resetDatabase } from "../fixture/db"
  9. import { tmpdir } from "../fixture/fixture"
  10. import { RemoteSender } from "../../src/kilo-sessions/remote-sender"
  11. beforeEach(() => {
  12. spyOn(RemoteSender, "create").mockReturnValue({ handle() {}, dispose() {} })
  13. })
  14. Log.init({ print: false })
  15. afterEach(async () => {
  16. mock.restore()
  17. await resetDatabase()
  18. })
  19. describe("experimental.session.list", () => {
  20. test("filters sessions by repo worktree family even when project IDs drift", async () => {
  21. await using first = await tmpdir({ git: true })
  22. await using second = await tmpdir({ git: true })
  23. const worktree = path.join(first.path, "..", path.basename(first.path) + "-worktree")
  24. try {
  25. await $`git worktree add ${worktree} -b test-branch-${Date.now()}`.cwd(first.path).quiet()
  26. const share = Config.get
  27. Config.get = async () => ({ share: "manual" }) as Awaited<ReturnType<typeof Config.get>>
  28. try {
  29. const { Server } = await import("../../src/server/server")
  30. const { Session } = await import("../../src/session/index")
  31. // Create worktree session first so it computes its own project ID via rev-list
  32. const branch = await Instance.provide({
  33. directory: worktree,
  34. fn: async () => Session.create({ title: "worktree-session" }),
  35. })
  36. // Now write a stale project ID to .git/kilo — this overrides the root's cached ID
  37. await Bun.write(path.join(first.path, ".git", "kilo"), "stale-project-id")
  38. const root = await Instance.provide({
  39. directory: first.path,
  40. fn: async () => ({
  41. app: Server.Default().app,
  42. project: await Server.Default().app.request("/project/current", {
  43. headers: { "x-kilo-directory": first.path },
  44. }),
  45. session: await Session.create({ title: "root-session" }),
  46. }),
  47. })
  48. await Bun.file(path.join(first.path, ".git", "kilo")).delete()
  49. await Instance.provide({
  50. directory: second.path,
  51. fn: async () => Session.create({ title: "other-project-session" }),
  52. })
  53. const app = root.app
  54. const project = await root.project.json()
  55. const response = await app.request(
  56. `/experimental/session?projectID=${encodeURIComponent(project.id)}&roots=true&worktrees=true`,
  57. {
  58. headers: { "x-kilo-directory": first.path },
  59. },
  60. )
  61. expect(response.status).toBe(200)
  62. const body = await response.json()
  63. const ids = body.map((item: { id: string }) => item.id)
  64. const dirs = body.map((item: { directory: string }) => item.directory)
  65. expect(root.session.projectID).not.toBe(branch.projectID)
  66. expect(project.id).toBe(root.session.projectID)
  67. expect(ids).toContain(root.session.id)
  68. expect(ids).toContain(branch.id)
  69. expect(dirs).toContain(worktree)
  70. expect(body.some((item: { title: string }) => item.title === "other-project-session")).toBe(false)
  71. } finally {
  72. Config.get = share
  73. }
  74. } finally {
  75. await $`git worktree remove ${worktree}`.cwd(first.path).quiet().nothrow()
  76. }
  77. })
  78. test("worktrees=true ignores SDK-injected directory query param", async () => {
  79. await using first = await tmpdir({ git: true })
  80. await using second = await tmpdir({ git: true })
  81. const worktree = path.join(first.path, "..", path.basename(first.path) + "-worktree")
  82. try {
  83. await $`git worktree add ${worktree} -b test-branch-sdk-${Date.now()}`.cwd(first.path).quiet()
  84. const share = Config.get
  85. Config.get = async () => ({ share: "manual" }) as Awaited<ReturnType<typeof Config.get>>
  86. try {
  87. const { Server } = await import("../../src/server/server")
  88. const { Session } = await import("../../src/session/index")
  89. const branch = await Instance.provide({
  90. directory: worktree,
  91. fn: async () => Session.create({ title: "worktree-session" }),
  92. })
  93. const root = await Instance.provide({
  94. directory: first.path,
  95. fn: async () => ({
  96. app: Server.Default().app,
  97. project: await Server.Default().app.request("/project/current", {
  98. headers: { "x-kilo-directory": first.path },
  99. }),
  100. session: await Session.create({ title: "root-session" }),
  101. }),
  102. })
  103. await Instance.provide({
  104. directory: second.path,
  105. fn: async () => Session.create({ title: "other-project-session" }),
  106. })
  107. const app = root.app
  108. const project = await root.project.json()
  109. // Include directory in query params — mimics what the SDK rewrite interceptor does.
  110. // Without the server fix, this would restrict results to only first.path sessions.
  111. const response = await app.request(
  112. `/experimental/session?projectID=${encodeURIComponent(project.id)}&roots=true&worktrees=true&directory=${encodeURIComponent(first.path)}`,
  113. {
  114. headers: { "x-kilo-directory": first.path },
  115. },
  116. )
  117. expect(response.status).toBe(200)
  118. const body = await response.json()
  119. const ids = body.map((item: { id: string }) => item.id)
  120. // Both root and worktree sessions must be returned despite directory= in query
  121. expect(ids).toContain(root.session.id)
  122. expect(ids).toContain(branch.id)
  123. expect(body.some((item: { title: string }) => item.title === "other-project-session")).toBe(false)
  124. } finally {
  125. Config.get = share
  126. }
  127. } finally {
  128. await $`git worktree remove ${worktree}`.cwd(first.path).quiet().nothrow()
  129. }
  130. })
  131. })