Skip to content

Commit 148062f

Browse files
authored
feat: add proper windows support for executeBash and remove mocks in tests. (#934)
* test: avoid mocks in tests * feat: switch to posix version * test: switch back to which * test: implement defaulting strategy for windows * refactor: remove fallback on non-windows * docs: update error message to reflect command that is run
1 parent bf944e0 commit 148062f

File tree

2 files changed

+24
-68
lines changed

2 files changed

+24
-68
lines changed
Lines changed: 11 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,26 @@
1-
/*!
2-
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3-
* SPDX-License-Identifier: Apache-2.0
4-
*/
5-
61
import { strict as assert } from 'assert'
2+
import * as mockfs from 'mock-fs'
73
import * as sinon from 'sinon'
84
import { ExecuteBash } from './executeBash'
9-
import { processUtils } from '@aws/lsp-core'
105
import { Logging } from '@aws/language-server-runtimes/server-interface'
116
import { TestFeatures } from '@aws/language-server-runtimes/testing'
127

138
describe('ExecuteBash Tool', () => {
14-
let runStub: sinon.SinonStub
15-
let invokeStub: sinon.SinonStub
169
let logging: Logging
1710

1811
before(function () {
1912
logging = new TestFeatures().logging
2013
})
2114

2215
beforeEach(() => {
23-
runStub = sinon.stub(processUtils.ChildProcess.prototype, 'run')
24-
invokeStub = sinon.stub(ExecuteBash.prototype, 'invoke')
16+
mockfs.restore()
2517
})
2618

2719
afterEach(() => {
2820
sinon.restore()
2921
})
3022

3123
it('pass validation for a safe command (read-only)', async () => {
32-
runStub.resolves({
33-
exitCode: 0,
34-
stdout: '/bin/ls',
35-
stderr: '',
36-
error: undefined,
37-
signal: undefined,
38-
})
3924
const execBash = new ExecuteBash(logging)
4025
await execBash.validate(logging, 'ls')
4126
})
@@ -62,14 +47,6 @@ describe('ExecuteBash Tool', () => {
6247
})
6348

6449
it('whichCommand cannot find the first arg', async () => {
65-
runStub.resolves({
66-
exitCode: 1,
67-
stdout: '',
68-
stderr: '',
69-
error: undefined,
70-
signal: undefined,
71-
})
72-
7350
const execBash = new ExecuteBash(logging)
7451
await assert.rejects(
7552
execBash.validate(logging, 'noSuchCmd'),
@@ -78,46 +55,18 @@ describe('ExecuteBash Tool', () => {
7855
)
7956
})
8057

81-
it('whichCommand sees first arg on PATH', async () => {
82-
runStub.resolves({
83-
exitCode: 0,
84-
stdout: '/usr/bin/noSuchCmd\n',
85-
stderr: '',
86-
error: undefined,
87-
signal: undefined,
88-
})
89-
58+
it('validate and invokes the command', async () => {
9059
const execBash = new ExecuteBash(logging)
91-
await execBash.validate(logging, 'noSuchCmd')
92-
})
9360

94-
it('stub invoke() call', async () => {
95-
invokeStub.resolves({
96-
output: {
97-
kind: 'json',
98-
content: {
99-
exitStatus: '0',
100-
stdout: 'mocked stdout lines',
101-
stderr: '',
102-
},
103-
},
104-
})
105-
106-
const execBash = new ExecuteBash(logging)
107-
108-
const dummyWritable = { write: () => {} } as any
109-
const result = await execBash.invoke(dummyWritable)
61+
const writable = new WritableStream()
62+
const result = await execBash.invoke({ command: 'ls' }, writable)
11063

11164
assert.strictEqual(result.output.kind, 'json')
112-
const out = result.output.content as unknown as {
113-
exitStatus: string
114-
stdout: string
115-
stderr: string
116-
}
117-
assert.strictEqual(out.exitStatus, '0')
118-
assert.strictEqual(out.stdout, 'mocked stdout lines')
119-
assert.strictEqual(out.stderr, '')
120-
121-
assert.strictEqual(invokeStub.callCount, 1)
65+
assert.ok('exitStatus' in result.output.content && result.output.content.exitStatus === '0')
66+
assert.ok(
67+
'stdout' in result.output.content &&
68+
typeof result.output.content.stdout === 'string' &&
69+
result.output.content.stdout.length > 0
70+
)
12271
})
12372
})

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export class ExecuteBash {
220220

221221
let firstChunk = true
222222
let firstStderrChunk = true
223+
const writer = updates?.getWriter()
223224
const childProcessOptions: processUtils.ChildProcessOptions = {
224225
spawnOptions: {
225226
cwd: cwd,
@@ -228,11 +229,11 @@ export class ExecuteBash {
228229
collect: false,
229230
waitForStreams: true,
230231
onStdout: (chunk: string) => {
231-
ExecuteBash.handleChunk(firstChunk ? '```console\n' + chunk : chunk, stdoutBuffer, updates)
232+
ExecuteBash.handleChunk(firstChunk ? '```console\n' + chunk : chunk, stdoutBuffer, writer)
232233
firstChunk = false
233234
},
234235
onStderr: (chunk: string) => {
235-
ExecuteBash.handleChunk(firstStderrChunk ? '```console\n' + chunk : chunk, stderrBuffer, updates)
236+
ExecuteBash.handleChunk(firstStderrChunk ? '```console\n' + chunk : chunk, stderrBuffer, writer)
236237
firstStderrChunk = false
237238
},
238239
}
@@ -273,13 +274,15 @@ export class ExecuteBash {
273274
} catch (err: any) {
274275
this.logger.error(`Failed to execute bash command '${params.command}': ${err.message}`)
275276
reject(new Error(`Failed to execute command: ${err.message}`))
277+
} finally {
278+
await writer?.close()
279+
writer?.releaseLock()
276280
}
277281
})
278282
}
279283

280-
private static handleChunk(chunk: string, buffer: string[], updates?: WritableStream) {
284+
private static handleChunk(chunk: string, buffer: string[], writer?: WritableStreamDefaultWriter<any>) {
281285
try {
282-
const writer = updates?.getWriter()
283286
void writer?.write(chunk)
284287
const lines = chunk.split(/\r?\n/)
285288
for (const line of lines) {
@@ -302,7 +305,11 @@ export class ExecuteBash {
302305
}
303306

304307
private static async whichCommand(logger: Logging, cmd: string): Promise<string> {
305-
const cp = new processUtils.ChildProcess(logger, 'which', [cmd], {
308+
const isWindows = process.platform === 'win32'
309+
const { command, args } = isWindows
310+
? { command: 'where', args: [cmd] }
311+
: { command: 'sh', args: ['-c', `command -v ${cmd}`] }
312+
const cp = new processUtils.ChildProcess(logger, command, args, {
306313
collect: true,
307314
waitForStreams: true,
308315
})
@@ -314,7 +321,7 @@ export class ExecuteBash {
314321

315322
const output = result.stdout.trim()
316323
if (!output) {
317-
throw new Error(`Command '${cmd}' found but 'which' returned empty output.`)
324+
throw new Error(`Command '${cmd}' found but '${command} ${args.join(' ')}' returned empty output.`)
318325
}
319326
return output
320327
}

0 commit comments

Comments
 (0)