shell.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { ShellSelect } from "@opencode-ai/core/shell/select"
  4. import { FSUtil } from "@opencode-ai/util/fs-util"
  5. import { which } from "@opencode-ai/core/util/which"
  6. const withShell = async (shell: string | undefined, fn: () => void | Promise<void>) => {
  7. const prev = process.env.SHELL
  8. if (shell === undefined) delete process.env.SHELL
  9. else process.env.SHELL = shell
  10. ShellSelect.acceptable.reset()
  11. ShellSelect.preferred.reset()
  12. try {
  13. await fn()
  14. } finally {
  15. if (prev === undefined) delete process.env.SHELL
  16. else process.env.SHELL = prev
  17. ShellSelect.acceptable.reset()
  18. ShellSelect.preferred.reset()
  19. }
  20. }
  21. describe("shell", () => {
  22. test("normalizes shell names", () => {
  23. expect(ShellSelect.name("/bin/bash")).toBe("bash")
  24. if (process.platform === "win32") {
  25. expect(ShellSelect.name("C:/tools/NU.EXE")).toBe("nu")
  26. expect(ShellSelect.name("C:/tools/PWSH.EXE")).toBe("pwsh")
  27. }
  28. })
  29. test("detects login shells", () => {
  30. expect(ShellSelect.login("/bin/bash")).toBe(true)
  31. expect(ShellSelect.login("C:/tools/pwsh.exe")).toBe(false)
  32. })
  33. test("detects posix shells", () => {
  34. expect(ShellSelect.posix("/bin/bash")).toBe(true)
  35. expect(ShellSelect.posix("/bin/fish")).toBe(false)
  36. expect(ShellSelect.posix("C:/tools/pwsh.exe")).toBe(false)
  37. })
  38. test("falls back when configured shell cannot be resolved", async () => {
  39. await withShell(undefined, async () => {
  40. const preferred = ShellSelect.preferred()
  41. const acceptable = ShellSelect.acceptable()
  42. expect(ShellSelect.preferred("opencode-missing-shell")).toBe(preferred)
  43. expect(ShellSelect.acceptable("opencode-missing-shell")).toBe(acceptable)
  44. })
  45. })
  46. test("falls back for terminal-only acceptable shells", () => {
  47. expect(ShellSelect.name(ShellSelect.acceptable("fish"))).not.toBe("fish")
  48. expect(ShellSelect.name(ShellSelect.acceptable("nu"))).not.toBe("nu")
  49. })
  50. test("builds command args per shell family", () => {
  51. expect(ShellSelect.args("/bin/sh", "echo hi")).toEqual(["-c", "echo hi"])
  52. expect(ShellSelect.args("/usr/bin/fish", "echo hi")).toEqual(["-c", "echo hi"])
  53. expect(ShellSelect.args("/bin/zsh", "echo hi")).toEqual(["-c", "echo hi"])
  54. expect(ShellSelect.args("/bin/bash", "echo hi")).toEqual(["-c", "echo hi"])
  55. expect(ShellSelect.args("pwsh", "Write-Output hi")).toEqual([
  56. "-NoLogo",
  57. "-NoProfile",
  58. "-NonInteractive",
  59. "-Command",
  60. "Write-Output hi",
  61. ])
  62. })
  63. if (process.platform === "win32") {
  64. test("rejects blacklisted shells case-insensitively", async () => {
  65. await withShell("NU.EXE", async () => {
  66. expect(ShellSelect.name(ShellSelect.acceptable())).not.toBe("nu")
  67. })
  68. })
  69. test("normalizes Git Bash shell paths from env", async () => {
  70. const shell = "/cygdrive/c/Program Files/Git/bin/bash.exe"
  71. await withShell(shell, async () => {
  72. expect(ShellSelect.preferred()).toBe(FSUtil.windowsPath(shell))
  73. })
  74. })
  75. test("resolves /usr/bin/bash from env to Git Bash", async () => {
  76. const bash = ShellSelect.gitbash()
  77. if (!bash) return
  78. await withShell("/usr/bin/bash", async () => {
  79. expect(ShellSelect.acceptable()).toBe(bash)
  80. expect(ShellSelect.preferred()).toBe(bash)
  81. })
  82. })
  83. test("resolves bare bash to Git Bash before PATH", async () => {
  84. const bash = ShellSelect.gitbash()
  85. if (!bash) return
  86. expect(ShellSelect.acceptable("bash")).toBe(bash)
  87. expect(ShellSelect.preferred("bash")).toBe(bash)
  88. await withShell("bash", async () => {
  89. expect(ShellSelect.acceptable()).toBe(bash)
  90. expect(ShellSelect.preferred()).toBe(bash)
  91. })
  92. })
  93. test("resolves bare PowerShell shells", async () => {
  94. const shell = which("pwsh") || which("powershell")
  95. if (!shell) return
  96. await withShell(path.win32.basename(shell), async () => {
  97. expect(ShellSelect.preferred()).toBe(shell)
  98. })
  99. })
  100. }
  101. })