external-directory.test.ts 4.9 KB

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