feat: add execution plan helper

这个提交包含在:
Zain 2026-02-18 22:35:30 +08:00
父节点 6d940e607e
当前提交 f3a89f28e6
共有 2 个文件被更改,包括 115 次插入 和 0 次删除

75
src/core/execution-plan.ts 普通文件
查看文件

@ -0,0 +1,75 @@
import { FileItem } from '../types/platform';
import { Task } from '../types/core';
import {
ConflictResolution,
ConflictResult,
resolveBatchConflicts,
} from './conflict-detector';
export interface ExecutionPlanInput {
files: FileItem[];
newNames: string[];
conflicts?: Map<string, ConflictResult>;
resolution?: ConflictResolution | null;
skipUnchanged?: boolean;
}
export interface ExecutionPlanResult {
resolvedNames: string[];
tasks: Task[];
skippedIndexes: number[];
}
export function buildExecutionPlan({
files,
newNames,
conflicts,
resolution,
skipUnchanged = true,
}: ExecutionPlanInput): ExecutionPlanResult {
if (files.length !== newNames.length) {
throw new Error('Files and newNames length mismatch');
}
const conflictMap = conflicts ?? new Map<string, ConflictResult>();
let resolvedNames = newNames.slice();
if (resolution && conflictMap.size > 0) {
switch (resolution) {
case ConflictResolution.AUTO_NUMBER:
resolvedNames = resolveBatchConflicts(files, newNames, conflictMap);
break;
case ConflictResolution.SKIP:
resolvedNames = newNames.map((name, index) => {
const file = files[index];
const conflict = conflictMap.get(file.id);
return conflict?.hasConflict ? file.name : name;
});
break;
case ConflictResolution.OVERWRITE:
default:
resolvedNames = newNames.slice();
break;
}
}
const tasks: Task[] = [];
const skippedIndexes: number[] = [];
resolvedNames.forEach((resolvedName, index) => {
const file = files[index];
if (skipUnchanged && resolvedName === file.name) {
skippedIndexes.push(index);
return;
}
tasks.push({
file,
newName: resolvedName,
index,
});
});
return { resolvedNames, tasks, skippedIndexes };
}

查看文件

@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest';
import { buildExecutionPlan } from '../../src/core/execution-plan';
import { checkBatchConflicts, ConflictResolution } from '../../src/core/conflict-detector';
const files = [
{ id: '1', name: 'a.txt', ext: '.txt', parentId: '0', size: 1, mtime: 1 },
{ id: '2', name: 'b.txt', ext: '.txt', parentId: '0', size: 1, mtime: 1 },
];
describe('buildExecutionPlan', () => {
it('auto-number resolves duplicate names and skips unchanged', () => {
const newNames = ['same.txt', 'same.txt'];
const conflicts = checkBatchConflicts(files as any, newNames);
const plan = buildExecutionPlan({
files: files as any,
newNames,
conflicts,
resolution: ConflictResolution.AUTO_NUMBER,
});
expect(plan.resolvedNames[0]).toMatch(/same\(1\)\.txt/);
expect(plan.resolvedNames[1]).toMatch(/same\(2\)\.txt/);
expect(plan.tasks.length).toBe(2);
});
it('skip strategy preserves original names', () => {
const newNames = ['same.txt', 'same.txt'];
const conflicts = checkBatchConflicts(files as any, newNames);
const plan = buildExecutionPlan({
files: files as any,
newNames,
conflicts,
resolution: ConflictResolution.SKIP,
});
expect(plan.resolvedNames).toEqual(['a.txt', 'b.txt']);
});
});