which.test.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { describe, expect, test } from "bun:test"
  2. import fs from "fs/promises"
  3. import path from "path"
  4. import { which } from "../../src/util/which"
  5. import { tmpdir } from "../fixture/fixture"
  6. async function cmd(dir: string, name: string, exec = true) {
  7. const ext = process.platform === "win32" ? ".cmd" : ""
  8. const file = path.join(dir, name + ext)
  9. const body = process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n"
  10. await fs.writeFile(file, body)
  11. if (process.platform !== "win32") {
  12. await fs.chmod(file, exec ? 0o755 : 0o644)
  13. }
  14. return file
  15. }
  16. function env(PATH: string): NodeJS.ProcessEnv {
  17. return {
  18. PATH,
  19. PATHEXT: process.env["PATHEXT"],
  20. }
  21. }
  22. function envPath(Path: string): NodeJS.ProcessEnv {
  23. return {
  24. Path,
  25. PathExt: process.env["PathExt"] ?? process.env["PATHEXT"],
  26. }
  27. }
  28. function same(a: string | null, b: string) {
  29. if (process.platform === "win32") {
  30. expect(a?.toLowerCase()).toBe(b.toLowerCase())
  31. return
  32. }
  33. expect(a).toBe(b)
  34. }
  35. describe("util.which", () => {
  36. test("returns null when command is missing", () => {
  37. expect(which("opencode-missing-command-for-test")).toBeNull()
  38. })
  39. test("finds a command from PATH override", async () => {
  40. await using tmp = await tmpdir()
  41. const bin = path.join(tmp.path, "bin")
  42. await fs.mkdir(bin)
  43. const file = await cmd(bin, "tool")
  44. same(which("tool", env(bin)), file)
  45. })
  46. test("uses first PATH match", async () => {
  47. await using tmp = await tmpdir()
  48. const a = path.join(tmp.path, "a")
  49. const b = path.join(tmp.path, "b")
  50. await fs.mkdir(a)
  51. await fs.mkdir(b)
  52. const first = await cmd(a, "dupe")
  53. await cmd(b, "dupe")
  54. same(which("dupe", env([a, b].join(path.delimiter))), first)
  55. })
  56. test("returns null for non-executable file on unix", async () => {
  57. if (process.platform === "win32") return
  58. await using tmp = await tmpdir()
  59. const bin = path.join(tmp.path, "bin")
  60. await fs.mkdir(bin)
  61. await cmd(bin, "noexec", false)
  62. expect(which("noexec", env(bin))).toBeNull()
  63. })
  64. test("uses PATHEXT on windows", async () => {
  65. if (process.platform !== "win32") return
  66. await using tmp = await tmpdir()
  67. const bin = path.join(tmp.path, "bin")
  68. await fs.mkdir(bin)
  69. const file = path.join(bin, "pathext.CMD")
  70. await fs.writeFile(file, "@echo off\r\n")
  71. expect(which("pathext", { PATH: bin, PATHEXT: ".CMD" })).toBe(file)
  72. })
  73. test("uses Windows Path casing fallback", async () => {
  74. if (process.platform !== "win32") return
  75. await using tmp = await tmpdir()
  76. const bin = path.join(tmp.path, "bin")
  77. await fs.mkdir(bin)
  78. const file = await cmd(bin, "mixed")
  79. same(which("mixed", envPath(bin)), file)
  80. })
  81. })