TerminalProcess.test.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. // npx jest src/integrations/terminal/__tests__/TerminalProcess.test.ts
  2. import * as vscode from "vscode"
  3. import { TerminalProcess, mergePromise } from "../TerminalProcess"
  4. import { Terminal } from "../Terminal"
  5. import { TerminalRegistry } from "../TerminalRegistry"
  6. // Mock vscode.window.createTerminal
  7. const mockCreateTerminal = jest.fn()
  8. jest.mock("vscode", () => ({
  9. workspace: {
  10. getConfiguration: jest.fn().mockReturnValue({
  11. get: jest.fn().mockReturnValue(null),
  12. }),
  13. },
  14. window: {
  15. createTerminal: (...args: any[]) => {
  16. mockCreateTerminal(...args)
  17. return {
  18. exitStatus: undefined,
  19. }
  20. },
  21. },
  22. ThemeIcon: jest.fn(),
  23. }))
  24. describe("TerminalProcess", () => {
  25. let terminalProcess: TerminalProcess
  26. let mockTerminal: jest.Mocked<
  27. vscode.Terminal & {
  28. shellIntegration: {
  29. executeCommand: jest.Mock
  30. }
  31. }
  32. >
  33. let mockTerminalInfo: Terminal
  34. let mockExecution: any
  35. let mockStream: AsyncIterableIterator<string>
  36. beforeEach(() => {
  37. // Create properly typed mock terminal
  38. mockTerminal = {
  39. shellIntegration: {
  40. executeCommand: jest.fn(),
  41. },
  42. name: "Roo Code",
  43. processId: Promise.resolve(123),
  44. creationOptions: {},
  45. exitStatus: undefined,
  46. state: { isInteractedWith: true },
  47. dispose: jest.fn(),
  48. hide: jest.fn(),
  49. show: jest.fn(),
  50. sendText: jest.fn(),
  51. } as unknown as jest.Mocked<
  52. vscode.Terminal & {
  53. shellIntegration: {
  54. executeCommand: jest.Mock
  55. }
  56. }
  57. >
  58. mockTerminalInfo = new Terminal(1, mockTerminal, "./")
  59. // Create a process for testing
  60. terminalProcess = new TerminalProcess(mockTerminalInfo)
  61. TerminalRegistry["terminals"].push(mockTerminalInfo)
  62. // Reset event listeners
  63. terminalProcess.removeAllListeners()
  64. })
  65. describe("run", () => {
  66. it("handles shell integration commands correctly", async () => {
  67. let lines: string[] = []
  68. terminalProcess.on("completed", (output) => {
  69. if (output) {
  70. lines = output.split("\n")
  71. }
  72. })
  73. // Mock stream data with shell integration sequences.
  74. mockStream = (async function* () {
  75. yield "\x1b]633;C\x07" // The first chunk contains the command start sequence with bell character.
  76. yield "Initial output\n"
  77. yield "More output\n"
  78. yield "Final output"
  79. yield "\x1b]633;D\x07" // The last chunk contains the command end sequence with bell character.
  80. terminalProcess.emit("shell_execution_complete", { exitCode: 0 })
  81. })()
  82. mockExecution = {
  83. read: jest.fn().mockReturnValue(mockStream),
  84. }
  85. mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
  86. const runPromise = terminalProcess.run("test command")
  87. terminalProcess.emit("stream_available", mockStream)
  88. await runPromise
  89. expect(lines).toEqual(["Initial output", "More output", "Final output"])
  90. expect(terminalProcess.isHot).toBe(false)
  91. })
  92. it("handles terminals without shell integration", async () => {
  93. // Create a terminal without shell integration
  94. const noShellTerminal = {
  95. sendText: jest.fn(),
  96. shellIntegration: undefined,
  97. name: "No Shell Terminal",
  98. processId: Promise.resolve(456),
  99. creationOptions: {},
  100. exitStatus: undefined,
  101. state: { isInteractedWith: true },
  102. dispose: jest.fn(),
  103. hide: jest.fn(),
  104. show: jest.fn(),
  105. } as unknown as vscode.Terminal
  106. // Create new terminal info with the no-shell terminal
  107. const noShellTerminalInfo = new Terminal(2, noShellTerminal, "./")
  108. // Create new process with the no-shell terminal
  109. const noShellProcess = new TerminalProcess(noShellTerminalInfo)
  110. // Set up event listeners to verify events are emitted
  111. const eventPromises = Promise.all([
  112. new Promise<void>((resolve) =>
  113. noShellProcess.once("no_shell_integration", (_message: string) => resolve()),
  114. ),
  115. new Promise<void>((resolve) => noShellProcess.once("completed", (_output?: string) => resolve())),
  116. new Promise<void>((resolve) => noShellProcess.once("continue", resolve)),
  117. ])
  118. // Run command and wait for all events
  119. await noShellProcess.run("test command")
  120. await eventPromises
  121. // Verify sendText was called with the command
  122. expect(noShellTerminal.sendText).toHaveBeenCalledWith("test command", true)
  123. })
  124. it("sets hot state for compiling commands", async () => {
  125. let lines: string[] = []
  126. terminalProcess.on("completed", (output) => {
  127. if (output) {
  128. lines = output.split("\n")
  129. }
  130. })
  131. const completePromise = new Promise<void>((resolve) => {
  132. terminalProcess.on("shell_execution_complete", () => resolve())
  133. })
  134. mockStream = (async function* () {
  135. yield "\x1b]633;C\x07" // The first chunk contains the command start sequence with bell character.
  136. yield "compiling...\n"
  137. yield "still compiling...\n"
  138. yield "done"
  139. yield "\x1b]633;D\x07" // The last chunk contains the command end sequence with bell character.
  140. terminalProcess.emit("shell_execution_complete", { exitCode: 0 })
  141. })()
  142. mockTerminal.shellIntegration.executeCommand.mockReturnValue({
  143. read: jest.fn().mockReturnValue(mockStream),
  144. })
  145. const runPromise = terminalProcess.run("npm run build")
  146. terminalProcess.emit("stream_available", mockStream)
  147. expect(terminalProcess.isHot).toBe(true)
  148. await runPromise
  149. expect(lines).toEqual(["compiling...", "still compiling...", "done"])
  150. await completePromise
  151. expect(terminalProcess.isHot).toBe(false)
  152. })
  153. })
  154. describe("continue", () => {
  155. it("stops listening and emits continue event", () => {
  156. const continueSpy = jest.fn()
  157. terminalProcess.on("continue", continueSpy)
  158. terminalProcess.continue()
  159. expect(continueSpy).toHaveBeenCalled()
  160. expect(terminalProcess["isListening"]).toBe(false)
  161. })
  162. })
  163. describe("getUnretrievedOutput", () => {
  164. it("returns and clears unretrieved output", () => {
  165. terminalProcess["fullOutput"] = `\x1b]633;C\x07previous\nnew output\x1b]633;D\x07`
  166. terminalProcess["lastRetrievedIndex"] = 17 // After "previous\n"
  167. const unretrieved = terminalProcess.getUnretrievedOutput()
  168. expect(unretrieved).toBe("new output")
  169. expect(terminalProcess["lastRetrievedIndex"]).toBe(terminalProcess["fullOutput"].length - "previous".length)
  170. })
  171. })
  172. describe("interpretExitCode", () => {
  173. it("handles undefined exit code", () => {
  174. const result = TerminalProcess.interpretExitCode(undefined)
  175. expect(result).toEqual({ exitCode: undefined })
  176. })
  177. it("handles normal exit codes (0-128)", () => {
  178. const result = TerminalProcess.interpretExitCode(0)
  179. expect(result).toEqual({ exitCode: 0 })
  180. const result2 = TerminalProcess.interpretExitCode(1)
  181. expect(result2).toEqual({ exitCode: 1 })
  182. const result3 = TerminalProcess.interpretExitCode(128)
  183. expect(result3).toEqual({ exitCode: 128 })
  184. })
  185. it("interprets signal exit codes (>128)", () => {
  186. // SIGTERM (15) -> 128 + 15 = 143
  187. const result = TerminalProcess.interpretExitCode(143)
  188. expect(result).toEqual({
  189. exitCode: 143,
  190. signal: 15,
  191. signalName: "SIGTERM",
  192. coreDumpPossible: false,
  193. })
  194. // SIGSEGV (11) -> 128 + 11 = 139
  195. const result2 = TerminalProcess.interpretExitCode(139)
  196. expect(result2).toEqual({
  197. exitCode: 139,
  198. signal: 11,
  199. signalName: "SIGSEGV",
  200. coreDumpPossible: true,
  201. })
  202. })
  203. it("handles unknown signals", () => {
  204. const result = TerminalProcess.interpretExitCode(255)
  205. expect(result).toEqual({
  206. exitCode: 255,
  207. signal: 127,
  208. signalName: "Unknown Signal (127)",
  209. coreDumpPossible: false,
  210. })
  211. })
  212. })
  213. describe("mergePromise", () => {
  214. it("merges promise methods with terminal process", async () => {
  215. const process = new TerminalProcess(mockTerminalInfo)
  216. const promise = Promise.resolve()
  217. const merged = mergePromise(process, promise)
  218. expect(merged).toHaveProperty("then")
  219. expect(merged).toHaveProperty("catch")
  220. expect(merged).toHaveProperty("finally")
  221. expect(merged instanceof TerminalProcess).toBe(true)
  222. await expect(merged).resolves.toBeUndefined()
  223. })
  224. })
  225. })