Merge pull request #274 from QwenLM/feat/memory_tool_docs

Make `/init` respect configured context filename and align docs with QWEN.md
This commit is contained in:
tanzhenxin
2025-08-12 17:56:16 +08:00
committed by GitHub
8 changed files with 108 additions and 59 deletions

View File

@@ -11,16 +11,31 @@ import { initCommand } from './initCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { type CommandContext } from './types.js';
// Mock the 'fs' module
vi.mock('fs', () => ({
existsSync: vi.fn(),
writeFileSync: vi.fn(),
}));
// Mock the 'fs' module with both named and default exports to avoid breaking default import sites
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
const existsSync = vi.fn();
const writeFileSync = vi.fn();
const readFileSync = vi.fn();
return {
...actual,
existsSync,
writeFileSync,
readFileSync,
default: {
...(actual as unknown as Record<string, unknown>),
existsSync,
writeFileSync,
readFileSync,
},
} as unknown as typeof import('fs');
});
describe('initCommand', () => {
let mockContext: CommandContext;
const targetDir = '/test/dir';
const geminiMdPath = path.join(targetDir, 'QWEN.md');
const DEFAULT_CONTEXT_FILENAME = 'QWEN.md';
const geminiMdPath = path.join(targetDir, DEFAULT_CONTEXT_FILENAME);
beforeEach(() => {
// Create a fresh mock context for each test
@@ -38,9 +53,10 @@ describe('initCommand', () => {
vi.clearAllMocks();
});
it('should inform the user if QWEN.md already exists', async () => {
it(`should inform the user if ${DEFAULT_CONTEXT_FILENAME} already exists and is non-empty`, async () => {
// Arrange: Simulate that the file exists
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.spyOn(fs, 'readFileSync').mockReturnValue('# Existing content');
// Act: Run the command's action
const result = await initCommand.action!(mockContext, '');
@@ -49,14 +65,13 @@ describe('initCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'info',
content:
'A QWEN.md file already exists in this directory. No changes were made.',
content: `A ${DEFAULT_CONTEXT_FILENAME} file already exists in this directory. No changes were made.`,
});
// Assert: Ensure no file was written
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
it('should create QWEN.md and submit a prompt if it does not exist', async () => {
it(`should create ${DEFAULT_CONTEXT_FILENAME} and submit a prompt if it does not exist`, async () => {
// Arrange: Simulate that the file does not exist
vi.mocked(fs.existsSync).mockReturnValue(false);
@@ -70,7 +85,7 @@ describe('initCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'Empty QWEN.md created. Now analyzing the project to populate it.',
text: `Empty ${DEFAULT_CONTEXT_FILENAME} created. Now analyzing the project to populate it.`,
},
expect.any(Number),
);
@@ -78,10 +93,20 @@ describe('initCommand', () => {
// Assert: Check that the correct prompt is submitted
expect(result.type).toBe('submit_prompt');
expect(result.content).toContain(
'You are an AI agent that brings the power of Gemini',
'You are Qwen Code, an interactive CLI agent',
);
});
it(`should proceed to initialize when ${DEFAULT_CONTEXT_FILENAME} exists but is empty`, async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.spyOn(fs, 'readFileSync').mockReturnValue(' \n ');
const result = await initCommand.action!(mockContext, '');
expect(fs.writeFileSync).toHaveBeenCalledWith(geminiMdPath, '', 'utf8');
expect(result.type).toBe('submit_prompt');
});
it('should return an error if config is not available', async () => {
// Arrange: Create a context without config
const noConfigContext = createMockCommandContext();

View File

@@ -12,6 +12,7 @@ import {
SlashCommandActionReturn,
CommandKind,
} from './types.js';
import { getCurrentGeminiMdFilename } from '@qwen-code/qwen-code-core';
export const initCommand: SlashCommand = {
name: 'init',
@@ -29,32 +30,55 @@ export const initCommand: SlashCommand = {
};
}
const targetDir = context.services.config.getTargetDir();
const geminiMdPath = path.join(targetDir, 'QWEN.md');
const contextFileName = getCurrentGeminiMdFilename();
const contextFilePath = path.join(targetDir, contextFileName);
if (fs.existsSync(geminiMdPath)) {
try {
if (fs.existsSync(contextFilePath)) {
// If file exists but is empty (or whitespace), continue to initialize; otherwise, bail out
try {
const existing = fs.readFileSync(contextFilePath, 'utf8');
if (existing && existing.trim().length > 0) {
return {
type: 'message',
messageType: 'info',
content: `A ${contextFileName} file already exists in this directory. No changes were made.`,
};
}
} catch {
// If we fail to read, conservatively proceed to (re)create the file
}
}
// Ensure an empty context file exists before prompting the model to populate it
try {
fs.writeFileSync(contextFilePath, '', 'utf8');
context.ui.addItem(
{
type: 'info',
text: `Empty ${contextFileName} created. Now analyzing the project to populate it.`,
},
Date.now(),
);
} catch (err) {
return {
type: 'message',
messageType: 'error',
content: `Failed to create ${contextFileName}: ${err instanceof Error ? err.message : String(err)}`,
};
}
} catch (error) {
return {
type: 'message',
messageType: 'info',
content:
'A QWEN.md file already exists in this directory. No changes were made.',
messageType: 'error',
content: `Unexpected error preparing ${contextFileName}: ${error instanceof Error ? error.message : String(error)}`,
};
}
// Create an empty QWEN.md file
fs.writeFileSync(geminiMdPath, '', 'utf8');
context.ui.addItem(
{
type: 'info',
text: 'Empty QWEN.md created. Now analyzing the project to populate it.',
},
Date.now(),
);
return {
type: 'submit_prompt',
content: `
You are an AI agent that brings the power of Gemini directly into the terminal. Your task is to analyze the current directory and generate a comprehensive QWEN.md file to be used as instructional context for future interactions.
You are Qwen Code, an interactive CLI agent. Analyze the current directory and generate a comprehensive ${contextFileName} file to be used as instructional context for future interactions.
**Analysis Process:**
@@ -70,7 +94,7 @@ You are an AI agent that brings the power of Gemini directly into the terminal.
* **Code Project:** Look for clues like \`package.json\`, \`requirements.txt\`, \`pom.xml\`, \`go.mod\`, \`Cargo.toml\`, \`build.gradle\`, or a \`src\` directory. If you find them, this is likely a software project.
* **Non-Code Project:** If you don't find code-related files, this might be a directory for documentation, research papers, notes, or something else.
**QWEN.md Content Generation:**
**${contextFileName} Content Generation:**
**For a Code Project:**
@@ -86,7 +110,7 @@ You are an AI agent that brings the power of Gemini directly into the terminal.
**Final Output:**
Write the complete content to the \`QWEN.md\` file. The output must be well-formatted Markdown.
Write the complete content to the \`${contextFileName}\` file. The output must be well-formatted Markdown.
`,
};
},