Skip to content

Commit 3fb3d0b

Browse files
authored
Merge pull request aws#6840 from tsmithsz/feature/agentic-chat
feat(chat): Add validate method to fsRead tool
2 parents 5a51146 + ffd9d7c commit 3fb3d0b

File tree

3 files changed

+82
-17
lines changed

3 files changed

+82
-17
lines changed

packages/core/src/codewhispererChat/tools/fsRead.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,57 @@ import * as vscode from 'vscode'
66
import { getLogger } from '../../shared/logger/logger'
77
import { readDirectoryRecursively } from '../../shared/utilities/workspaceUtils'
88
import fs from '../../shared/fs/fs'
9-
import { InvokeOutput, maxToolResponseSize, OutputKind } from './toolShared'
9+
import { InvokeOutput, maxToolResponseSize, OutputKind, sanitizePath } from './toolShared'
1010

1111
export interface FsReadParams {
1212
path: string
1313
readRange?: number[]
1414
}
1515

1616
export class FsRead {
17-
private readonly fsPath: string
17+
private fsPath: string
1818
private readonly readRange?: number[]
19+
private type?: boolean // true for file, false for directory
1920
private readonly logger = getLogger('fsRead')
2021

2122
constructor(params: FsReadParams) {
2223
this.fsPath = params.path
2324
this.readRange = params.readRange
2425
}
2526

26-
public async invoke(): Promise<InvokeOutput> {
27+
public async validate(): Promise<void> {
28+
this.logger.debug(`Validating fsPath: ${this.fsPath}`)
29+
if (!this.fsPath || this.fsPath.trim().length === 0) {
30+
throw new Error('Path cannot be empty.')
31+
}
32+
33+
const sanitized = sanitizePath(this.fsPath)
34+
this.fsPath = sanitized
35+
36+
const fileUri = vscode.Uri.file(this.fsPath)
37+
let exists: boolean
2738
try {
28-
const fileUri = vscode.Uri.file(this.fsPath)
29-
let exists: boolean
30-
try {
31-
exists = await fs.exists(fileUri)
32-
if (!exists) {
33-
throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed.`)
34-
}
35-
} catch (err) {
36-
throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed. (${err})`)
39+
exists = await fs.exists(fileUri)
40+
if (!exists) {
41+
throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed.`)
3742
}
43+
} catch (err) {
44+
throw new Error(`Path: "${this.fsPath}" does not exist or cannot be accessed. (${err})`)
45+
}
3846

39-
const isFile = await fs.existsFile(fileUri)
40-
const isDirectory = await fs.existsDir(fileUri)
47+
this.type = await fs.existsFile(fileUri)
48+
this.logger.debug(`Validation succeeded for path: ${this.fsPath}`)
49+
}
50+
51+
public async invoke(): Promise<InvokeOutput> {
52+
try {
53+
const fileUri = vscode.Uri.file(this.fsPath)
4154

42-
if (isFile) {
55+
if (this.type) {
4356
const fileContents = await this.readFile(fileUri)
4457
this.logger.info(`Read file: ${this.fsPath}, size: ${fileContents.length}`)
4558
return this.handleFileRange(fileContents)
46-
} else if (isDirectory) {
59+
} else if (!this.type) {
4760
const maxDepth = this.getDirectoryDepth() ?? 0
4861
const listing = await readDirectoryRecursively(fileUri, maxDepth)
4962
return this.createOutput(listing.join('\n'))

packages/core/src/codewhispererChat/tools/toolShared.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5+
6+
import path from 'path'
7+
import fs from '../../shared/fs/fs'
8+
59
export const maxToolResponseSize = 30720 // 30KB
610

711
export enum OutputKind {
@@ -15,3 +19,16 @@ export interface InvokeOutput {
1519
content: string
1620
}
1721
}
22+
23+
export function sanitizePath(inputPath: string): string {
24+
let sanitized = inputPath.trim()
25+
26+
if (sanitized.startsWith('~')) {
27+
sanitized = path.join(fs.getUserHomeDir(), sanitized.slice(1))
28+
}
29+
30+
if (!path.isAbsolute(sanitized)) {
31+
sanitized = path.resolve(sanitized)
32+
}
33+
return sanitized
34+
}

packages/core/src/test/codewhispererChat/tools/fsRead.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@ describe('FsRead Tool', () => {
1414
testFolder = await TestFolder.create()
1515
})
1616

17+
it('throws if path is empty', async () => {
18+
const fsRead = new FsRead({ path: '' })
19+
await assert.rejects(fsRead.validate(), /Path cannot be empty/i, 'Expected an error about empty path')
20+
})
21+
1722
it('reads entire file', async () => {
1823
const fileContent = 'Line 1\nLine 2\nLine 3'
1924
const filePath = await testFolder.write('fullFile.txt', fileContent)
2025

2126
const fsRead = new FsRead({ path: filePath })
27+
await fsRead.validate()
2228
const result = await fsRead.invoke()
2329

2430
assert.strictEqual(result.output.kind, 'text', 'Output kind should be "text"')
@@ -30,6 +36,7 @@ describe('FsRead Tool', () => {
3036
const filePath = await testFolder.write('partialFile.txt', fileContent)
3137

3238
const fsRead = new FsRead({ path: filePath, readRange: [2, 4] })
39+
await fsRead.validate()
3340
const result = await fsRead.invoke()
3441

3542
assert.strictEqual(result.output.kind, 'text')
@@ -42,6 +49,7 @@ describe('FsRead Tool', () => {
4249
await testFolder.write(path.join('subfolder', 'fileB.md'), '# fileB')
4350

4451
const fsRead = new FsRead({ path: testFolder.path, readRange: [1] })
52+
await fsRead.validate()
4553
const result = await fsRead.invoke()
4654

4755
const lines = result.output.content.split('\n')
@@ -57,7 +65,7 @@ describe('FsRead Tool', () => {
5765
const fsRead = new FsRead({ path: missingPath })
5866

5967
await assert.rejects(
60-
fsRead.invoke(),
68+
fsRead.validate(),
6169
/does not exist or cannot be accessed/i,
6270
'Expected an error indicating the path does not exist'
6371
)
@@ -68,6 +76,7 @@ describe('FsRead Tool', () => {
6876
const bigFilePath = await testFolder.write('bigFile.txt', bigContent)
6977

7078
const fsRead = new FsRead({ path: bigFilePath })
79+
await fsRead.validate()
7180

7281
await assert.rejects(
7382
fsRead.invoke(),
@@ -80,8 +89,34 @@ describe('FsRead Tool', () => {
8089
const filePath = await testFolder.write('rangeTest.txt', '1\n2\n3')
8190
const fsRead = new FsRead({ path: filePath, readRange: [3, 2] })
8291

92+
await fsRead.validate()
8393
const result = await fsRead.invoke()
8494
assert.strictEqual(result.output.kind, 'text')
8595
assert.strictEqual(result.output.content, '')
8696
})
97+
98+
it('expands ~ path', async () => {
99+
const fsRead = new FsRead({ path: '~' })
100+
await fsRead.validate()
101+
const result = await fsRead.invoke()
102+
103+
assert.strictEqual(result.output.kind, 'text')
104+
assert.ok(result.output.content.length > 0)
105+
})
106+
107+
it('resolves relative path', async () => {
108+
await testFolder.mkdir('relTest')
109+
const filePath = path.join('relTest', 'relFile.txt')
110+
const content = 'Hello from a relative file!'
111+
await testFolder.write(filePath, content)
112+
113+
const relativePath = path.relative(process.cwd(), path.join(testFolder.path, filePath))
114+
115+
const fsRead = new FsRead({ path: relativePath })
116+
await fsRead.validate()
117+
const result = await fsRead.invoke()
118+
119+
assert.strictEqual(result.output.kind, 'text')
120+
assert.strictEqual(result.output.content, content)
121+
})
87122
})

0 commit comments

Comments
 (0)