mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-02 11:22:23 +00:00
test: verify Best-Effort Path Extractor in handleAtCommand and fix multi-workspace mocks
This commit is contained in:
@@ -1553,7 +1553,38 @@ describe('handleAtCommand', () => {
|
||||
// Malformed path should be skipped and original query part preserved as text
|
||||
expect(result.processedQuery).toEqual([{ text: query }]);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Skipping invalid path in @-command'),
|
||||
expect.stringContaining(
|
||||
'Identified invalid path fragment, attempting to extract path',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should recover a buried path from a malformed fragment during handleAtCommand', async () => {
|
||||
const buriedFile = 'src/recovered.ts';
|
||||
await createTestFile(
|
||||
path.join(testRootDir, buriedFile),
|
||||
'Recovered content',
|
||||
);
|
||||
const malformedFragment = `"FAIL ${buriedFile}:10:5 (AssertionError)"`;
|
||||
const query = `@${malformedFragment}`;
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 703,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// It should extract src/recovered.ts and attach its content
|
||||
expect(result.processedQuery).toContainEqual(
|
||||
expect.objectContaining({ text: 'Recovered content' }),
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Identified invalid path fragment, attempting to extract path',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,6 +275,109 @@ describe('atCommandUtils', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('Best-Effort Path Extraction (tryExtractPath)', () => {
|
||||
const mockFile = 'src/index.ts';
|
||||
const absMockFile = path.resolve('/mock/root', mockFile);
|
||||
const mockStats = { isDirectory: () => false, isFile: () => true };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(fsPromises.stat).mockImplementation(async (p) => {
|
||||
if (p === absMockFile) return mockStats as unknown as Stats;
|
||||
throw new Error('ENOENT');
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract path from "AssertionError: ..." format', async () => {
|
||||
const result = await resolveAtCommandPath(
|
||||
`AssertionError: expected something but got something else at ${mockFile}:10:5`,
|
||||
mockConfig as unknown as Config,
|
||||
);
|
||||
expect(result.status).toBe('resolved');
|
||||
if (result.status === 'resolved') {
|
||||
expect(result.resolved.absolutePath).toBe(absMockFile);
|
||||
}
|
||||
});
|
||||
|
||||
it('should extract path wrapped in parentheses', async () => {
|
||||
const result = await resolveAtCommandPath(
|
||||
`FAIL (${mockFile})`,
|
||||
mockConfig as unknown as Config,
|
||||
);
|
||||
expect(result.status).toBe('resolved');
|
||||
if (result.status === 'resolved') {
|
||||
expect(result.resolved.absolutePath).toBe(absMockFile);
|
||||
}
|
||||
});
|
||||
|
||||
it('should extract path wrapped in square brackets', async () => {
|
||||
const result = await resolveAtCommandPath(
|
||||
`FAIL [${mockFile}]`,
|
||||
mockConfig as unknown as Config,
|
||||
);
|
||||
expect(result.status).toBe('resolved');
|
||||
if (result.status === 'resolved') {
|
||||
expect(result.resolved.absolutePath).toBe(absMockFile);
|
||||
}
|
||||
});
|
||||
|
||||
it('should extract path from "✓" pass marker', async () => {
|
||||
const result = await resolveAtCommandPath(
|
||||
`✓ ${mockFile}`,
|
||||
mockConfig as unknown as Config,
|
||||
);
|
||||
expect(result.status).toBe('resolved');
|
||||
});
|
||||
|
||||
it('should extract path from "×" fail marker', async () => {
|
||||
const result = await resolveAtCommandPath(
|
||||
`× ${mockFile}`,
|
||||
mockConfig as unknown as Config,
|
||||
);
|
||||
expect(result.status).toBe('resolved');
|
||||
});
|
||||
|
||||
it('should handle paths with slashes and extensions correctly', async () => {
|
||||
const complexPath = 'packages/core/src/utils/deep.test.ts';
|
||||
const absComplexPath = path.resolve('/mock/root', complexPath);
|
||||
vi.mocked(fsPromises.stat).mockImplementation(async (p) => {
|
||||
if (p === absComplexPath) return mockStats as unknown as Stats;
|
||||
throw new Error('ENOENT');
|
||||
});
|
||||
|
||||
const result = await resolveAtCommandPath(
|
||||
`FAIL ${complexPath}:123`,
|
||||
mockConfig as unknown as Config,
|
||||
);
|
||||
expect(result.status).toBe('resolved');
|
||||
if (result.status === 'resolved') {
|
||||
expect(result.resolved.relativePath).toBe(complexPath);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail gracefully if no valid path can be extracted', async () => {
|
||||
const result = await resolveAtCommandPath(
|
||||
'FAIL some random text with no slashes or dots',
|
||||
mockConfig as unknown as Config,
|
||||
);
|
||||
expect(result.status).toBe('invalid');
|
||||
});
|
||||
|
||||
it('should return unauthorized if the extracted path is not authorized', async () => {
|
||||
const secretFile = '/etc/passwd';
|
||||
(mockConfig['validatePathAccess'] as Mock).mockImplementation((p) => p === secretFile ? 'Unauthorized' : null);
|
||||
vi.mocked(fsPromises.stat).mockResolvedValue(
|
||||
mockStats as unknown as Stats,
|
||||
);
|
||||
|
||||
const result = await resolveAtCommandPath(
|
||||
`FAIL ${secretFile}`,
|
||||
mockConfig as unknown as Config,
|
||||
);
|
||||
// It should try to resolve /etc/passwd, identify it as unauthorized, and return that status.
|
||||
expect(result.status).toBe('unauthorized');
|
||||
});
|
||||
});
|
||||
|
||||
it('should include reason in debug message for unauthorized paths', async () => {
|
||||
const onDebug = vi.fn();
|
||||
(mockConfig['validatePathAccess'] as Mock).mockReturnValue(
|
||||
|
||||
@@ -37,20 +37,14 @@ export async function resolveAtCommandPath(
|
||||
): Promise<ResolveAtCommandPathResult> {
|
||||
const pathValidation = validatePath(pathName);
|
||||
if (!pathValidation.isValid) {
|
||||
// If it's a log fragment, try to extract a real path from it
|
||||
if (
|
||||
pathValidation.error ===
|
||||
'Path appears to be a misinterpreted log fragment.'
|
||||
) {
|
||||
const extractedPath = tryExtractPath(pathName);
|
||||
if (extractedPath && extractedPath !== pathName) {
|
||||
onDebugMessage(
|
||||
`Identified log fragment, attempting to extract path: "${extractedPath}" from "${pathName}"`,
|
||||
);
|
||||
// Recurse once with the extracted path.
|
||||
// We pass a dummy onDebugMessage to avoid double logging the "invalid" reason if it fails.
|
||||
return resolveAtCommandPath(extractedPath, config);
|
||||
}
|
||||
// Attempt to extract a real path from the invalid fragment
|
||||
const extractedPath = tryExtractPath(pathName);
|
||||
if (extractedPath && extractedPath !== pathName) {
|
||||
onDebugMessage(
|
||||
`Identified invalid path fragment, attempting to extract path: "${extractedPath}" from "${pathName}"`,
|
||||
);
|
||||
// Recurse once with the extracted path.
|
||||
return resolveAtCommandPath(extractedPath, config, onDebugMessage);
|
||||
}
|
||||
|
||||
onDebugMessage(
|
||||
@@ -147,11 +141,11 @@ function tryExtractPath(noisyString: string): string | null {
|
||||
const segments = noisyString.split(/\s+/);
|
||||
|
||||
for (const segment of segments) {
|
||||
// 1. Strip leading/trailing punctuation commonly found in logs (commas, parens, etc.)
|
||||
// 1. Strip leading/trailing punctuation and quotes commonly found in logs
|
||||
// 2. Strip trailing line/column numbers (e.g. src/main.ts:10:5)
|
||||
const cleanSegment = segment
|
||||
.replace(/^[(),;[\]]/, '')
|
||||
.replace(/[(),;[\]]$/, '')
|
||||
.replace(/^[(),;[\]"']/, '')
|
||||
.replace(/[(),;[\]"']$/, '')
|
||||
.replace(/:\d+(?::\d+)?$/, '');
|
||||
|
||||
if (cleanSegment.length === 0) continue;
|
||||
|
||||
Reference in New Issue
Block a user