recall.test.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. // kilocode_change - new file
  2. import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
  3. import { $ } from "bun"
  4. import { Effect } from "effect"
  5. import path from "path"
  6. import { Instance } from "../../src/project/instance"
  7. import { Config } from "../../src/config/config"
  8. import { RecallTool } from "../../src/tool/recall"
  9. import { AppRuntime } from "../../src/effect/app-runtime"
  10. import { resetDatabase } from "../fixture/db"
  11. import { tmpdir } from "../fixture/fixture"
  12. import type { Tool } from "../../src/tool/tool"
  13. import { SessionID, MessageID } from "../../src/session/schema"
  14. import { RemoteSender } from "../../src/kilo-sessions/remote-sender"
  15. beforeEach(() => {
  16. spyOn(RemoteSender, "create").mockReturnValue({ handle() {}, dispose() {} })
  17. })
  18. const ctx: Tool.Context = {
  19. sessionID: SessionID.make("ses_test"),
  20. messageID: MessageID.make("msg_test"),
  21. callID: "call_test",
  22. agent: "code",
  23. abort: AbortSignal.any([]),
  24. messages: [],
  25. metadata: () => Effect.void,
  26. ask: () => Effect.void,
  27. }
  28. afterEach(async () => {
  29. mock.restore()
  30. await resetDatabase()
  31. })
  32. describe("tool.recall", () => {
  33. test("search is limited to the current project worktrees", async () => {
  34. await using first = await tmpdir({ git: true })
  35. await using second = await tmpdir({ git: true })
  36. const worktree = path.join(first.path, "..", path.basename(first.path) + "-worktree")
  37. try {
  38. await $`git worktree add ${worktree} -b test-branch-${Date.now()}`.cwd(first.path).quiet()
  39. await Bun.write(path.join(first.path, ".git", "opencode"), "stale-project-id")
  40. const share = Config.get
  41. Config.get = async () => ({ share: "manual" }) as Awaited<ReturnType<typeof Config.get>>
  42. try {
  43. const { Session } = await import("../../src/session/index")
  44. await Instance.provide({
  45. directory: first.path,
  46. fn: async () => Session.create({ title: "search-target root" }),
  47. })
  48. await Instance.provide({
  49. directory: worktree,
  50. fn: async () => Session.create({ title: "search-target worktree" }),
  51. })
  52. await Instance.provide({
  53. directory: second.path,
  54. fn: async () => Session.create({ title: "search-target other" }),
  55. })
  56. const result = await Instance.provide({
  57. directory: first.path,
  58. fn: async () => {
  59. const info = await AppRuntime.runPromise(RecallTool)
  60. const tool = await AppRuntime.runPromise(info.init())
  61. return AppRuntime.runPromise(tool.execute({ mode: "search", query: "search-target" }, ctx))
  62. },
  63. })
  64. expect(result.output).toContain("search-target root")
  65. expect(result.output).toContain("search-target worktree")
  66. expect(result.output).not.toContain("search-target other")
  67. } finally {
  68. Config.get = share
  69. }
  70. } finally {
  71. await $`git worktree remove ${worktree}`.cwd(first.path).quiet().nothrow()
  72. }
  73. })
  74. test("read rejects sessions from another project", async () => {
  75. await using first = await tmpdir({ git: true })
  76. await using second = await tmpdir({ git: true })
  77. const share = Config.get
  78. Config.get = async () => ({ share: "manual" }) as Awaited<ReturnType<typeof Config.get>>
  79. try {
  80. const { Session } = await import("../../src/session/index")
  81. const session = await Instance.provide({
  82. directory: second.path,
  83. fn: async () => Session.create({ title: "other-project-session" }),
  84. })
  85. const err = await Instance.provide({
  86. directory: first.path,
  87. fn: async () => {
  88. const info = await AppRuntime.runPromise(RecallTool)
  89. const tool = await AppRuntime.runPromise(info.init())
  90. return AppRuntime.runPromise(tool.execute({ mode: "read", sessionID: session.id }, ctx)).catch(
  91. (error: unknown) => error as Error,
  92. )
  93. },
  94. })
  95. expect(err).toBeInstanceOf(Error)
  96. expect((err as Error).message).toContain("belongs to a different workspace")
  97. } finally {
  98. Config.get = share
  99. }
  100. })
  101. test("read allows sessions from sibling worktrees when project IDs drift", async () => {
  102. await using first = await tmpdir({ git: true })
  103. const worktree = path.join(first.path, "..", path.basename(first.path) + "-worktree")
  104. try {
  105. await $`git worktree add ${worktree} -b test-branch-${Date.now()}`.cwd(first.path).quiet()
  106. await Bun.write(path.join(first.path, ".git", "opencode"), "stale-project-id")
  107. const share = Config.get
  108. Config.get = async () => ({ share: "manual" }) as Awaited<ReturnType<typeof Config.get>>
  109. try {
  110. const { Session } = await import("../../src/session/index")
  111. const session = await Instance.provide({
  112. directory: worktree,
  113. fn: async () => Session.create({ title: "worktree readable" }),
  114. })
  115. const result = await Instance.provide({
  116. directory: first.path,
  117. fn: async () => {
  118. const info = await AppRuntime.runPromise(RecallTool)
  119. const tool = await AppRuntime.runPromise(info.init())
  120. return AppRuntime.runPromise(tool.execute({ mode: "read", sessionID: session.id }, ctx))
  121. },
  122. })
  123. expect(result.output).toContain("# Session: worktree readable")
  124. } finally {
  125. Config.get = share
  126. }
  127. } finally {
  128. await $`git worktree remove ${worktree}`.cwd(first.path).quiet().nothrow()
  129. }
  130. })
  131. })