mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 15:31:27 +00:00
Sync upstream Gemini-CLI v0.8.2 (#838)
This commit is contained in:
parent
096fabb5d6
commit
eb95c131be
644 changed files with 70389 additions and 23709 deletions
|
|
@ -17,13 +17,15 @@ import type { Content } from '@google/genai';
|
|||
import type { GeminiClient } from '@qwen-code/qwen-code-core';
|
||||
|
||||
import * as fsPromises from 'node:fs/promises';
|
||||
import { chatCommand } from './chatCommand.js';
|
||||
import { chatCommand, serializeHistoryToMarkdown } from './chatCommand.js';
|
||||
import type { Stats } from 'node:fs';
|
||||
import type { HistoryItemWithoutId } from '../types.js';
|
||||
import path from 'node:path';
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
stat: vi.fn(),
|
||||
readdir: vi.fn().mockResolvedValue(['file1.txt', 'file2.txt'] as string[]),
|
||||
writeFile: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('chatCommand', () => {
|
||||
|
|
@ -37,7 +39,7 @@ describe('chatCommand', () => {
|
|||
let mockGetHistory: ReturnType<typeof vi.fn>;
|
||||
|
||||
const getSubCommand = (
|
||||
name: 'list' | 'save' | 'resume' | 'delete',
|
||||
name: 'list' | 'save' | 'resume' | 'delete' | 'share',
|
||||
): SlashCommand => {
|
||||
const subCommand = chatCommand.subCommands?.find(
|
||||
(cmd) => cmd.name === name,
|
||||
|
|
@ -86,7 +88,7 @@ describe('chatCommand', () => {
|
|||
it('should have the correct main command definition', () => {
|
||||
expect(chatCommand.name).toBe('chat');
|
||||
expect(chatCommand.description).toBe('Manage conversation history.');
|
||||
expect(chatCommand.subCommands).toHaveLength(4);
|
||||
expect(chatCommand.subCommands).toHaveLength(5);
|
||||
});
|
||||
|
||||
describe('list subcommand', () => {
|
||||
|
|
@ -407,4 +409,293 @@ describe('chatCommand', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('share subcommand', () => {
|
||||
let shareCommand: SlashCommand;
|
||||
const mockHistory = [
|
||||
{ role: 'user', parts: [{ text: 'context' }] },
|
||||
{ role: 'model', parts: [{ text: 'context response' }] },
|
||||
{ role: 'user', parts: [{ text: 'Hello' }] },
|
||||
{ role: 'model', parts: [{ text: 'Hi there!' }] },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
shareCommand = getSubCommand('share');
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(
|
||||
path.resolve('/usr/local/google/home/myuser/gemini-cli'),
|
||||
);
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1234567890);
|
||||
mockGetHistory.mockReturnValue(mockHistory);
|
||||
mockFs.writeFile.mockClear();
|
||||
});
|
||||
|
||||
it('should default to a json file if no path is provided', async () => {
|
||||
const result = await shareCommand?.action?.(mockContext, '');
|
||||
const expectedPath = path.join(
|
||||
process.cwd(),
|
||||
'gemini-conversation-1234567890.json',
|
||||
);
|
||||
const [actualPath, actualContent] = mockFs.writeFile.mock.calls[0];
|
||||
expect(actualPath).toEqual(expectedPath);
|
||||
expect(actualContent).toEqual(JSON.stringify(mockHistory, null, 2));
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Conversation shared to ${expectedPath}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should share the conversation to a JSON file', async () => {
|
||||
const filePath = 'my-chat.json';
|
||||
const result = await shareCommand?.action?.(mockContext, filePath);
|
||||
const expectedPath = path.join(process.cwd(), 'my-chat.json');
|
||||
const [actualPath, actualContent] = mockFs.writeFile.mock.calls[0];
|
||||
expect(actualPath).toEqual(expectedPath);
|
||||
expect(actualContent).toEqual(JSON.stringify(mockHistory, null, 2));
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Conversation shared to ${expectedPath}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should share the conversation to a Markdown file', async () => {
|
||||
const filePath = 'my-chat.md';
|
||||
const result = await shareCommand?.action?.(mockContext, filePath);
|
||||
const expectedPath = path.join(process.cwd(), 'my-chat.md');
|
||||
const [actualPath, actualContent] = mockFs.writeFile.mock.calls[0];
|
||||
expect(actualPath).toEqual(expectedPath);
|
||||
const expectedContent = `🧑💻 ## USER
|
||||
|
||||
context
|
||||
|
||||
---
|
||||
|
||||
✨ ## MODEL
|
||||
|
||||
context response
|
||||
|
||||
---
|
||||
|
||||
🧑💻 ## USER
|
||||
|
||||
Hello
|
||||
|
||||
---
|
||||
|
||||
✨ ## MODEL
|
||||
|
||||
Hi there!`;
|
||||
expect(actualContent).toEqual(expectedContent);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Conversation shared to ${expectedPath}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error for unsupported file extensions', async () => {
|
||||
const filePath = 'my-chat.txt';
|
||||
const result = await shareCommand?.action?.(mockContext, filePath);
|
||||
expect(mockFs.writeFile).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Invalid file format. Only .md and .json are supported.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should inform if there is no conversation to share', async () => {
|
||||
mockGetHistory.mockReturnValue([
|
||||
{ role: 'user', parts: [{ text: 'context' }] },
|
||||
{ role: 'model', parts: [{ text: 'context response' }] },
|
||||
]);
|
||||
const result = await shareCommand?.action?.(mockContext, 'my-chat.json');
|
||||
expect(mockFs.writeFile).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No conversation found to share.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors during file writing', async () => {
|
||||
const error = new Error('Permission denied');
|
||||
mockFs.writeFile.mockRejectedValue(error);
|
||||
const result = await shareCommand?.action?.(mockContext, 'my-chat.json');
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Error sharing conversation: ${error.message}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should output valid JSON schema', async () => {
|
||||
const filePath = 'my-chat.json';
|
||||
await shareCommand?.action?.(mockContext, filePath);
|
||||
const expectedPath = path.join(process.cwd(), 'my-chat.json');
|
||||
const [actualPath, actualContent] = mockFs.writeFile.mock.calls[0];
|
||||
expect(actualPath).toEqual(expectedPath);
|
||||
const parsedContent = JSON.parse(actualContent);
|
||||
expect(Array.isArray(parsedContent)).toBe(true);
|
||||
parsedContent.forEach((item: Content) => {
|
||||
expect(item).toHaveProperty('role');
|
||||
expect(item).toHaveProperty('parts');
|
||||
expect(Array.isArray(item.parts)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should output correct markdown format', async () => {
|
||||
const filePath = 'my-chat.md';
|
||||
await shareCommand?.action?.(mockContext, filePath);
|
||||
const expectedPath = path.join(process.cwd(), 'my-chat.md');
|
||||
const [actualPath, actualContent] = mockFs.writeFile.mock.calls[0];
|
||||
expect(actualPath).toEqual(expectedPath);
|
||||
const entries = actualContent.split('\n\n---\n\n');
|
||||
expect(entries.length).toBe(mockHistory.length);
|
||||
entries.forEach((entry, index) => {
|
||||
const { role, parts } = mockHistory[index];
|
||||
const text = parts.map((p) => p.text).join('');
|
||||
const roleIcon = role === 'user' ? '🧑💻' : '✨';
|
||||
expect(entry).toBe(`${roleIcon} ## ${role.toUpperCase()}\n\n${text}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeHistoryToMarkdown', () => {
|
||||
it('should correctly serialize chat history to Markdown with icons', () => {
|
||||
const history: Content[] = [
|
||||
{ role: 'user', parts: [{ text: 'Hello' }] },
|
||||
{ role: 'model', parts: [{ text: 'Hi there!' }] },
|
||||
{ role: 'user', parts: [{ text: 'How are you?' }] },
|
||||
];
|
||||
|
||||
const expectedMarkdown =
|
||||
'🧑💻 ## USER\n\nHello\n\n---\n\n' +
|
||||
'✨ ## MODEL\n\nHi there!\n\n---\n\n' +
|
||||
'🧑💻 ## USER\n\nHow are you?';
|
||||
|
||||
const result = serializeHistoryToMarkdown(history);
|
||||
expect(result).toBe(expectedMarkdown);
|
||||
});
|
||||
|
||||
it('should handle empty history', () => {
|
||||
const history: Content[] = [];
|
||||
const result = serializeHistoryToMarkdown(history);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle items with no text parts', () => {
|
||||
const history: Content[] = [
|
||||
{ role: 'user', parts: [{ text: 'Hello' }] },
|
||||
{ role: 'model', parts: [] },
|
||||
{ role: 'user', parts: [{ text: 'How are you?' }] },
|
||||
];
|
||||
|
||||
const expectedMarkdown = `🧑💻 ## USER
|
||||
|
||||
Hello
|
||||
|
||||
---
|
||||
|
||||
✨ ## MODEL
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
🧑💻 ## USER
|
||||
|
||||
How are you?`;
|
||||
|
||||
const result = serializeHistoryToMarkdown(history);
|
||||
expect(result).toBe(expectedMarkdown);
|
||||
});
|
||||
|
||||
it('should correctly serialize function calls and responses', () => {
|
||||
const history: Content[] = [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'Please call a function.' }],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
functionCall: {
|
||||
name: 'my-function',
|
||||
args: { arg1: 'value1' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: 'my-function',
|
||||
response: { result: 'success' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expectedMarkdown = `🧑💻 ## USER
|
||||
|
||||
Please call a function.
|
||||
|
||||
---
|
||||
|
||||
✨ ## MODEL
|
||||
|
||||
**Tool Command**:
|
||||
\`\`\`json
|
||||
{
|
||||
"name": "my-function",
|
||||
"args": {
|
||||
"arg1": "value1"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
🧑💻 ## USER
|
||||
|
||||
**Tool Response**:
|
||||
\`\`\`json
|
||||
{
|
||||
"name": "my-function",
|
||||
"response": {
|
||||
"result": "success"
|
||||
}
|
||||
}
|
||||
\`\`\``;
|
||||
|
||||
const result = serializeHistoryToMarkdown(history);
|
||||
expect(result).toBe(expectedMarkdown);
|
||||
});
|
||||
|
||||
it('should handle items with undefined role', () => {
|
||||
const history: Array<Partial<Content>> = [
|
||||
{ role: 'user', parts: [{ text: 'Hello' }] },
|
||||
{ parts: [{ text: 'Hi there!' }] },
|
||||
];
|
||||
|
||||
const expectedMarkdown = `🧑💻 ## USER
|
||||
|
||||
Hello
|
||||
|
||||
---
|
||||
|
||||
✨ ## MODEL
|
||||
|
||||
Hi there!`;
|
||||
|
||||
const result = serializeHistoryToMarkdown(history as Content[]);
|
||||
expect(result).toBe(expectedMarkdown);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue