mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Allen Hutchison <adh@google.com>
387 lines
12 KiB
TypeScript
387 lines
12 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fsPromises from 'fs/promises';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { loadServerHierarchicalMemory } from './memoryDiscovery.js';
|
|
import {
|
|
GEMINI_CONFIG_DIR,
|
|
setGeminiMdFilename,
|
|
DEFAULT_CONTEXT_FILENAME,
|
|
} from '../tools/memoryTool.js';
|
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
|
|
|
vi.mock('os', async (importOriginal) => {
|
|
const actualOs = await importOriginal<typeof os>();
|
|
return {
|
|
...actualOs,
|
|
homedir: vi.fn(),
|
|
};
|
|
});
|
|
|
|
describe('loadServerHierarchicalMemory', () => {
|
|
let testRootDir: string;
|
|
let cwd: string;
|
|
let projectRoot: string;
|
|
let homedir: string;
|
|
|
|
async function createEmptyDir(fullPath: string) {
|
|
await fsPromises.mkdir(fullPath, { recursive: true });
|
|
return fullPath;
|
|
}
|
|
|
|
async function createTestFile(fullPath: string, fileContents: string) {
|
|
await fsPromises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
await fsPromises.writeFile(fullPath, fileContents);
|
|
return path.resolve(testRootDir, fullPath);
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
testRootDir = await fsPromises.mkdtemp(
|
|
path.join(os.tmpdir(), 'folder-structure-test-'),
|
|
);
|
|
|
|
vi.resetAllMocks();
|
|
// Set environment variables to indicate test environment
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.VITEST = 'true';
|
|
|
|
projectRoot = await createEmptyDir(path.join(testRootDir, 'project'));
|
|
cwd = await createEmptyDir(path.join(projectRoot, 'src'));
|
|
homedir = await createEmptyDir(path.join(testRootDir, 'userhome'));
|
|
vi.mocked(os.homedir).mockReturnValue(homedir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Some tests set this to a different value.
|
|
setGeminiMdFilename(DEFAULT_CONTEXT_FILENAME);
|
|
// Clean up the temporary directory to prevent resource leaks.
|
|
await fsPromises.rm(testRootDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('should return empty memory and count if no context files are found', async () => {
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: '',
|
|
fileCount: 0,
|
|
});
|
|
});
|
|
|
|
it('should load only the global context file if present and others are not (default filename)', async () => {
|
|
const defaultContextFile = await createTestFile(
|
|
path.join(homedir, GEMINI_CONFIG_DIR, DEFAULT_CONTEXT_FILENAME),
|
|
'default context content',
|
|
);
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---
|
|
default context content
|
|
--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---`,
|
|
fileCount: 1,
|
|
});
|
|
});
|
|
|
|
it('should load only the global custom context file if present and filename is changed', async () => {
|
|
const customFilename = 'CUSTOM_AGENTS.md';
|
|
setGeminiMdFilename(customFilename);
|
|
|
|
const customContextFile = await createTestFile(
|
|
path.join(homedir, GEMINI_CONFIG_DIR, customFilename),
|
|
'custom context content',
|
|
);
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${path.relative(cwd, customContextFile)} ---
|
|
custom context content
|
|
--- End of Context from: ${path.relative(cwd, customContextFile)} ---`,
|
|
fileCount: 1,
|
|
});
|
|
});
|
|
|
|
it('should load context files by upward traversal with custom filename', async () => {
|
|
const customFilename = 'PROJECT_CONTEXT.md';
|
|
setGeminiMdFilename(customFilename);
|
|
|
|
const projectContextFile = await createTestFile(
|
|
path.join(projectRoot, customFilename),
|
|
'project context content',
|
|
);
|
|
const cwdContextFile = await createTestFile(
|
|
path.join(cwd, customFilename),
|
|
'cwd context content',
|
|
);
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${path.relative(cwd, projectContextFile)} ---
|
|
project context content
|
|
--- End of Context from: ${path.relative(cwd, projectContextFile)} ---
|
|
|
|
--- Context from: ${path.relative(cwd, cwdContextFile)} ---
|
|
cwd context content
|
|
--- End of Context from: ${path.relative(cwd, cwdContextFile)} ---`,
|
|
fileCount: 2,
|
|
});
|
|
});
|
|
|
|
it('should load context files by downward traversal with custom filename', async () => {
|
|
const customFilename = 'LOCAL_CONTEXT.md';
|
|
setGeminiMdFilename(customFilename);
|
|
|
|
await createTestFile(
|
|
path.join(cwd, 'subdir', customFilename),
|
|
'Subdir custom memory',
|
|
);
|
|
await createTestFile(path.join(cwd, customFilename), 'CWD custom memory');
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${customFilename} ---
|
|
CWD custom memory
|
|
--- End of Context from: ${customFilename} ---
|
|
|
|
--- Context from: ${path.join('subdir', customFilename)} ---
|
|
Subdir custom memory
|
|
--- End of Context from: ${path.join('subdir', customFilename)} ---`,
|
|
fileCount: 2,
|
|
});
|
|
});
|
|
|
|
it('should load ORIGINAL_GEMINI_MD_FILENAME files by upward traversal from CWD to project root', async () => {
|
|
const projectRootGeminiFile = await createTestFile(
|
|
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
|
|
'Project root memory',
|
|
);
|
|
const srcGeminiFile = await createTestFile(
|
|
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
|
|
'Src directory memory',
|
|
);
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
|
Project root memory
|
|
--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
|
|
|
--- Context from: ${path.relative(cwd, srcGeminiFile)} ---
|
|
Src directory memory
|
|
--- End of Context from: ${path.relative(cwd, srcGeminiFile)} ---`,
|
|
fileCount: 2,
|
|
});
|
|
});
|
|
|
|
it('should load ORIGINAL_GEMINI_MD_FILENAME files by downward traversal from CWD', async () => {
|
|
await createTestFile(
|
|
path.join(cwd, 'subdir', DEFAULT_CONTEXT_FILENAME),
|
|
'Subdir memory',
|
|
);
|
|
await createTestFile(
|
|
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
|
|
'CWD memory',
|
|
);
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${DEFAULT_CONTEXT_FILENAME} ---
|
|
CWD memory
|
|
--- End of Context from: ${DEFAULT_CONTEXT_FILENAME} ---
|
|
|
|
--- Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---
|
|
Subdir memory
|
|
--- End of Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---`,
|
|
fileCount: 2,
|
|
});
|
|
});
|
|
|
|
it('should load and correctly order global, upward, and downward ORIGINAL_GEMINI_MD_FILENAME files', async () => {
|
|
const defaultContextFile = await createTestFile(
|
|
path.join(homedir, GEMINI_CONFIG_DIR, DEFAULT_CONTEXT_FILENAME),
|
|
'default context content',
|
|
);
|
|
const rootGeminiFile = await createTestFile(
|
|
path.join(testRootDir, DEFAULT_CONTEXT_FILENAME),
|
|
'Project parent memory',
|
|
);
|
|
const projectRootGeminiFile = await createTestFile(
|
|
path.join(projectRoot, DEFAULT_CONTEXT_FILENAME),
|
|
'Project root memory',
|
|
);
|
|
const cwdGeminiFile = await createTestFile(
|
|
path.join(cwd, DEFAULT_CONTEXT_FILENAME),
|
|
'CWD memory',
|
|
);
|
|
const subDirGeminiFile = await createTestFile(
|
|
path.join(cwd, 'sub', DEFAULT_CONTEXT_FILENAME),
|
|
'Subdir memory',
|
|
);
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---
|
|
default context content
|
|
--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---
|
|
|
|
--- Context from: ${path.relative(cwd, rootGeminiFile)} ---
|
|
Project parent memory
|
|
--- End of Context from: ${path.relative(cwd, rootGeminiFile)} ---
|
|
|
|
--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
|
Project root memory
|
|
--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
|
|
|
--- Context from: ${path.relative(cwd, cwdGeminiFile)} ---
|
|
CWD memory
|
|
--- End of Context from: ${path.relative(cwd, cwdGeminiFile)} ---
|
|
|
|
--- Context from: ${path.relative(cwd, subDirGeminiFile)} ---
|
|
Subdir memory
|
|
--- End of Context from: ${path.relative(cwd, subDirGeminiFile)} ---`,
|
|
fileCount: 5,
|
|
});
|
|
});
|
|
|
|
it('should ignore specified directories during downward scan', async () => {
|
|
await createEmptyDir(path.join(projectRoot, '.git'));
|
|
await createTestFile(path.join(projectRoot, '.gitignore'), 'node_modules');
|
|
|
|
await createTestFile(
|
|
path.join(cwd, 'node_modules', DEFAULT_CONTEXT_FILENAME),
|
|
'Ignored memory',
|
|
);
|
|
const regularSubDirGeminiFile = await createTestFile(
|
|
path.join(cwd, 'my_code', DEFAULT_CONTEXT_FILENAME),
|
|
'My code memory',
|
|
);
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
[],
|
|
'tree',
|
|
{
|
|
respectGitIgnore: true,
|
|
respectGeminiIgnore: true,
|
|
},
|
|
200, // maxDirs parameter
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---
|
|
My code memory
|
|
--- End of Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---`,
|
|
fileCount: 1,
|
|
});
|
|
});
|
|
|
|
it('should respect the maxDirs parameter during downward scan', async () => {
|
|
const consoleDebugSpy = vi
|
|
.spyOn(console, 'debug')
|
|
.mockImplementation(() => {});
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
await createEmptyDir(path.join(cwd, `deep_dir_${i}`));
|
|
}
|
|
|
|
// Pass the custom limit directly to the function
|
|
await loadServerHierarchicalMemory(
|
|
cwd,
|
|
true,
|
|
new FileDiscoveryService(projectRoot),
|
|
[],
|
|
'tree', // importFormat
|
|
{
|
|
respectGitIgnore: true,
|
|
respectGeminiIgnore: true,
|
|
},
|
|
50, // maxDirs
|
|
);
|
|
|
|
expect(consoleDebugSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('[DEBUG] [BfsFileSearch]'),
|
|
expect.stringContaining('Scanning [50/50]:'),
|
|
);
|
|
|
|
vi.mocked(console.debug).mockRestore();
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: '',
|
|
fileCount: 0,
|
|
});
|
|
});
|
|
|
|
it('should load extension context file paths', async () => {
|
|
const extensionFilePath = await createTestFile(
|
|
path.join(testRootDir, 'extensions/ext1/GEMINI.md'),
|
|
'Extension memory content',
|
|
);
|
|
|
|
const result = await loadServerHierarchicalMemory(
|
|
cwd,
|
|
false,
|
|
new FileDiscoveryService(projectRoot),
|
|
[extensionFilePath],
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
memoryContent: `--- Context from: ${path.relative(cwd, extensionFilePath)} ---
|
|
Extension memory content
|
|
--- End of Context from: ${path.relative(cwd, extensionFilePath)} ---`,
|
|
fileCount: 1,
|
|
});
|
|
});
|
|
});
|