mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 04:00:36 +00:00
refactor: remove read_many_files tool, add readManyFiles utility for user @-commands
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
bd900d3668
commit
7e5c1ae43a
26 changed files with 1118 additions and 2121 deletions
|
|
@ -10,10 +10,7 @@ import { handleAtCommand } from './atCommandProcessor.js';
|
|||
import type { Config } from '@qwen-code/qwen-code-core';
|
||||
import {
|
||||
FileDiscoveryService,
|
||||
GlobTool,
|
||||
ReadManyFilesTool,
|
||||
StandardFileSystemService,
|
||||
ToolRegistry,
|
||||
COMMON_IGNORE_PATTERNS,
|
||||
// DEFAULT_FILE_EXCLUDES,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
|
@ -47,11 +44,9 @@ describe('handleAtCommand', () => {
|
|||
|
||||
abortController = new AbortController();
|
||||
|
||||
const getToolRegistry = vi.fn();
|
||||
|
||||
mockConfig = {
|
||||
getToolRegistry,
|
||||
getTargetDir: () => testRootDir,
|
||||
getProjectRoot: () => testRootDir,
|
||||
isSandboxed: () => false,
|
||||
getFileService: () => new FileDiscoveryService(testRootDir),
|
||||
getFileFilteringRespectGitIgnore: () => true,
|
||||
|
|
@ -83,11 +78,6 @@ describe('handleAtCommand', () => {
|
|||
getTruncateToolOutputThreshold: () => 2500,
|
||||
getTruncateToolOutputLines: () => 500,
|
||||
} as unknown as Config;
|
||||
|
||||
const registry = new ToolRegistry(mockConfig);
|
||||
registry.registerTool(new ReadManyFilesTool(mockConfig));
|
||||
registry.registerTool(new GlobTool(mockConfig));
|
||||
getToolRegistry.mockReturnValue(registry);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -101,13 +91,12 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 123,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [{ text: query }],
|
||||
shouldProceed: true,
|
||||
});
|
||||
|
|
@ -119,13 +108,12 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query: queryWithSpaces,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 124,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [{ text: queryWithSpaces }],
|
||||
shouldProceed: true,
|
||||
});
|
||||
|
|
@ -145,62 +133,59 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 125,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
processedQuery: [
|
||||
{ text: `@${filePath}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
shouldProceed: true,
|
||||
});
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'tool_group',
|
||||
tools: [expect.objectContaining({ status: ToolCallStatus.Success })],
|
||||
}),
|
||||
125,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([
|
||||
{ text: `@${filePath}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
]);
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
// toolDisplays should be returned for caller to add to UI history
|
||||
expect(result.toolDisplays).toBeDefined();
|
||||
expect(result.toolDisplays).toHaveLength(1);
|
||||
expect(result.toolDisplays![0].status).toBe(ToolCallStatus.Success);
|
||||
});
|
||||
|
||||
it('should process a valid directory path and convert to glob', async () => {
|
||||
const fileContent = 'This is the file content.';
|
||||
it('should process a valid directory path', async () => {
|
||||
const filePath = await createTestFile(
|
||||
path.join(testRootDir, 'path', 'to', 'file.txt'),
|
||||
fileContent,
|
||||
'This is the file content.',
|
||||
);
|
||||
const dirPath = path.dirname(filePath);
|
||||
const query = `@${dirPath}`;
|
||||
const resolvedGlob = `${dirPath}/**`;
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 126,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
processedQuery: [
|
||||
{ text: `@${resolvedGlob}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
shouldProceed: true,
|
||||
});
|
||||
const processedText = Array.isArray(result.processedQuery)
|
||||
? result.processedQuery
|
||||
.map((part) =>
|
||||
typeof part === 'string'
|
||||
? part
|
||||
: 'text' in part
|
||||
? part.text
|
||||
: JSON.stringify(part),
|
||||
)
|
||||
.join('')
|
||||
: '';
|
||||
|
||||
expect(processedText).toContain(`@${dirPath}`);
|
||||
expect(processedText).toContain(`Content from ${dirPath}:`);
|
||||
expect(processedText).toContain('Showing up to');
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Path ${dirPath} resolved to directory, using glob: ${resolvedGlob}`,
|
||||
`Path ${dirPath} resolved to directory.`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -217,17 +202,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 128,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `${textBefore}@${filePath}${textAfter}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -247,29 +231,23 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 125,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
processedQuery: [
|
||||
{ text: `@${filePath}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
shouldProceed: true,
|
||||
});
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'tool_group',
|
||||
tools: [expect.objectContaining({ status: ToolCallStatus.Success })],
|
||||
}),
|
||||
125,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([
|
||||
{ text: `@${filePath}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
]);
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
// toolDisplays should be returned for caller to add to UI history
|
||||
expect(result.toolDisplays).toBeDefined();
|
||||
expect(result.toolDisplays).toHaveLength(1);
|
||||
expect(result.toolDisplays![0].status).toBe(ToolCallStatus.Success);
|
||||
});
|
||||
|
||||
it('should handle multiple @file references', async () => {
|
||||
|
|
@ -288,19 +266,18 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 130,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: query },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${file1Path}:\n` },
|
||||
{ text: `\nContent from ${file1Path}:\n` },
|
||||
{ text: content1 },
|
||||
{ text: `\nContent from @${file2Path}:\n` },
|
||||
{ text: `\nContent from ${file2Path}:\n` },
|
||||
{ text: content2 },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -327,19 +304,18 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 131,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: query },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${file1Path}:\n` },
|
||||
{ text: `\nContent from ${file1Path}:\n` },
|
||||
{ text: content1 },
|
||||
{ text: `\nContent from @${file2Path}:\n` },
|
||||
{ text: `\nContent from ${file2Path}:\n` },
|
||||
{ text: content2 },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -364,31 +340,27 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 132,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{
|
||||
text: `Look at @${file1Path} then @${invalidFile} and also just @ symbol, then @${file2Path}`,
|
||||
},
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${file2Path}:\n` },
|
||||
{ text: content2 },
|
||||
{ text: `\nContent from @${file1Path}:\n` },
|
||||
{ text: `\nContent from ${file1Path}:\n` },
|
||||
{ text: content1 },
|
||||
{ text: `\nContent from ${file2Path}:\n` },
|
||||
{ text: content2 },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
shouldProceed: true,
|
||||
});
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Path ${invalidFile} not found directly, attempting glob search.`,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Glob search for '**/*${invalidFile}*' found no files or an error. Path ${invalidFile} will be skipped.`,
|
||||
`Path ${invalidFile} not found. Path ${invalidFile} will be skipped.`,
|
||||
);
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
'Lone @ detected, will be treated as text in the modified query.',
|
||||
|
|
@ -401,13 +373,12 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 133,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [{ text: 'Check @nonexistent.txt and @ also' }],
|
||||
shouldProceed: true,
|
||||
});
|
||||
|
|
@ -435,13 +406,12 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 200,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [{ text: query }],
|
||||
shouldProceed: true,
|
||||
});
|
||||
|
|
@ -468,17 +438,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 201,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `@${validFile}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${validFile}:\n` },
|
||||
{ text: `\nContent from ${validFile}:\n` },
|
||||
{ text: 'console.log("Hello world");' },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -501,17 +470,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 202,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `@${validFile} @${gitIgnoredFile}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${validFile}:\n` },
|
||||
{ text: `\nContent from ${validFile}:\n` },
|
||||
{ text: '# Project README' },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -535,13 +503,12 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 203,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [{ text: query }],
|
||||
shouldProceed: true,
|
||||
});
|
||||
|
|
@ -554,32 +521,6 @@ describe('handleAtCommand', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when recursive file search is disabled', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(mockConfig.getEnableRecursiveFileSearch).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('should not use glob search for a nonexistent file', async () => {
|
||||
const invalidFile = 'nonexistent.txt';
|
||||
const query = `@${invalidFile}`;
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 300,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Glob tool not found. Path ${invalidFile} will be skipped.`,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([{ text: query }]);
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('qwen-ignore filtering', () => {
|
||||
it('should skip qwen-ignored files in @ commands', async () => {
|
||||
await createTestFile(
|
||||
|
|
@ -595,13 +536,12 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 204,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [{ text: query }],
|
||||
shouldProceed: true,
|
||||
});
|
||||
|
|
@ -627,17 +567,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 205,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `@${validFile}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${validFile}:\n` },
|
||||
{ text: `\nContent from ${validFile}:\n` },
|
||||
{ text: 'console.log("Hello world");' },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -663,17 +602,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 206,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `@${validFile} @${qwenIgnoredFile}` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${validFile}:\n` },
|
||||
{ text: `\nContent from ${validFile}:\n` },
|
||||
{ text: '// Main application entry' },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -791,17 +729,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: query },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -826,19 +763,18 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 411,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Compare @${file1Path}, @${file2Path}; what's different?` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${file1Path}:\n` },
|
||||
{ text: `\nContent from ${file1Path}:\n` },
|
||||
{ text: content1 },
|
||||
{ text: `\nContent from @${file2Path}:\n` },
|
||||
{ text: `\nContent from ${file2Path}:\n` },
|
||||
{ text: content2 },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -858,17 +794,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 412,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Check @${filePath}, it has spaces.` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -887,17 +822,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 413,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Analyze @${filePath} for type definitions.` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -916,17 +850,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 414,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Check @${filePath}. This file contains settings.` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -945,17 +878,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 415,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Review @${filePath}, then check dependencies.` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -974,17 +906,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 416,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Check @${filePath} contains version information.` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -1003,17 +934,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 417,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Show me @${filePath}.` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -1032,17 +962,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 418,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Check @${filePath} for content.` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -1061,17 +990,16 @@ describe('handleAtCommand', () => {
|
|||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 421,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(result).toMatchObject({
|
||||
processedQuery: [
|
||||
{ text: `Check @${filePath} please.` },
|
||||
{ text: '\n--- Content from referenced files ---' },
|
||||
{ text: `\nContent from @${filePath}:\n` },
|
||||
{ text: `\nContent from ${filePath}:\n` },
|
||||
{ text: fileContent },
|
||||
{ text: '\n--- End of content ---' },
|
||||
],
|
||||
|
|
@ -1080,7 +1008,7 @@ describe('handleAtCommand', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should not add the user's turn to history, as that is the caller's responsibility", async () => {
|
||||
it("should not add any items to history, as that is the caller's responsibility", async () => {
|
||||
// Arrange
|
||||
const fileContent = 'This is the file content.';
|
||||
const filePath = await createTestFile(
|
||||
|
|
@ -1090,26 +1018,119 @@ describe('handleAtCommand', () => {
|
|||
const query = `A query with @${filePath}`;
|
||||
|
||||
// Act
|
||||
await handleAtCommand({
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 999,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Assert
|
||||
// It SHOULD be called for the tool_group
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'tool_group' }),
|
||||
999,
|
||||
);
|
||||
// handleAtCommand should NOT call addItem at all - it returns data for caller to add
|
||||
expect(mockAddItem).not.toHaveBeenCalled();
|
||||
|
||||
// It should NOT have been called for the user turn
|
||||
const userTurnCalls = mockAddItem.mock.calls.filter(
|
||||
(call) => call[0].type === 'user',
|
||||
);
|
||||
expect(userTurnCalls).toHaveLength(0);
|
||||
// Instead, it returns toolDisplays for the caller to add to UI history
|
||||
expect(result.toolDisplays).toBeDefined();
|
||||
expect(result.toolDisplays!.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
describe('chat recording', () => {
|
||||
it('should return tool result info for each file read', async () => {
|
||||
const content1 = 'Content file1';
|
||||
const file1Path = await createTestFile(
|
||||
path.join(testRootDir, 'file1.txt'),
|
||||
content1,
|
||||
);
|
||||
const content2 = 'Content file2';
|
||||
const file2Path = await createTestFile(
|
||||
path.join(testRootDir, 'file2.txt'),
|
||||
content2,
|
||||
);
|
||||
const query = `@${file1Path} @${file2Path}`;
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 500,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should return toolDisplays (one summary for all files)
|
||||
expect(result.toolDisplays).toBeDefined();
|
||||
expect(result.toolDisplays!.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should return toolDisplays for UI and function parts in processedQuery', async () => {
|
||||
const fileContent = 'Test content';
|
||||
const filePath = await createTestFile(
|
||||
path.join(testRootDir, 'test.txt'),
|
||||
fileContent,
|
||||
);
|
||||
const query = `@${filePath}`;
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 501,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
// Should return toolDisplays for UI
|
||||
expect(result.toolDisplays).toBeDefined();
|
||||
expect(result.toolDisplays!.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// processedQuery should include file content sections
|
||||
expect(result.processedQuery).toBeDefined();
|
||||
const parts = Array.isArray(result.processedQuery)
|
||||
? result.processedQuery
|
||||
: [result.processedQuery];
|
||||
const flattened = parts
|
||||
.map((part) =>
|
||||
typeof part === 'string'
|
||||
? part
|
||||
: (part as { text?: string }).text || '',
|
||||
)
|
||||
.join('');
|
||||
expect(flattened).toContain('Content from ');
|
||||
expect(flattened).toContain(fileContent);
|
||||
});
|
||||
|
||||
it('should not return tool result infos when no files are read', async () => {
|
||||
const query = 'query without any @ commands';
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 502,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result.toolDisplays).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include file path in tool display result', async () => {
|
||||
const fileContent = 'File content here';
|
||||
const filePath = await createTestFile(
|
||||
path.join(testRootDir, 'specific-file.txt'),
|
||||
fileContent,
|
||||
);
|
||||
const query = `@${filePath}`;
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 503,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(result.toolDisplays).toBeDefined();
|
||||
expect(result.toolDisplays!.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.toolDisplays![0].description).toContain('file.txt');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue