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:
@@ -10,6 +10,7 @@ import reactPlugin from 'eslint-plugin-react';
|
|||||||
import reactHooks from 'eslint-plugin-react-hooks';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import prettierConfig from 'eslint-config-prettier';
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
import importPlugin from 'eslint-plugin-import';
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
import vitest from '@vitest/eslint-plugin';
|
||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
import licenseHeader from 'eslint-plugin-license-header';
|
import licenseHeader from 'eslint-plugin-license-header';
|
||||||
import path from 'node:path'; // Use node: prefix for built-ins
|
import path from 'node:path'; // Use node: prefix for built-ins
|
||||||
@@ -159,6 +160,17 @@ export default tseslint.config(
|
|||||||
'default-case': 'error',
|
'default-case': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['packages/*/src/**/*.test.{ts,tsx}'],
|
||||||
|
plugins: {
|
||||||
|
vitest,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...vitest.configs.recommended.rules,
|
||||||
|
'vitest/expect-expect': 'off',
|
||||||
|
'vitest/no-commented-out-tests': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ['./**/*.{tsx,ts,js}'],
|
files: ['./**/*.{tsx,ts,js}'],
|
||||||
plugins: {
|
plugins: {
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"@types/mock-fs": "^4.13.4",
|
"@types/mock-fs": "^4.13.4",
|
||||||
"@types/shell-quote": "^1.7.5",
|
"@types/shell-quote": "^1.7.5",
|
||||||
"@vitest/coverage-v8": "^3.1.1",
|
"@vitest/coverage-v8": "^3.1.1",
|
||||||
|
"@vitest/eslint-plugin": "^1.3.4",
|
||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
@@ -3386,6 +3387,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/eslint-plugin": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-EOg8d0jn3BAiKnR55WkFxmxfWA3nmzrbIIuOXyTe6A72duryNgyU+bdBEauA97Aab3ho9kLmAwgPX63Ckj4QEg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/utils": "^8.24.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": ">= 8.57.0",
|
||||||
|
"typescript": ">= 5.0.0",
|
||||||
|
"vitest": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vitest": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
"@types/mock-fs": "^4.13.4",
|
"@types/mock-fs": "^4.13.4",
|
||||||
"@types/shell-quote": "^1.7.5",
|
"@types/shell-quote": "^1.7.5",
|
||||||
"@vitest/coverage-v8": "^3.1.1",
|
"@vitest/coverage-v8": "^3.1.1",
|
||||||
|
"@vitest/eslint-plugin": "^1.3.4",
|
||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
|
|||||||
@@ -720,19 +720,23 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
|||||||
// readability, and content based on paths derived from the mocked os.homedir().
|
// readability, and content based on paths derived from the mocked os.homedir().
|
||||||
// 3. Spies on console functions (for logger output) are correctly set up if needed.
|
// 3. Spies on console functions (for logger output) are correctly set up if needed.
|
||||||
// Example of a previously failing test structure:
|
// Example of a previously failing test structure:
|
||||||
/*
|
it.skip('should correctly use mocked homedir for global path', async () => {
|
||||||
it('should correctly use mocked homedir for global path', async () => {
|
|
||||||
const MOCK_GEMINI_DIR_LOCAL = path.join('/mock/home/user', '.gemini');
|
const MOCK_GEMINI_DIR_LOCAL = path.join('/mock/home/user', '.gemini');
|
||||||
const MOCK_GLOBAL_PATH_LOCAL = path.join(MOCK_GEMINI_DIR_LOCAL, 'GEMINI.md');
|
const MOCK_GLOBAL_PATH_LOCAL = path.join(
|
||||||
|
MOCK_GEMINI_DIR_LOCAL,
|
||||||
|
'GEMINI.md',
|
||||||
|
);
|
||||||
mockFs({
|
mockFs({
|
||||||
[MOCK_GLOBAL_PATH_LOCAL]: { type: 'file', content: 'GlobalContentOnly' }
|
[MOCK_GLOBAL_PATH_LOCAL]: { type: 'file', content: 'GlobalContentOnly' },
|
||||||
});
|
});
|
||||||
const memory = await loadHierarchicalGeminiMemory("/some/other/cwd", false);
|
const memory = await loadHierarchicalGeminiMemory('/some/other/cwd', false);
|
||||||
expect(memory).toBe('GlobalContentOnly');
|
expect(memory).toBe('GlobalContentOnly');
|
||||||
expect(vi.mocked(os.homedir)).toHaveBeenCalled();
|
expect(vi.mocked(os.homedir)).toHaveBeenCalled();
|
||||||
expect(fsPromises.readFile).toHaveBeenCalledWith(MOCK_GLOBAL_PATH_LOCAL, 'utf-8');
|
expect(fsPromises.readFile).toHaveBeenCalledWith(
|
||||||
|
MOCK_GLOBAL_PATH_LOCAL,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mergeMcpServers', () => {
|
describe('mergeMcpServers', () => {
|
||||||
|
|||||||
@@ -1464,6 +1464,64 @@ describe('useGeminiStream', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should process @include commands, adding user turn after processing to prevent race conditions', async () => {
|
||||||
|
const rawQuery = '@include file.txt Summarize this.';
|
||||||
|
const processedQueryParts = [
|
||||||
|
{ text: 'Summarize this with content from @file.txt' },
|
||||||
|
{ text: 'File content...' },
|
||||||
|
];
|
||||||
|
const userMessageTimestamp = Date.now();
|
||||||
|
vi.spyOn(Date, 'now').mockReturnValue(userMessageTimestamp);
|
||||||
|
|
||||||
|
handleAtCommandSpy.mockResolvedValue({
|
||||||
|
processedQuery: processedQueryParts,
|
||||||
|
shouldProceed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGeminiStream(
|
||||||
|
mockConfig.getGeminiClient() as GeminiClient,
|
||||||
|
[],
|
||||||
|
mockAddItem,
|
||||||
|
mockConfig,
|
||||||
|
mockOnDebugMessage,
|
||||||
|
mockHandleSlashCommand,
|
||||||
|
false, // shellModeActive
|
||||||
|
vi.fn(), // getPreferredEditor
|
||||||
|
vi.fn(), // onAuthError
|
||||||
|
vi.fn(), // performMemoryRefresh
|
||||||
|
false, // modelSwitched
|
||||||
|
vi.fn(), // setModelSwitched
|
||||||
|
vi.fn(), // onEditorClose
|
||||||
|
vi.fn(), // onCancelSubmit
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.submitQuery(rawQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleAtCommandSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
query: rawQuery,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockAddItem).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
type: MessageType.USER,
|
||||||
|
text: rawQuery,
|
||||||
|
},
|
||||||
|
userMessageTimestamp,
|
||||||
|
);
|
||||||
|
|
||||||
|
// FIX: The expectation now matches the actual call signature.
|
||||||
|
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
||||||
|
processedQueryParts, // Argument 1: The parts array directly
|
||||||
|
expect.any(AbortSignal), // Argument 2: An AbortSignal
|
||||||
|
expect.any(String), // Argument 3: The prompt_id string
|
||||||
|
);
|
||||||
|
});
|
||||||
describe('Thought Reset', () => {
|
describe('Thought Reset', () => {
|
||||||
it('should reset thought to null when starting a new prompt', async () => {
|
it('should reset thought to null when starting a new prompt', async () => {
|
||||||
// First, simulate a response with a thought
|
// First, simulate a response with a thought
|
||||||
@@ -1660,301 +1718,4 @@ describe('useGeminiStream', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process @include commands, adding user turn after processing to prevent race conditions', async () => {
|
|
||||||
const rawQuery = '@include file.txt Summarize this.';
|
|
||||||
const processedQueryParts = [
|
|
||||||
{ text: 'Summarize this with content from @file.txt' },
|
|
||||||
{ text: 'File content...' },
|
|
||||||
];
|
|
||||||
const userMessageTimestamp = Date.now();
|
|
||||||
vi.spyOn(Date, 'now').mockReturnValue(userMessageTimestamp);
|
|
||||||
|
|
||||||
handleAtCommandSpy.mockResolvedValue({
|
|
||||||
processedQuery: processedQueryParts,
|
|
||||||
shouldProceed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useGeminiStream(
|
|
||||||
mockConfig.getGeminiClient() as GeminiClient,
|
|
||||||
[],
|
|
||||||
mockAddItem,
|
|
||||||
mockConfig,
|
|
||||||
mockOnDebugMessage,
|
|
||||||
mockHandleSlashCommand,
|
|
||||||
false, // shellModeActive
|
|
||||||
vi.fn(), // getPreferredEditor
|
|
||||||
vi.fn(), // onAuthError
|
|
||||||
vi.fn(), // performMemoryRefresh
|
|
||||||
false, // modelSwitched
|
|
||||||
vi.fn(), // setModelSwitched
|
|
||||||
vi.fn(), // onEditorClose
|
|
||||||
vi.fn(), // onCancelSubmit
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitQuery(rawQuery);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleAtCommandSpy).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
query: rawQuery,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockAddItem).toHaveBeenCalledWith(
|
|
||||||
{
|
|
||||||
type: MessageType.USER,
|
|
||||||
text: rawQuery,
|
|
||||||
},
|
|
||||||
userMessageTimestamp,
|
|
||||||
);
|
|
||||||
|
|
||||||
// FIX: The expectation now matches the actual call signature.
|
|
||||||
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
|
||||||
processedQueryParts, // Argument 1: The parts array directly
|
|
||||||
expect.any(AbortSignal), // Argument 2: An AbortSignal
|
|
||||||
expect.any(String), // Argument 3: The prompt_id string
|
|
||||||
);
|
|
||||||
});
|
|
||||||
describe('Thought Reset', () => {
|
|
||||||
it('should reset thought to null when starting a new prompt', async () => {
|
|
||||||
mockSendMessageStream.mockReturnValue(
|
|
||||||
(async function* () {
|
|
||||||
yield {
|
|
||||||
type: ServerGeminiEventType.Thought,
|
|
||||||
value: {
|
|
||||||
subject: 'Previous thought',
|
|
||||||
description: 'Old description',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
yield {
|
|
||||||
type: ServerGeminiEventType.Content,
|
|
||||||
value: 'Some response content',
|
|
||||||
};
|
|
||||||
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useGeminiStream(
|
|
||||||
new MockedGeminiClientClass(mockConfig),
|
|
||||||
[],
|
|
||||||
mockAddItem,
|
|
||||||
mockConfig,
|
|
||||||
mockOnDebugMessage,
|
|
||||||
mockHandleSlashCommand,
|
|
||||||
false,
|
|
||||||
() => 'vscode' as EditorType,
|
|
||||||
() => {},
|
|
||||||
() => Promise.resolve(),
|
|
||||||
false,
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitQuery('First query');
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockAddItem).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'gemini',
|
|
||||||
text: 'Some response content',
|
|
||||||
}),
|
|
||||||
expect.any(Number),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
mockSendMessageStream.mockReturnValue(
|
|
||||||
(async function* () {
|
|
||||||
yield {
|
|
||||||
type: ServerGeminiEventType.Content,
|
|
||||||
value: 'New response content',
|
|
||||||
};
|
|
||||||
yield { type: ServerGeminiEventType.Finished, value: 'STOP' };
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitQuery('Second query');
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockAddItem).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'gemini',
|
|
||||||
text: 'New response content',
|
|
||||||
}),
|
|
||||||
expect.any(Number),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset thought to null when user cancels', async () => {
|
|
||||||
mockSendMessageStream.mockReturnValue(
|
|
||||||
(async function* () {
|
|
||||||
yield {
|
|
||||||
type: ServerGeminiEventType.Thought,
|
|
||||||
value: { subject: 'Some thought', description: 'Description' },
|
|
||||||
};
|
|
||||||
yield { type: ServerGeminiEventType.UserCancelled };
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useGeminiStream(
|
|
||||||
new MockedGeminiClientClass(mockConfig),
|
|
||||||
[],
|
|
||||||
mockAddItem,
|
|
||||||
mockConfig,
|
|
||||||
mockOnDebugMessage,
|
|
||||||
mockHandleSlashCommand,
|
|
||||||
false,
|
|
||||||
() => 'vscode' as EditorType,
|
|
||||||
() => {},
|
|
||||||
() => Promise.resolve(),
|
|
||||||
false,
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitQuery('Test query');
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockAddItem).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'info',
|
|
||||||
text: 'User cancelled the request.',
|
|
||||||
}),
|
|
||||||
expect.any(Number),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset thought to null when there is an error', async () => {
|
|
||||||
mockSendMessageStream.mockReturnValue(
|
|
||||||
(async function* () {
|
|
||||||
yield {
|
|
||||||
type: ServerGeminiEventType.Thought,
|
|
||||||
value: { subject: 'Some thought', description: 'Description' },
|
|
||||||
};
|
|
||||||
yield {
|
|
||||||
type: ServerGeminiEventType.Error,
|
|
||||||
value: { error: { message: 'Test error' } },
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useGeminiStream(
|
|
||||||
new MockedGeminiClientClass(mockConfig),
|
|
||||||
[],
|
|
||||||
mockAddItem,
|
|
||||||
mockConfig,
|
|
||||||
mockOnDebugMessage,
|
|
||||||
mockHandleSlashCommand,
|
|
||||||
false,
|
|
||||||
() => 'vscode' as EditorType,
|
|
||||||
() => {},
|
|
||||||
() => Promise.resolve(),
|
|
||||||
false,
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
() => {},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitQuery('Test query');
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockAddItem).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'error',
|
|
||||||
}),
|
|
||||||
expect.any(Number),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockParseAndFormatApiError).toHaveBeenCalledWith(
|
|
||||||
{ message: 'Test error' },
|
|
||||||
expect.any(String),
|
|
||||||
undefined,
|
|
||||||
'gemini-2.5-pro',
|
|
||||||
'gemini-2.5-flash',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should process @include commands, adding user turn after processing to prevent race conditions', async () => {
|
|
||||||
const rawQuery = '@include file.txt Summarize this.';
|
|
||||||
const processedQueryParts = [
|
|
||||||
{ text: 'Summarize this with content from @file.txt' },
|
|
||||||
{ text: 'File content...' },
|
|
||||||
];
|
|
||||||
const userMessageTimestamp = Date.now();
|
|
||||||
vi.spyOn(Date, 'now').mockReturnValue(userMessageTimestamp);
|
|
||||||
|
|
||||||
handleAtCommandSpy.mockResolvedValue({
|
|
||||||
processedQuery: processedQueryParts,
|
|
||||||
shouldProceed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useGeminiStream(
|
|
||||||
mockConfig.getGeminiClient() as GeminiClient,
|
|
||||||
[],
|
|
||||||
mockAddItem,
|
|
||||||
mockConfig,
|
|
||||||
mockOnDebugMessage,
|
|
||||||
mockHandleSlashCommand,
|
|
||||||
false,
|
|
||||||
vi.fn(),
|
|
||||||
vi.fn(),
|
|
||||||
vi.fn(),
|
|
||||||
false,
|
|
||||||
vi.fn(),
|
|
||||||
vi.fn(),
|
|
||||||
vi.fn(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitQuery(rawQuery);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleAtCommandSpy).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
query: rawQuery,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockAddItem).toHaveBeenCalledWith(
|
|
||||||
{
|
|
||||||
type: MessageType.USER,
|
|
||||||
text: rawQuery,
|
|
||||||
},
|
|
||||||
userMessageTimestamp,
|
|
||||||
);
|
|
||||||
|
|
||||||
// FIX: This expectation now correctly matches the actual function call signature.
|
|
||||||
expect(mockSendMessageStream).toHaveBeenCalledWith(
|
|
||||||
processedQueryParts, // Argument 1: The parts array directly
|
|
||||||
expect.any(AbortSignal), // Argument 2: An AbortSignal
|
|
||||||
expect.any(String), // Argument 3: The prompt_id string
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ describe('getLatestRelease', async () => {
|
|||||||
|
|
||||||
it('throws an error if the fetch fails', async () => {
|
it('throws an error if the fetch fails', async () => {
|
||||||
global.fetch = vi.fn(() => Promise.reject('nope'));
|
global.fetch = vi.fn(() => Promise.reject('nope'));
|
||||||
expect(getLatestGitHubRelease()).rejects.toThrowError(
|
await expect(getLatestGitHubRelease()).rejects.toThrowError(
|
||||||
/Unable to determine the latest/,
|
/Unable to determine the latest/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -132,7 +132,7 @@ describe('getLatestRelease', async () => {
|
|||||||
json: () => Promise.resolve({ foo: 'bar' }),
|
json: () => Promise.resolve({ foo: 'bar' }),
|
||||||
} as Response),
|
} as Response),
|
||||||
);
|
);
|
||||||
expect(getLatestGitHubRelease()).rejects.toThrowError(
|
await expect(getLatestGitHubRelease()).rejects.toThrowError(
|
||||||
/Unable to determine the latest/,
|
/Unable to determine the latest/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -144,6 +144,6 @@ describe('getLatestRelease', async () => {
|
|||||||
json: () => Promise.resolve({ tag_name: 'v1.2.3' }),
|
json: () => Promise.resolve({ tag_name: 'v1.2.3' }),
|
||||||
} as Response),
|
} as Response),
|
||||||
);
|
);
|
||||||
expect(getLatestGitHubRelease()).resolves.toBe('v1.2.3');
|
await expect(getLatestGitHubRelease()).resolves.toBe('v1.2.3');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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', () => {
|
describe('generateJson', () => {
|
||||||
it('should call generateContent with the correct parameters', async () => {
|
it('should call generateContent with the correct parameters', async () => {
|
||||||
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
const contents = [{ role: 'user', parts: [{ text: 'hello' }] }];
|
||||||
@@ -705,6 +674,60 @@ describe('Gemini Client (client.ts)', () => {
|
|||||||
// Assert that the chat was reset
|
// Assert that the chat was reset
|
||||||
expect(newChat).not.toBe(initialChat);
|
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', () => {
|
describe('sendMessageStream', () => {
|
||||||
@@ -1866,6 +1889,35 @@ ${JSON.stringify(
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('generateContent', () => {
|
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 () => {
|
it('should use current model from config for content generation', async () => {
|
||||||
const initialModel = client['config'].getModel();
|
const initialModel = client['config'].getModel();
|
||||||
const contents = [{ role: 'user', parts: [{ text: 'test' }] }];
|
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', () => {
|
describe('handleFlashFallback', () => {
|
||||||
it('should use current model from config when checking for fallback', async () => {
|
it('should use current model from config when checking for fallback', async () => {
|
||||||
const initialModel = client['config'].getModel();
|
const initialModel = client['config'].getModel();
|
||||||
|
|||||||
@@ -675,201 +675,6 @@ describe('memoryImportProcessor', () => {
|
|||||||
expect(result.content).toContain('A @./b.md');
|
expect(result.content).toContain('A @./b.md');
|
||||||
expect(result.content).toContain('B content');
|
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', () => {
|
describe('validateImportPath', () => {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ describe('retryWithBackoff', () => {
|
|||||||
// 2. IMPORTANT: Attach the rejection expectation to the promise *immediately*.
|
// 2. IMPORTANT: Attach the rejection expectation to the promise *immediately*.
|
||||||
// This ensures a 'catch' handler is present before the promise can reject.
|
// 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.
|
// 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(
|
const assertionPromise = expect(promise).rejects.toThrow(
|
||||||
'Simulated error attempt 3',
|
'Simulated error attempt 3',
|
||||||
);
|
);
|
||||||
@@ -126,7 +127,7 @@ describe('retryWithBackoff', () => {
|
|||||||
|
|
||||||
// Attach the rejection expectation *before* running timers
|
// Attach the rejection expectation *before* running timers
|
||||||
const assertionPromise =
|
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
|
// Run timers to trigger retries and eventual rejection
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
@@ -194,6 +195,7 @@ describe('retryWithBackoff', () => {
|
|||||||
// We expect rejections as mockFn fails 5 times
|
// We expect rejections as mockFn fails 5 times
|
||||||
const promise1 = runRetry();
|
const promise1 = runRetry();
|
||||||
// Attach the rejection expectation *before* running timers
|
// Attach the rejection expectation *before* running timers
|
||||||
|
// eslint-disable-next-line vitest/valid-expect
|
||||||
const assertionPromise1 = expect(promise1).rejects.toThrow();
|
const assertionPromise1 = expect(promise1).rejects.toThrow();
|
||||||
await vi.runAllTimersAsync(); // Advance for the delay in the first runRetry
|
await vi.runAllTimersAsync(); // Advance for the delay in the first runRetry
|
||||||
await assertionPromise1;
|
await assertionPromise1;
|
||||||
@@ -208,6 +210,7 @@ describe('retryWithBackoff', () => {
|
|||||||
|
|
||||||
const promise2 = runRetry();
|
const promise2 = runRetry();
|
||||||
// Attach the rejection expectation *before* running timers
|
// Attach the rejection expectation *before* running timers
|
||||||
|
// eslint-disable-next-line vitest/valid-expect
|
||||||
const assertionPromise2 = expect(promise2).rejects.toThrow();
|
const assertionPromise2 = expect(promise2).rejects.toThrow();
|
||||||
await vi.runAllTimersAsync(); // Advance for the delay in the second runRetry
|
await vi.runAllTimersAsync(); // Advance for the delay in the second runRetry
|
||||||
await assertionPromise2;
|
await assertionPromise2;
|
||||||
|
|||||||
Reference in New Issue
Block a user