glob.test.ts 4.4 KB

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