import { describe, it, expect, beforeEach, vi } from 'vitest'; import { BatchExecutor, ExecutorState } from '../../src/core/executor'; import { FileItem, PlatformAdapter, RenameResult } from '../../src/types/platform'; import { RuleConfig } from '../../src/types/rule'; // Mock adapter class MockAdapter implements PlatformAdapter { readonly platform = 'quark' as const; renameDelay = 100; // 模拟API延迟 failureRate = 0; // 失败率(0-1) async getSelectedFiles(): Promise { return []; } async renameFile(fileId: string, newName: string): Promise { // 模拟延迟 await new Promise((resolve) => setTimeout(resolve, this.renameDelay)); // 模拟失败 if (Math.random() < this.failureRate) { return { success: false, error: new Error('Mock rename failed'), }; } return { success: true, newName, }; } async checkNameConflict(fileName: string, parentId: string): Promise { return false; } async getFileInfo(fileId: string): Promise { return { id: fileId, name: 'test.txt', ext: '.txt', parentId: '0', size: 1024, mtime: Date.now(), }; } getConfig() { return { platform: 'quark' as const, requestInterval: 800, maxRetries: 3, }; } } // Mock rule const mockRule: RuleConfig = { type: 'prefix', params: { prefix: 'test_', }, }; // Mock RuleFactory vi.mock('../../src/rules/rule-factory', () => ({ RuleFactory: { create: () => ({ execute: (fileName: string, index: number) => `test_${fileName}`, }), }, })); describe('BatchExecutor', () => { let adapter: MockAdapter; let files: FileItem[]; beforeEach(() => { adapter = new MockAdapter(); files = [ { id: '1', name: 'file1.txt', ext: '.txt', parentId: '0', size: 1024, mtime: Date.now(), }, { id: '2', name: 'file2.txt', ext: '.txt', parentId: '0', size: 2048, mtime: Date.now(), }, { id: '3', name: 'file3.txt', ext: '.txt', parentId: '0', size: 3072, mtime: Date.now(), }, ]; }); describe('构造函数', () => { it('应该正确初始化', () => { const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 800, }); expect(executor.getState()).toBe(ExecutorState.IDLE); }); it('空文件列表应该抛出错误', () => { expect(() => { new BatchExecutor([], mockRule, adapter, { requestInterval: 800, }); }).toThrow('Files array cannot be empty'); }); it('缺少规则应该抛出错误', () => { expect(() => { new BatchExecutor(files, null as any, adapter, { requestInterval: 800, }); }).toThrow('Rule configuration is required'); }); }); describe('execute', () => { it('应该成功执行批量重命名', async () => { const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 100, // 使用较短的间隔加快测试 }); const results = await executor.execute(); expect(results.success.length).toBe(3); expect(results.failed.length).toBe(0); expect(executor.getState()).toBe(ExecutorState.COMPLETED); }); it('应该控制请求间隔', async () => { const startTime = Date.now(); const requestInterval = 200; const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval, }); await executor.execute(); const elapsed = Date.now() - startTime; const expectedMin = (files.length - 1) * requestInterval; // 实际时间应该大于预期最小时间 expect(elapsed).toBeGreaterThanOrEqual(expectedMin); }); it('应该触发进度回调', async () => { const progressEvents: any[] = []; const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 100, onProgress: (progress) => { progressEvents.push(progress); }, }); await executor.execute(); expect(progressEvents.length).toBe(files.length); expect(progressEvents[0].completed).toBe(1); expect(progressEvents[2].completed).toBe(3); }); it('应该触发完成回调', async () => { let completedResults: any = null; const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 100, onComplete: (results) => { completedResults = results; }, }); await executor.execute(); expect(completedResults).not.toBeNull(); expect(completedResults.success.length).toBe(3); }); it('应该处理部分失败', async () => { adapter.failureRate = 0.5; // 50%失败率 const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 100, }); const results = await executor.execute(); expect(results.success.length + results.failed.length).toBe(files.length); }); it('should increase adaptive interval factor when backoff error occurs', async () => { const backoffAdapter = new MockAdapter(); let called = 0; backoffAdapter.renameFile = async () => { called++; if (called === 1) { return { success: false, error: new Error('TooManyRequests') } as any; } return { success: true, newName: 'ok' } as any; }; const executor = new BatchExecutor(files, mockRule, backoffAdapter, { requestInterval: 50, }); await executor.execute(); expect(executor.getAdaptiveIntervalFactor()).toBeGreaterThan(1); }); it('应该限制最大并发数', async () => { // 让单次请求更慢,便于观测并发峰值 adapter.renameDelay = 200; let inFlight = 0; let maxInFlight = 0; const originalRename = adapter.renameFile.bind(adapter); adapter.renameFile = async (fileId: string, newName: string) => { inFlight++; maxInFlight = Math.max(maxInFlight, inFlight); try { return await originalRename(fileId, newName); } finally { inFlight--; } }; const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 0, maxConcurrent: 2, }); await executor.execute(); expect(maxInFlight).toBeLessThanOrEqual(2); }); }); describe('pause/resume', () => { it('应该支持暂停和恢复', async () => { const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 100, }); // 启动执行 const executePromise = executor.execute(); // 等待一段时间后暂停 await new Promise((resolve) => setTimeout(resolve, 150)); executor.pause(); expect(executor.getState()).toBe(ExecutorState.PAUSED); // 等待一段时间后恢复 await new Promise((resolve) => setTimeout(resolve, 200)); executor.resume(); expect(executor.getState()).toBe(ExecutorState.RUNNING); // 等待完成 await executePromise; expect(executor.getState()).toBe(ExecutorState.COMPLETED); }); }); describe('cancel', () => { it('应该支持取消', async () => { const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 100, }); // 启动执行 const executePromise = executor.execute(); // 等待一段时间后取消 await new Promise((resolve) => setTimeout(resolve, 150)); executor.cancel(); expect(executor.getState()).toBe(ExecutorState.CANCELLED); // 等待完成 await executePromise; const results = executor.getResults(); // 取消后不是所有文件都会完成 expect(results.success.length + results.failed.length).toBeLessThan(files.length); }); }); describe('getStatistics', () => { it('应该返回正确的统计信息', async () => { const executor = new BatchExecutor(files, mockRule, adapter, { requestInterval: 100, }); await executor.execute(); const stats = executor.getStatistics(); expect(stats.completed).toBe(files.length); expect(stats.total).toBe(files.length); expect(stats.percentage).toBe(100); expect(stats.elapsed).toBeGreaterThan(0); }); }); });