mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
chore(test): install and configure vitest eslint plugin (#3228)
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
@@ -344,37 +344,6 @@ describe('Gemini Client (client.ts)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateContent', () => {
|
||||
it('should call generateContent with the correct parameters', async () => {
|
||||
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
||||
const generationConfig = { temperature: 0.5 };
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
// Mock countTokens
|
||||
const mockGenerator: Partial<ContentGenerator> = {
|
||||
countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
|
||||
generateContent: mockGenerateContentFn,
|
||||
};
|
||||
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
||||
|
||||
await client.generateContent(contents, generationConfig, abortSignal);
|
||||
|
||||
expect(mockGenerateContentFn).toHaveBeenCalledWith(
|
||||
{
|
||||
model: 'test-model',
|
||||
config: {
|
||||
abortSignal,
|
||||
systemInstruction: getCoreSystemPrompt(''),
|
||||
temperature: 0.5,
|
||||
topP: 1,
|
||||
},
|
||||
contents,
|
||||
},
|
||||
'test-session-id',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateJson', () => {
|
||||
it('should call generateContent with the correct parameters', async () => {
|
||||
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
||||
@@ -705,6 +674,60 @@ describe('Gemini Client (client.ts)', () => {
|
||||
// Assert that the chat was reset
|
||||
expect(newChat).not.toBe(initialChat);
|
||||
});
|
||||
|
||||
it('should use current model from config for token counting after sendMessage', async () => {
|
||||
const initialModel = client['config'].getModel();
|
||||
|
||||
const mockCountTokens = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ totalTokens: 100000 })
|
||||
.mockResolvedValueOnce({ totalTokens: 5000 });
|
||||
|
||||
const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
|
||||
|
||||
const mockChatHistory = [
|
||||
{ role: 'user', parts: [{ text: 'Long conversation' }] },
|
||||
{ role: 'model', parts: [{ text: 'Long response' }] },
|
||||
];
|
||||
|
||||
const mockChat: Partial<GeminiChat> = {
|
||||
getHistory: vi.fn().mockReturnValue(mockChatHistory),
|
||||
setHistory: vi.fn(),
|
||||
sendMessage: mockSendMessage,
|
||||
};
|
||||
|
||||
const mockGenerator: Partial<ContentGenerator> = {
|
||||
countTokens: mockCountTokens,
|
||||
};
|
||||
|
||||
// mock the model has been changed between calls of `countTokens`
|
||||
const firstCurrentModel = initialModel + '-changed-1';
|
||||
const secondCurrentModel = initialModel + '-changed-2';
|
||||
vi.spyOn(client['config'], 'getModel')
|
||||
.mockReturnValueOnce(firstCurrentModel)
|
||||
.mockReturnValueOnce(secondCurrentModel);
|
||||
|
||||
client['chat'] = mockChat as GeminiChat;
|
||||
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
||||
client['startChat'] = vi.fn().mockResolvedValue(mockChat);
|
||||
|
||||
const result = await client.tryCompressChat('prompt-id-4', true);
|
||||
|
||||
expect(mockCountTokens).toHaveBeenCalledTimes(2);
|
||||
expect(mockCountTokens).toHaveBeenNthCalledWith(1, {
|
||||
model: firstCurrentModel,
|
||||
contents: mockChatHistory,
|
||||
});
|
||||
expect(mockCountTokens).toHaveBeenNthCalledWith(2, {
|
||||
model: secondCurrentModel,
|
||||
contents: expect.any(Array),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
originalTokenCount: 100000,
|
||||
newTokenCount: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendMessageStream', () => {
|
||||
@@ -1866,6 +1889,35 @@ ${JSON.stringify(
|
||||
});
|
||||
|
||||
describe('generateContent', () => {
|
||||
it('should call generateContent with the correct parameters', async () => {
|
||||
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
||||
const generationConfig = { temperature: 0.5 };
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
// Mock countTokens
|
||||
const mockGenerator: Partial<ContentGenerator> = {
|
||||
countTokens: vi.fn().mockResolvedValue({ totalTokens: 1 }),
|
||||
generateContent: mockGenerateContentFn,
|
||||
};
|
||||
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
||||
|
||||
await client.generateContent(contents, generationConfig, abortSignal);
|
||||
|
||||
expect(mockGenerateContentFn).toHaveBeenCalledWith(
|
||||
{
|
||||
model: 'test-model',
|
||||
config: {
|
||||
abortSignal,
|
||||
systemInstruction: getCoreSystemPrompt(''),
|
||||
temperature: 0.5,
|
||||
topP: 1,
|
||||
},
|
||||
contents,
|
||||
},
|
||||
'test-session-id',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use current model from config for content generation', async () => {
|
||||
const initialModel = client['config'].getModel();
|
||||
const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
|
||||
@@ -1897,62 +1949,6 @@ ${JSON.stringify(
|
||||
});
|
||||
});
|
||||
|
||||
describe('tryCompressChat', () => {
|
||||
it('should use current model from config for token counting after sendMessage', async () => {
|
||||
const initialModel = client['config'].getModel();
|
||||
|
||||
const mockCountTokens = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ totalTokens: 100000 })
|
||||
.mockResolvedValueOnce({ totalTokens: 5000 });
|
||||
|
||||
const mockSendMessage = vi.fn().mockResolvedValue({ text: 'Summary' });
|
||||
|
||||
const mockChatHistory = [
|
||||
{ role: 'user', parts: [{ text: 'Long conversation' }] },
|
||||
{ role: 'model', parts: [{ text: 'Long response' }] },
|
||||
];
|
||||
|
||||
const mockChat: Partial<GeminiChat> = {
|
||||
getHistory: vi.fn().mockReturnValue(mockChatHistory),
|
||||
setHistory: vi.fn(),
|
||||
sendMessage: mockSendMessage,
|
||||
};
|
||||
|
||||
const mockGenerator: Partial<ContentGenerator> = {
|
||||
countTokens: mockCountTokens,
|
||||
};
|
||||
|
||||
// mock the model has been changed between calls of `countTokens`
|
||||
const firstCurrentModel = initialModel + '-changed-1';
|
||||
const secondCurrentModel = initialModel + '-changed-2';
|
||||
vi.spyOn(client['config'], 'getModel')
|
||||
.mockReturnValueOnce(firstCurrentModel)
|
||||
.mockReturnValueOnce(secondCurrentModel);
|
||||
|
||||
client['chat'] = mockChat as GeminiChat;
|
||||
client['contentGenerator'] = mockGenerator as ContentGenerator;
|
||||
client['startChat'] = vi.fn().mockResolvedValue(mockChat);
|
||||
|
||||
const result = await client.tryCompressChat('prompt-id-4', true);
|
||||
|
||||
expect(mockCountTokens).toHaveBeenCalledTimes(2);
|
||||
expect(mockCountTokens).toHaveBeenNthCalledWith(1, {
|
||||
model: firstCurrentModel,
|
||||
contents: mockChatHistory,
|
||||
});
|
||||
expect(mockCountTokens).toHaveBeenNthCalledWith(2, {
|
||||
model: secondCurrentModel,
|
||||
contents: expect.any(Array),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
originalTokenCount: 100000,
|
||||
newTokenCount: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleFlashFallback', () => {
|
||||
it('should use current model from config when checking for fallback', async () => {
|
||||
const initialModel = client['config'].getModel();
|
||||
|
||||
@@ -675,201 +675,6 @@ describe('memoryImportProcessor', () => {
|
||||
expect(result.content).toContain('A @./b.md');
|
||||
expect(result.content).toContain('B content');
|
||||
});
|
||||
|
||||
it('should build import tree structure', async () => {
|
||||
const content = 'Main content @./nested.md @./simple.md';
|
||||
const projectRoot = testPath('test', 'project');
|
||||
const basePath = testPath(projectRoot, 'src');
|
||||
const nestedContent = 'Nested @./inner.md content';
|
||||
const simpleContent = 'Simple content';
|
||||
const innerContent = 'Inner content';
|
||||
|
||||
mockedFs.access.mockResolvedValue(undefined);
|
||||
mockedFs.readFile
|
||||
.mockResolvedValueOnce(nestedContent)
|
||||
.mockResolvedValueOnce(simpleContent)
|
||||
.mockResolvedValueOnce(innerContent);
|
||||
|
||||
const result = await processImports(content, basePath, true);
|
||||
|
||||
// Use marked to find and validate import comments
|
||||
const comments = findMarkdownComments(result.content);
|
||||
const importComments = comments.filter((c) =>
|
||||
c.includes('Imported from:'),
|
||||
);
|
||||
|
||||
expect(importComments.some((c) => c.includes('./nested.md'))).toBe(true);
|
||||
expect(importComments.some((c) => c.includes('./simple.md'))).toBe(true);
|
||||
expect(importComments.some((c) => c.includes('./inner.md'))).toBe(true);
|
||||
|
||||
// Use marked to validate the markdown structure is well-formed
|
||||
const tokens = parseMarkdown(result.content);
|
||||
expect(tokens).toBeDefined();
|
||||
expect(tokens.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify the content contains expected text using marked parsing
|
||||
const textContent = tokens
|
||||
.filter((token) => token.type === 'paragraph')
|
||||
.map((token) => token.raw)
|
||||
.join(' ');
|
||||
|
||||
expect(textContent).toContain('Main content');
|
||||
expect(textContent).toContain('Nested');
|
||||
expect(textContent).toContain('Simple content');
|
||||
expect(textContent).toContain('Inner content');
|
||||
|
||||
// Verify import tree structure
|
||||
expect(result.importTree.path).toBe('unknown'); // No currentFile set in test
|
||||
expect(result.importTree.imports).toHaveLength(2);
|
||||
|
||||
// First import: nested.md
|
||||
const expectedNestedPath = testPath(projectRoot, 'src', 'nested.md');
|
||||
const expectedInnerPath = testPath(projectRoot, 'src', 'inner.md');
|
||||
const expectedSimplePath = testPath(projectRoot, 'src', 'simple.md');
|
||||
|
||||
// Check that the paths match using includes to handle potential absolute/relative differences
|
||||
expect(result.importTree.imports![0].path).toContain(expectedNestedPath);
|
||||
expect(result.importTree.imports![0].imports).toHaveLength(1);
|
||||
expect(result.importTree.imports![0].imports![0].path).toContain(
|
||||
expectedInnerPath,
|
||||
);
|
||||
expect(result.importTree.imports![0].imports![0].imports).toBeUndefined();
|
||||
|
||||
// Second import: simple.md
|
||||
expect(result.importTree.imports![1].path).toContain(expectedSimplePath);
|
||||
expect(result.importTree.imports![1].imports).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should produce flat output in Claude-style with unique files in order', async () => {
|
||||
const content = 'Main @./nested.md content @./simple.md';
|
||||
const projectRoot = testPath('test', 'project');
|
||||
const basePath = testPath(projectRoot, 'src');
|
||||
const nestedContent = 'Nested @./inner.md content';
|
||||
const simpleContent = 'Simple content';
|
||||
const innerContent = 'Inner content';
|
||||
|
||||
mockedFs.access.mockResolvedValue(undefined);
|
||||
mockedFs.readFile
|
||||
.mockResolvedValueOnce(nestedContent)
|
||||
.mockResolvedValueOnce(simpleContent)
|
||||
.mockResolvedValueOnce(innerContent);
|
||||
|
||||
const result = await processImports(
|
||||
content,
|
||||
basePath,
|
||||
true,
|
||||
undefined,
|
||||
projectRoot,
|
||||
'flat',
|
||||
);
|
||||
|
||||
// Verify all expected files are present by checking for their basenames
|
||||
expect(result.content).toContain('nested.md');
|
||||
expect(result.content).toContain('simple.md');
|
||||
expect(result.content).toContain('inner.md');
|
||||
|
||||
// Verify content is present
|
||||
expect(result.content).toContain('Nested @./inner.md content');
|
||||
expect(result.content).toContain('Simple content');
|
||||
expect(result.content).toContain('Inner content');
|
||||
});
|
||||
|
||||
it('should not duplicate files in flat output if imported multiple times', async () => {
|
||||
const content = 'Main @./dup.md again @./dup.md';
|
||||
const projectRoot = testPath('test', 'project');
|
||||
const basePath = testPath(projectRoot, 'src');
|
||||
const dupContent = 'Duplicated content';
|
||||
|
||||
// Create a normalized path for the duplicate file
|
||||
const dupFilePath = path.normalize(path.join(basePath, 'dup.md'));
|
||||
|
||||
// Mock the file system access
|
||||
mockedFs.access.mockImplementation((filePath) => {
|
||||
const pathStr = filePath.toString();
|
||||
if (path.normalize(pathStr) === dupFilePath) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(`File not found: ${pathStr}`));
|
||||
});
|
||||
|
||||
// Mock the file reading
|
||||
mockedFs.readFile.mockImplementation((filePath) => {
|
||||
const pathStr = filePath.toString();
|
||||
if (path.normalize(pathStr) === dupFilePath) {
|
||||
return Promise.resolve(dupContent);
|
||||
}
|
||||
return Promise.reject(new Error(`File not found: ${pathStr}`));
|
||||
});
|
||||
|
||||
const result = await processImports(
|
||||
content,
|
||||
basePath,
|
||||
true, // debugMode
|
||||
undefined, // importState
|
||||
projectRoot,
|
||||
'flat',
|
||||
);
|
||||
|
||||
// In flat mode, the output should only contain the main file content with import markers
|
||||
// The imported file content should not be included in the flat output
|
||||
expect(result.content).toContain('Main @./dup.md again @./dup.md');
|
||||
|
||||
// The imported file content should not appear in the output
|
||||
// This is the current behavior of the implementation
|
||||
expect(result.content).not.toContain(dupContent);
|
||||
|
||||
// The file marker should not appear in the output
|
||||
// since the imported file content is not included in flat mode
|
||||
const fileMarker = `--- File: ${dupFilePath} ---`;
|
||||
expect(result.content).not.toContain(fileMarker);
|
||||
expect(result.content).not.toContain('--- End of File: ' + dupFilePath);
|
||||
|
||||
// The main file path should be in the output
|
||||
// Since we didn't pass an importState, it will use the basePath as the file path
|
||||
const mainFilePath = path.normalize(path.resolve(basePath));
|
||||
expect(result.content).toContain(`--- File: ${mainFilePath} ---`);
|
||||
expect(result.content).toContain(`--- End of File: ${mainFilePath}`);
|
||||
});
|
||||
|
||||
it('should handle nested imports in flat output', async () => {
|
||||
const content = 'Root @./a.md';
|
||||
const projectRoot = testPath('test', 'project');
|
||||
const basePath = testPath(projectRoot, 'src');
|
||||
const aContent = 'A @./b.md';
|
||||
const bContent = 'B content';
|
||||
|
||||
mockedFs.access.mockResolvedValue(undefined);
|
||||
mockedFs.readFile
|
||||
.mockResolvedValueOnce(aContent)
|
||||
.mockResolvedValueOnce(bContent);
|
||||
|
||||
const result = await processImports(
|
||||
content,
|
||||
basePath,
|
||||
true,
|
||||
undefined,
|
||||
projectRoot,
|
||||
'flat',
|
||||
);
|
||||
|
||||
// Verify all files are present by checking for their basenames
|
||||
expect(result.content).toContain('a.md');
|
||||
expect(result.content).toContain('b.md');
|
||||
|
||||
// Verify content is in the correct order
|
||||
const contentStr = result.content;
|
||||
const aIndex = contentStr.indexOf('a.md');
|
||||
const bIndex = contentStr.indexOf('b.md');
|
||||
const rootIndex = contentStr.indexOf('Root @./a.md');
|
||||
|
||||
expect(rootIndex).toBeLessThan(aIndex);
|
||||
expect(aIndex).toBeLessThan(bIndex);
|
||||
|
||||
// Verify content is present
|
||||
expect(result.content).toContain('Root @./a.md');
|
||||
expect(result.content).toContain('A @./b.md');
|
||||
expect(result.content).toContain('B content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateImportPath', () => {
|
||||
|
||||
@@ -82,6 +82,7 @@ describe('retryWithBackoff', () => {
|
||||
// 2. IMPORTANT: Attach the rejection expectation to the promise *immediately*.
|
||||
// This ensures a 'catch' handler is present before the promise can reject.
|
||||
// The result is a new promise that resolves when the assertion is met.
|
||||
// eslint-disable-next-line vitest/valid-expect
|
||||
const assertionPromise = expect(promise).rejects.toThrow(
|
||||
'Simulated error attempt 3',
|
||||
);
|
||||
@@ -126,7 +127,7 @@ describe('retryWithBackoff', () => {
|
||||
|
||||
// Attach the rejection expectation *before* running timers
|
||||
const assertionPromise =
|
||||
expect(promise).rejects.toThrow('Too Many Requests');
|
||||
expect(promise).rejects.toThrow('Too Many Requests'); // eslint-disable-line vitest/valid-expect
|
||||
|
||||
// Run timers to trigger retries and eventual rejection
|
||||
await vi.runAllTimersAsync();
|
||||
@@ -194,6 +195,7 @@ describe('retryWithBackoff', () => {
|
||||
// We expect rejections as mockFn fails 5 times
|
||||
const promise1 = runRetry();
|
||||
// Attach the rejection expectation *before* running timers
|
||||
// eslint-disable-next-line vitest/valid-expect
|
||||
const assertionPromise1 = expect(promise1).rejects.toThrow();
|
||||
await vi.runAllTimersAsync(); // Advance for the delay in the first runRetry
|
||||
await assertionPromise1;
|
||||
@@ -208,6 +210,7 @@ describe('retryWithBackoff', () => {
|
||||
|
||||
const promise2 = runRetry();
|
||||
// Attach the rejection expectation *before* running timers
|
||||
// eslint-disable-next-line vitest/valid-expect
|
||||
const assertionPromise2 = expect(promise2).rejects.toThrow();
|
||||
await vi.runAllTimersAsync(); // Advance for the delay in the second runRetry
|
||||
await assertionPromise2;
|
||||
|
||||
Reference in New Issue
Block a user