2
0

external-directory.test.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { Effect } from "effect"
  4. import type { Tool } from "../../src/tool/tool"
  5. import { Instance } from "../../src/project/instance"
  6. import { assertExternalDirectory } from "../../src/tool/external-directory"
  7. import { Filesystem } from "../../src/util/filesystem"
  8. import { tmpdir } from "../fixture/fixture"
  9. import type { Permission } from "../../src/permission"
  10. import { SessionID, MessageID } from "../../src/session/schema"
  11. const baseCtx: Omit<Tool.Context, "ask"> = {
  12. sessionID: SessionID.make("ses_test"),
  13. messageID: MessageID.make(""),
  14. callID: "",
  15. agent: "code", // kilocode_change
  16. abort: AbortSignal.any([]),
  17. messages: [],
  18. metadata: () => Effect.void,
  19. }
  20. const glob = (p: string) =>
  21. process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/")
  22. function makeCtx() {
  23. const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
  24. const ctx: Tool.Context = {
  25. ...baseCtx,
  26. ask: (req) =>
  27. Effect.sync(() => {
  28. requests.push(req)
  29. }),
  30. }
  31. return { requests, ctx }
  32. }
  33. describe("tool.assertExternalDirectory", () => {
  34. test("no-ops for empty target", async () => {
  35. const { requests, ctx } = makeCtx()
  36. await Instance.provide({
  37. directory: "/tmp",
  38. fn: async () => {
  39. await assertExternalDirectory(ctx)
  40. },
  41. })
  42. expect(requests.length).toBe(0)
  43. })
  44. test("no-ops for paths inside Instance.directory", async () => {
  45. const { requests, ctx } = makeCtx()
  46. await Instance.provide({
  47. directory: "/tmp/project",
  48. fn: async () => {
  49. await assertExternalDirectory(ctx, path.join("/tmp/project", "file.txt"))
  50. },
  51. })
  52. expect(requests.length).toBe(0)
  53. })
  54. test("asks with a single canonical glob", async () => {
  55. const { requests, ctx } = makeCtx()
  56. const directory = "/tmp/project"
  57. const target = "/tmp/outside/file.txt"
  58. const expected = glob(path.join(path.dirname(target), "*"))
  59. await Instance.provide({
  60. directory,
  61. fn: async () => {
  62. await assertExternalDirectory(ctx, target)
  63. },
  64. })
  65. const req = requests.find((r) => r.permission === "external_directory")
  66. expect(req).toBeDefined()
  67. expect(req!.patterns).toEqual([expected])
  68. expect(req!.always).toEqual([expected])
  69. })
  70. test("uses target directory when kind=directory", async () => {
  71. const { requests, ctx } = makeCtx()
  72. const directory = "/tmp/project"
  73. const target = "/tmp/outside"
  74. const expected = glob(path.join(target, "*"))
  75. await Instance.provide({
  76. directory,
  77. fn: async () => {
  78. await assertExternalDirectory(ctx, target, { kind: "directory" })
  79. },
  80. })
  81. const req = requests.find((r) => r.permission === "external_directory")
  82. expect(req).toBeDefined()
  83. expect(req!.patterns).toEqual([expected])
  84. expect(req!.always).toEqual([expected])
  85. })
  86. test("skips prompting when bypass=true", async () => {
  87. const { requests, ctx } = makeCtx()
  88. await Instance.provide({
  89. directory: "/tmp/project",
  90. fn: async () => {
  91. await assertExternalDirectory(ctx, "/tmp/outside/file.txt", { bypass: true })
  92. },
  93. })
  94. expect(requests.length).toBe(0)
  95. })
  96. if (process.platform === "win32") {
  97. test("normalizes Windows path variants to one glob", async () => {
  98. const { requests, ctx } = makeCtx()
  99. await using outerTmp = await tmpdir({
  100. init: async (dir) => {
  101. await Bun.write(path.join(dir, "outside.txt"), "x")
  102. },
  103. })
  104. await using tmp = await tmpdir({ git: true })
  105. const target = path.join(outerTmp.path, "outside.txt")
  106. const alt = target
  107. .replace(/^[A-Za-z]:/, "")
  108. .replaceAll("\\", "/")
  109. .toLowerCase()
  110. await Instance.provide({
  111. directory: tmp.path,
  112. fn: async () => {
  113. await assertExternalDirectory(ctx, alt)
  114. },
  115. })
  116. const req = requests.find((r) => r.permission === "external_directory")
  117. const expected = glob(path.join(outerTmp.path, "*"))
  118. expect(req).toBeDefined()
  119. expect(req!.patterns).toEqual([expected])
  120. expect(req!.always).toEqual([expected])
  121. })
  122. test("uses drive root glob for root files", async () => {
  123. const { requests, ctx } = makeCtx()
  124. await using tmp = await tmpdir({ git: true })
  125. const root = path.parse(tmp.path).root
  126. const target = path.join(root, "boot.ini")
  127. await Instance.provide({
  128. directory: tmp.path,
  129. fn: async () => {
  130. await assertExternalDirectory(ctx, target)
  131. },
  132. })
  133. const req = requests.find((r) => r.permission === "external_directory")
  134. const expected = path.join(root, "*")
  135. expect(req).toBeDefined()
  136. expect(req!.patterns).toEqual([expected])
  137. expect(req!.always).toEqual([expected])
  138. })
  139. }
  140. })