test: verify Best-Effort Path Extractor in handleAtCommand and fix multi-workspace mocks

This commit is contained in:
Coco Sheng
2026-05-19 13:45:44 -04:00
parent 5467769e6c
commit 6a85c9b339
3 changed files with 146 additions and 18 deletions

View File

@@ -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',
),
);
});
});

View File

@@ -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(

View File

@@ -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;