|
|
@@ -20,6 +20,9 @@ jest.mock("vscode", () => ({
|
|
|
ThemeIcon: jest.fn(),
|
|
|
}))
|
|
|
|
|
|
+const TERMINAL_OUTPUT_LIMIT = 100 * 1024
|
|
|
+const STALL_TIMEOUT = 100
|
|
|
+
|
|
|
describe("TerminalProcess", () => {
|
|
|
let terminalProcess: TerminalProcess
|
|
|
let mockTerminal: jest.Mocked<
|
|
|
@@ -34,7 +37,7 @@ describe("TerminalProcess", () => {
|
|
|
let mockStream: AsyncIterableIterator<string>
|
|
|
|
|
|
beforeEach(() => {
|
|
|
- terminalProcess = new TerminalProcess(100 * 1024)
|
|
|
+ terminalProcess = new TerminalProcess(TERMINAL_OUTPUT_LIMIT, STALL_TIMEOUT)
|
|
|
|
|
|
// Create properly typed mock terminal
|
|
|
mockTerminal = {
|
|
|
@@ -173,4 +176,156 @@ describe("TerminalProcess", () => {
|
|
|
expect(terminalProcess["isListening"]).toBe(false)
|
|
|
})
|
|
|
})
|
|
|
+
|
|
|
+ describe("stalled stream handling", () => {
|
|
|
+ it("emits stream_stalled event when no output is received within timeout", async () => {
|
|
|
+ // Create a promise that resolves when stream_stalled is emitted
|
|
|
+ const streamStalledPromise = new Promise<number>((resolve) => {
|
|
|
+ terminalProcess.once("stream_stalled", (id: number) => {
|
|
|
+ resolve(id)
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ // Create a stream that doesn't emit any data
|
|
|
+ mockStream = (async function* () {
|
|
|
+ yield "\x1b]633;C\x07" // Command start sequence
|
|
|
+ // No data is yielded after this, causing the stall
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, STALL_TIMEOUT * 2))
|
|
|
+ // This would normally be yielded, but the stall timer will fire first
|
|
|
+ yield "Output after stall"
|
|
|
+ yield "\x1b]633;D\x07" // Command end sequence
|
|
|
+ terminalProcess.emit("shell_execution_complete", mockTerminalInfo.id, { exitCode: 0 })
|
|
|
+ })()
|
|
|
+
|
|
|
+ mockExecution = {
|
|
|
+ read: jest.fn().mockReturnValue(mockStream),
|
|
|
+ }
|
|
|
+
|
|
|
+ mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
|
|
|
+
|
|
|
+ // Start the terminal process
|
|
|
+ const runPromise = terminalProcess.run(mockTerminal, "test command")
|
|
|
+ terminalProcess.emit("stream_available", mockTerminalInfo.id, mockStream)
|
|
|
+
|
|
|
+ // Wait for the stream_stalled event
|
|
|
+ const stalledId = await streamStalledPromise
|
|
|
+
|
|
|
+ // Verify the event was emitted with the correct terminal ID
|
|
|
+ expect(stalledId).toBe(mockTerminalInfo.id)
|
|
|
+
|
|
|
+ // Complete the run
|
|
|
+ await runPromise
|
|
|
+ })
|
|
|
+
|
|
|
+ it("clears stall timer when output is received", async () => {
|
|
|
+ // Spy on the emit method to check if stream_stalled is emitted
|
|
|
+ const emitSpy = jest.spyOn(terminalProcess, "emit")
|
|
|
+
|
|
|
+ // Create a stream that emits data before the stall timeout
|
|
|
+ mockStream = (async function* () {
|
|
|
+ yield "\x1b]633;C\x07" // Command start sequence
|
|
|
+ yield "Initial output\n" // This should clear the stall timer
|
|
|
+
|
|
|
+ // Wait longer than the stall timeout
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, STALL_TIMEOUT * 2))
|
|
|
+
|
|
|
+ yield "More output\n"
|
|
|
+ yield "\x1b]633;D\x07" // Command end sequence
|
|
|
+ terminalProcess.emit("shell_execution_complete", mockTerminalInfo.id, { exitCode: 0 })
|
|
|
+ })()
|
|
|
+
|
|
|
+ mockExecution = {
|
|
|
+ read: jest.fn().mockReturnValue(mockStream),
|
|
|
+ }
|
|
|
+
|
|
|
+ mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
|
|
|
+
|
|
|
+ // Start the terminal process
|
|
|
+ const runPromise = terminalProcess.run(mockTerminal, "test command")
|
|
|
+ terminalProcess.emit("stream_available", mockTerminalInfo.id, mockStream)
|
|
|
+
|
|
|
+ // Wait for the run to complete
|
|
|
+ await runPromise
|
|
|
+
|
|
|
+ // Wait a bit longer to ensure the stall timer would have fired if not cleared
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, STALL_TIMEOUT * 2))
|
|
|
+
|
|
|
+ // Verify stream_stalled was not emitted
|
|
|
+ expect(emitSpy).not.toHaveBeenCalledWith("stream_stalled", expect.anything())
|
|
|
+ })
|
|
|
+
|
|
|
+ it("returns true from flushLine when a line is emitted", async () => {
|
|
|
+ // Create a stream with output
|
|
|
+ mockStream = (async function* () {
|
|
|
+ yield "\x1b]633;C\x07" // Command start sequence
|
|
|
+ yield "Test output\n" // This should be flushed as a line
|
|
|
+ yield "\x1b]633;D\x07" // Command end sequence
|
|
|
+ terminalProcess.emit("shell_execution_complete", mockTerminalInfo.id, { exitCode: 0 })
|
|
|
+ })()
|
|
|
+
|
|
|
+ mockExecution = {
|
|
|
+ read: jest.fn().mockReturnValue(mockStream),
|
|
|
+ }
|
|
|
+
|
|
|
+ mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
|
|
|
+
|
|
|
+ // Spy on the flushLine method
|
|
|
+ const flushLineSpy = jest.spyOn(terminalProcess as any, "flushLine")
|
|
|
+
|
|
|
+ // Spy on the emit method to check if line is emitted
|
|
|
+ const emitSpy = jest.spyOn(terminalProcess, "emit")
|
|
|
+
|
|
|
+ // Start the terminal process
|
|
|
+ const runPromise = terminalProcess.run(mockTerminal, "test command")
|
|
|
+ terminalProcess.emit("stream_available", mockTerminalInfo.id, mockStream)
|
|
|
+
|
|
|
+ // Wait for the run to complete
|
|
|
+ await runPromise
|
|
|
+
|
|
|
+ // Verify flushLine was called and returned true
|
|
|
+ expect(flushLineSpy).toHaveBeenCalled()
|
|
|
+ expect(flushLineSpy.mock.results.some((result) => result.value === true)).toBe(true)
|
|
|
+
|
|
|
+ // Verify line event was emitted
|
|
|
+ expect(emitSpy).toHaveBeenCalledWith("line", expect.any(String))
|
|
|
+ })
|
|
|
+
|
|
|
+ it("returns false from flushLine when no line is emitted", async () => {
|
|
|
+ // Create a stream with no complete lines
|
|
|
+ mockStream = (async function* () {
|
|
|
+ yield "\x1b]633;C\x07" // Command start sequence
|
|
|
+ yield "Test output" // No newline, so this won't be flushed as a line yet
|
|
|
+ yield "\x1b]633;D\x07" // Command end sequence
|
|
|
+ terminalProcess.emit("shell_execution_complete", mockTerminalInfo.id, { exitCode: 0 })
|
|
|
+ })()
|
|
|
+
|
|
|
+ mockExecution = {
|
|
|
+ read: jest.fn().mockReturnValue(mockStream),
|
|
|
+ }
|
|
|
+
|
|
|
+ mockTerminal.shellIntegration.executeCommand.mockReturnValue(mockExecution)
|
|
|
+
|
|
|
+ // Create a custom implementation to test flushLine directly
|
|
|
+ const testFlushLine = async () => {
|
|
|
+ // Create a new instance with the same configuration
|
|
|
+ const testProcess = new TerminalProcess(TERMINAL_OUTPUT_LIMIT, STALL_TIMEOUT)
|
|
|
+
|
|
|
+ // Set up the output builder with content that doesn't have a newline
|
|
|
+ testProcess["outputBuilder"] = {
|
|
|
+ readLine: jest.fn().mockReturnValue(""),
|
|
|
+ append: jest.fn(),
|
|
|
+ reset: jest.fn(),
|
|
|
+ content: "Test output",
|
|
|
+ } as any
|
|
|
+
|
|
|
+ // Call flushLine directly
|
|
|
+ const result = testProcess["flushLine"]()
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ // Test flushLine directly
|
|
|
+ const flushLineResult = await testFlushLine()
|
|
|
+ expect(flushLineResult).toBe(false)
|
|
|
+ })
|
|
|
+ })
|
|
|
})
|