Skip to content

Commit 2ace4bc

Browse files
committed
Explain why read_files blocks gitignored files
1 parent f5adf61 commit 2ace4bc

3 files changed

Lines changed: 85 additions & 9 deletions

File tree

sdk/src/__tests__/read-files.test.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,61 @@ describe('getFiles', () => {
393393
fs: mockFs,
394394
})
395395

396-
expect(result['node_modules/package/index.js']).toBe(
397-
FILE_READ_STATUS.IGNORED,
396+
expect(
397+
result['node_modules/package/index.js']!.startsWith(
398+
FILE_READ_STATUS.IGNORED,
399+
),
400+
).toBe(true)
401+
expect(result['node_modules/package/index.js']).toContain(
402+
'excluded by ignore rules',
398403
)
399404
})
400405

406+
test('ignore-rule block explains reason and unblock path; env-policy block stays opaque', async () => {
407+
isFileIgnoredSpy.mockResolvedValue(true)
408+
409+
const mockFs = createMockFs({
410+
files: {
411+
'/project/AGENTS.local.md': { content: 'private notes' },
412+
},
413+
})
414+
415+
const result = await getFiles({
416+
filePaths: ['AGENTS.local.md'],
417+
cwd: '/project',
418+
fs: mockFs,
419+
})
420+
421+
expect(
422+
result['AGENTS.local.md']!.startsWith(FILE_READ_STATUS.IGNORED),
423+
).toBe(true)
424+
expect(result['AGENTS.local.md']).toContain('excluded by ignore rules')
425+
expect(result['AGENTS.local.md']).toContain('cannot re-include')
426+
427+
// Ignored-and-deleted: the block must not assert the file exists.
428+
const goneFs = createMockFs({ files: {} })
429+
const goneResult = await getFiles({
430+
filePaths: ['AGENTS.local.md'],
431+
cwd: '/project',
432+
fs: goneFs,
433+
})
434+
expect(
435+
goneResult['AGENTS.local.md']!.startsWith(FILE_READ_STATUS.IGNORED),
436+
).toBe(true)
437+
expect(goneResult['AGENTS.local.md']).not.toContain('exists on disk')
438+
439+
// The env-policy block must NOT leak a reason or an unblock hint.
440+
const envFs = createMockFs({
441+
files: { '/project/.env': { content: 'SECRET=value' } },
442+
})
443+
const envResult = await getFiles({
444+
filePaths: ['.env'],
445+
cwd: '/project',
446+
fs: envFs,
447+
})
448+
expect(envResult['.env']).toBe(FILE_READ_STATUS.IGNORED)
449+
})
450+
401451
test('should call isFileIgnored with correct parameters', async () => {
402452
const mockFs = createMockFs({
403453
files: {
@@ -436,7 +486,11 @@ describe('getFiles', () => {
436486
})
437487

438488
expect(result['src/index.ts']).toBe('main code')
439-
expect(result['node_modules/pkg/index.js']).toBe(FILE_READ_STATUS.IGNORED)
489+
expect(
490+
result['node_modules/pkg/index.js']!.startsWith(
491+
FILE_READ_STATUS.IGNORED,
492+
),
493+
).toBe(true)
440494
})
441495
})
442496

@@ -454,10 +508,13 @@ describe('getFiles', () => {
454508
filePaths: ['node_modules/pkg/index.js'],
455509
cwd: '/project',
456510
fs: mockFs,
457-
// No fileFilter provided - SDK applies default gitignore checking
458511
})
459512

460-
expect(result['node_modules/pkg/index.js']).toBe(FILE_READ_STATUS.IGNORED)
513+
expect(
514+
result['node_modules/pkg/index.js']!.startsWith(
515+
FILE_READ_STATUS.IGNORED,
516+
),
517+
).toBe(true)
461518
expect(isFileIgnoredSpy).toHaveBeenCalled()
462519
})
463520

@@ -615,8 +672,12 @@ describe('getFiles', () => {
615672
fileFilter: () => ({ status: 'allow' }),
616673
})
617674

618-
expect(result['.env.example']).toBe(FILE_READ_STATUS.IGNORED)
619-
expect(result['.ENV.SAMPLE']).toBe(FILE_READ_STATUS.IGNORED)
675+
expect(result['.env.example']!.startsWith(FILE_READ_STATUS.IGNORED)).toBe(
676+
true,
677+
)
678+
expect(result['.ENV.SAMPLE']!.startsWith(FILE_READ_STATUS.IGNORED)).toBe(
679+
true,
680+
)
620681
expect(isFileIgnoredSpy).toHaveBeenCalledTimes(2)
621682
})
622683

sdk/src/__tests__/run-file-filter.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,9 @@ describe('CodebuffClientOptions fileFilter', () => {
275275
})
276276

277277
expect(result.output.type).toBe('lastMessage')
278-
expect(requestedFiles['.env.example']).toBe(FILE_READ_STATUS.IGNORED)
278+
expect(
279+
requestedFiles['.env.example']!.startsWith(FILE_READ_STATUS.IGNORED),
280+
).toBe(true)
279281
})
280282

281283
it('should pass fileFilter to requestOptionalFile as well', async () => {

sdk/src/tools/read-files.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,20 @@ export async function getFiles(params: {
9999
...(isEnvTemplate ? { allowEnvTemplate: true } : {}),
100100
})
101101
if (ignored) {
102-
result[relativePath] = FILE_READ_STATUS.IGNORED
102+
// Keep the sentinel as the prefix (consumers match with startsWith)
103+
// and append the reason, following the FILE_TOO_LARGE precedent.
104+
// The ignore check never touches the file itself, so only claim
105+
// existence when a stat confirms it.
106+
let exists = false
107+
try {
108+
await fs.stat(fullPath)
109+
exists = true
110+
} catch {
111+
// missing or unreadable: omit the existence claim
112+
}
113+
result[relativePath] =
114+
FILE_READ_STATUS.IGNORED +
115+
`: ${isEnvTemplate ? 'blocked by ignore-rule checking' : 'excluded by ignore rules'} (.gitignore, .codebuffignore, or built-in defaults), not an OS permission issue.${exists ? ' The file exists on disk;' : ''} glob and code_search omit it for the same reason. To allow tool reads, adjust or negate the matching rule in .codebuffignore (a file-level negation cannot re-include a path under an excluded directory).`
103116
continue
104117
}
105118
}

0 commit comments

Comments
 (0)