From 03bd5e5cb7d5b0eb2b410182703a947ac0c5652e Mon Sep 17 00:00:00 2001 From: koolcandy <133551107+koolcandy@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:33:56 +0800 Subject: [PATCH] fix(terminal): wait for shell integration before running commands --- src/features/terminal/runInTerminal.ts | 4 + .../terminal/runInTerminal.unit.test.ts | 76 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/test/features/terminal/runInTerminal.unit.test.ts diff --git a/src/features/terminal/runInTerminal.ts b/src/features/terminal/runInTerminal.ts index b0cc9a59e..66171ffd8 100644 --- a/src/features/terminal/runInTerminal.ts +++ b/src/features/terminal/runInTerminal.ts @@ -7,6 +7,7 @@ import { identifyTerminalShell } from '../common/shellDetector'; import { quoteArgs } from '../execution/execUtils'; import { normalizeShellPath } from './shells/common/shellUtils'; import { traceLog } from '../../common/logging'; +import { waitForShellIntegration } from './utils'; export async function runInTerminal( environment: PythonEnvironment, @@ -26,6 +27,9 @@ export async function runInTerminal( if (shellType === ShellConstants.GITBASH) { executable = normalizeShellPath(executable, shellType); } + if (!terminal.shellIntegration) { + await waitForShellIntegration(terminal); + } if (terminal.shellIntegration) { let execution: TerminalShellExecution | undefined; const deferred = createDeferred(); diff --git a/src/test/features/terminal/runInTerminal.unit.test.ts b/src/test/features/terminal/runInTerminal.unit.test.ts new file mode 100644 index 000000000..6e3f639ad --- /dev/null +++ b/src/test/features/terminal/runInTerminal.unit.test.ts @@ -0,0 +1,76 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { Disposable, Terminal, TerminalShellExecution, TerminalShellExecutionEndEvent } from 'vscode'; +import * as windowApis from '../../../common/window.apis'; +import * as shellDetector from '../../../features/common/shellDetector'; +import { runInTerminal } from '../../../features/terminal/runInTerminal'; +import * as terminalUtils from '../../../features/terminal/utils'; +import { createMockPythonEnvironment } from '../../mocks/pythonEnvironment'; + +suite('runInTerminal', () => { + teardown(() => { + sinon.restore(); + }); + + test('waits for shell integration before executing a command', async () => { + sinon.stub(shellDetector, 'identifyTerminalShell').returns('fish'); + const waitForShellIntegrationStub = sinon.stub(terminalUtils, 'waitForShellIntegration'); + let resolveWaitForShellIntegration!: (result: boolean) => void; + const waitForShellIntegrationPromise = new Promise((resolve) => { + resolveWaitForShellIntegration = resolve; + }); + + const execution = {} as TerminalShellExecution; + const shellIntegration = { + executeCommand: sinon.stub().returns(execution), + }; + const terminal = { + name: 'Python', + shellIntegration: undefined, + sendText: sinon.stub(), + } as unknown as Terminal; + waitForShellIntegrationStub.returns(waitForShellIntegrationPromise); + + let endListener: ((event: TerminalShellExecutionEndEvent) => void) | undefined; + sinon.stub(windowApis, 'onDidEndTerminalShellExecution').callsFake((listener) => { + endListener = listener; + return new Disposable(() => undefined); + }); + + const environment = createMockPythonEnvironment({ envPath: '/env/bin/python' }); + const runPromise = runInTerminal(environment, terminal, { cwd: '/workspace', args: ['main.py'] }); + await new Promise((resolve) => setImmediate(resolve)); + + sinon.assert.calledOnce(waitForShellIntegrationStub); + sinon.assert.notCalled(terminal.sendText as sinon.SinonStub); + sinon.assert.notCalled(shellIntegration.executeCommand); + + (terminal as { shellIntegration?: typeof shellIntegration }).shellIntegration = shellIntegration; + resolveWaitForShellIntegration(true); + await new Promise((resolve) => setImmediate(resolve)); + + sinon.assert.notCalled(terminal.sendText as sinon.SinonStub); + sinon.assert.calledOnce(shellIntegration.executeCommand); + assert.ok(endListener, 'shell execution end listener should be registered'); + + endListener!({ terminal, execution } as unknown as TerminalShellExecutionEndEvent); + await runPromise; + }); + + test('uses sendText when shell integration is unavailable after waiting', async () => { + sinon.stub(shellDetector, 'identifyTerminalShell').returns('fish'); + const waitForShellIntegrationStub = sinon.stub(terminalUtils, 'waitForShellIntegration').resolves(false); + const sendText = sinon.stub(); + const terminal = { + name: 'Python', + shellIntegration: undefined, + sendText, + } as unknown as Terminal; + + const environment = createMockPythonEnvironment({ envPath: '/env/bin/python' }); + await runInTerminal(environment, terminal, { cwd: '/workspace', args: ['main.py'] }); + + sinon.assert.calledOnceWithExactly(waitForShellIntegrationStub, terminal); + sinon.assert.calledOnceWithExactly(sendText, 'python main.py\n'); + }); +});