glob.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import { PermissionV1 } from "@kirincode-ai/core/v1/permission"
  2. import { describe, expect } from "bun:test"
  3. import path from "path"
  4. import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
  5. import { Cause, Effect, Exit, Layer } from "effect"
  6. import { GlobTool } from "../../src/tool/glob"
  7. import { SessionID, MessageID } from "../../src/session/schema"
  8. import { CrossSpawnSpawner } from "@kirincode-ai/core/cross-spawn-spawner"
  9. import { Ripgrep } from "@kirincode-ai/core/ripgrep"
  10. import { FSUtil } from "@kirincode-ai/core/fs-util"
  11. import { Global } from "@kirincode-ai/core/global"
  12. import { Truncate } from "@/tool/truncate"
  13. import { Agent } from "../../src/agent/agent"
  14. import { TestInstance, tmpdirScoped } from "../fixture/fixture"
  15. import { testEffect } from "../lib/effect"
  16. import { Config } from "@/config/config"
  17. import { RuntimeFlags } from "@/effect/runtime-flags"
  18. import { Git } from "@/git"
  19. import { Filesystem } from "@/util/filesystem"
  20. import { Permission } from "../../src/permission"
  21. import type * as Tool from "../../src/tool/tool"
  22. const toolLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
  23. LayerNode.compile(
  24. LayerNode.group([CrossSpawnSpawner.node, FSUtil.node, Ripgrep.node, Truncate.node, Agent.node, Git.node]),
  25. )
  26. const it = testEffect(toolLayer())
  27. const full = (p: string) => (process.platform === "win32" ? Filesystem.normalizePath(p) : p)
  28. const ctx = {
  29. sessionID: SessionID.make("ses_test"),
  30. messageID: MessageID.make("msg_test"),
  31. callID: "",
  32. agent: "build",
  33. abort: AbortSignal.any([]),
  34. messages: [],
  35. metadata: () => Effect.void,
  36. ask: () => Effect.void,
  37. }
  38. const asks = () => {
  39. const items: Array<Omit<PermissionV1.Request, "id" | "sessionID" | "tool">> = []
  40. return {
  41. items,
  42. next: {
  43. ...ctx,
  44. ask: (req: Omit<PermissionV1.Request, "id" | "sessionID" | "tool">) =>
  45. Effect.sync(() => {
  46. items.push(req)
  47. }),
  48. } satisfies Tool.Context,
  49. }
  50. }
  51. const githubBase = <A, E, R>(url: string, self: Effect.Effect<A, E, R>) =>
  52. Effect.acquireUseRelease(
  53. Effect.sync(() => {
  54. const previous = process.env.KIRINCODE_REPO_CLONE_GITHUB_BASE_URL
  55. process.env.KIRINCODE_REPO_CLONE_GITHUB_BASE_URL = url
  56. return previous
  57. }),
  58. () => self,
  59. (previous) =>
  60. Effect.sync(() => {
  61. if (previous) process.env.KIRINCODE_REPO_CLONE_GITHUB_BASE_URL = previous
  62. else delete process.env.KIRINCODE_REPO_CLONE_GITHUB_BASE_URL
  63. }),
  64. )
  65. const git = Effect.fn("GlobToolTest.git")(function* (cwd: string, args: string[]) {
  66. return yield* Effect.promise(async () => {
  67. const proc = Bun.spawn(["git", ...args], {
  68. cwd,
  69. stdout: "pipe",
  70. stderr: "pipe",
  71. })
  72. const [stdout, stderr, code] = await Promise.all([
  73. new Response(proc.stdout).text(),
  74. new Response(proc.stderr).text(),
  75. proc.exited,
  76. ])
  77. if (code !== 0) throw new Error(stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`)
  78. return stdout.trim()
  79. })
  80. })
  81. describe("tool.glob", () => {
  82. it.instance("matches files from a directory path", () =>
  83. Effect.gen(function* () {
  84. const test = yield* TestInstance
  85. yield* Effect.promise(() => Bun.write(path.join(test.directory, "a.ts"), "export const a = 1\n"))
  86. yield* Effect.promise(() => Bun.write(path.join(test.directory, "b.txt"), "hello\n"))
  87. const info = yield* GlobTool
  88. const glob = yield* info.init()
  89. const result = yield* glob.execute(
  90. {
  91. pattern: "*.ts",
  92. path: test.directory,
  93. },
  94. ctx,
  95. )
  96. expect(result.metadata.count).toBe(1)
  97. expect(result.output).toContain(path.join(test.directory, "a.ts"))
  98. expect(result.output).not.toContain(path.join(test.directory, "b.txt"))
  99. }),
  100. )
  101. it.instance("rejects exact file paths", () =>
  102. Effect.gen(function* () {
  103. const test = yield* TestInstance
  104. const file = path.join(test.directory, "a.ts")
  105. yield* Effect.promise(() => Bun.write(file, "export const a = 1\n"))
  106. const info = yield* GlobTool
  107. const glob = yield* info.init()
  108. const exit = yield* glob
  109. .execute(
  110. {
  111. pattern: "*.ts",
  112. path: file,
  113. },
  114. ctx,
  115. )
  116. .pipe(Effect.exit)
  117. expect(Exit.isFailure(exit)).toBe(true)
  118. if (Exit.isFailure(exit)) {
  119. const err = Cause.squash(exit.cause)
  120. expect(err instanceof Error ? err.message : String(err)).toContain("glob path must be a directory")
  121. }
  122. }),
  123. )
  124. })