mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(queue): 添加队列合并功能,支持跳过重复项并标记源队列为已合并
This commit is contained in:
@@ -292,5 +292,65 @@ describe('issue routes integration', async () => {
|
||||
assert.equal(Array.isArray(res.json.execution_groups), true);
|
||||
assert.equal(typeof res.json.grouped_items, 'object');
|
||||
});
|
||||
|
||||
it('POST /api/queue/merge merges source queue into target and skips duplicates', async () => {
|
||||
const { writeFileSync, mkdirSync } = await import('fs');
|
||||
const { join } = await import('path');
|
||||
|
||||
// Create queues directory
|
||||
const queuesDir = join(projectRoot, '.workflow', 'issues', 'queues');
|
||||
mkdirSync(queuesDir, { recursive: true });
|
||||
|
||||
// Create target queue
|
||||
const targetQueue = {
|
||||
id: 'QUE-TARGET',
|
||||
status: 'active',
|
||||
issue_ids: ['ISS-1'],
|
||||
solutions: [
|
||||
{ item_id: 'S-1', issue_id: 'ISS-1', solution_id: 'SOL-1', status: 'pending' }
|
||||
],
|
||||
conflicts: []
|
||||
};
|
||||
writeFileSync(join(queuesDir, 'QUE-TARGET.json'), JSON.stringify(targetQueue));
|
||||
|
||||
// Create source queue with one duplicate and one new item
|
||||
const sourceQueue = {
|
||||
id: 'QUE-SOURCE',
|
||||
status: 'active',
|
||||
issue_ids: ['ISS-1', 'ISS-2'],
|
||||
solutions: [
|
||||
{ item_id: 'S-1', issue_id: 'ISS-1', solution_id: 'SOL-1', status: 'pending' }, // Duplicate
|
||||
{ item_id: 'S-2', issue_id: 'ISS-2', solution_id: 'SOL-2', status: 'pending' } // New
|
||||
],
|
||||
conflicts: []
|
||||
};
|
||||
writeFileSync(join(queuesDir, 'QUE-SOURCE.json'), JSON.stringify(sourceQueue));
|
||||
|
||||
// Create index
|
||||
writeFileSync(join(queuesDir, 'index.json'), JSON.stringify({
|
||||
active_queue_id: 'QUE-TARGET',
|
||||
queues: [
|
||||
{ id: 'QUE-TARGET', status: 'active' },
|
||||
{ id: 'QUE-SOURCE', status: 'active' }
|
||||
]
|
||||
}));
|
||||
|
||||
// Merge
|
||||
const res = await requestJson(baseUrl, 'POST', '/api/queue/merge', {
|
||||
sourceQueueId: 'QUE-SOURCE',
|
||||
targetQueueId: 'QUE-TARGET'
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.json.success, true);
|
||||
assert.equal(res.json.mergedItemCount, 1); // Only new item merged
|
||||
assert.equal(res.json.skippedDuplicates, 1); // Duplicate skipped
|
||||
assert.equal(res.json.totalItems, 2); // Target now has 2 items
|
||||
|
||||
// Verify source queue is marked as merged
|
||||
const sourceContent = JSON.parse(readFileSync(join(queuesDir, 'QUE-SOURCE.json'), 'utf8'));
|
||||
assert.equal(sourceContent.status, 'merged');
|
||||
assert.equal(sourceContent._metadata.merged_into, 'QUE-TARGET');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -965,6 +965,182 @@ describe('issue command module', async () => {
|
||||
|
||||
assert.equal(existsSync(join(env.queuesDir, `${queueId}.json`)), false);
|
||||
});
|
||||
|
||||
it('queue merge merges source queue into target and marks source as merged', async () => {
|
||||
issueModule ??= await import(issueCommandUrl);
|
||||
assert.ok(env);
|
||||
|
||||
const logs: string[] = [];
|
||||
mock.method(console, 'log', (...args: any[]) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
// Create target queue
|
||||
const targetId = 'QUE-TARGET-001';
|
||||
issueModule.writeQueue({
|
||||
id: targetId,
|
||||
status: 'active',
|
||||
issue_ids: ['ISS-1'],
|
||||
tasks: [],
|
||||
solutions: [
|
||||
{
|
||||
item_id: 'S-1',
|
||||
issue_id: 'ISS-1',
|
||||
solution_id: 'SOL-ISS-1-1',
|
||||
status: 'pending',
|
||||
execution_order: 1,
|
||||
files_touched: ['src/a.ts'],
|
||||
task_count: 1,
|
||||
},
|
||||
],
|
||||
conflicts: [],
|
||||
});
|
||||
|
||||
// Create source queue
|
||||
const sourceId = 'QUE-SOURCE-001';
|
||||
issueModule.writeQueue({
|
||||
id: sourceId,
|
||||
status: 'active',
|
||||
issue_ids: ['ISS-2'],
|
||||
tasks: [],
|
||||
solutions: [
|
||||
{
|
||||
item_id: 'S-1',
|
||||
issue_id: 'ISS-2',
|
||||
solution_id: 'SOL-ISS-2-1',
|
||||
status: 'pending',
|
||||
execution_order: 1,
|
||||
files_touched: ['src/b.ts'],
|
||||
task_count: 2,
|
||||
},
|
||||
],
|
||||
conflicts: [{ id: 'CFT-1', type: 'file', severity: 'low' }],
|
||||
});
|
||||
|
||||
// Set target as active queue
|
||||
const indexPath = join(env.queuesDir, 'index.json');
|
||||
writeFileSync(indexPath, JSON.stringify({ active_queue_id: targetId, queues: [] }));
|
||||
|
||||
await issueModule.issueCommand('queue', ['merge', sourceId], { queue: targetId });
|
||||
|
||||
// Verify merge result
|
||||
const mergedTarget = issueModule.readQueue(targetId);
|
||||
assert.ok(mergedTarget);
|
||||
assert.equal(mergedTarget.solutions.length, 2);
|
||||
assert.equal(mergedTarget.solutions[0].item_id, 'S-1');
|
||||
assert.equal(mergedTarget.solutions[1].item_id, 'S-2'); // Re-generated ID
|
||||
assert.equal(mergedTarget.solutions[1].issue_id, 'ISS-2');
|
||||
assert.deepEqual(mergedTarget.issue_ids, ['ISS-1', 'ISS-2']);
|
||||
assert.equal(mergedTarget.conflicts.length, 1); // Merged conflicts
|
||||
|
||||
// Verify source queue is marked as merged
|
||||
const sourceQueue = issueModule.readQueue(sourceId);
|
||||
assert.ok(sourceQueue);
|
||||
assert.equal(sourceQueue.status, 'merged');
|
||||
assert.equal(sourceQueue._metadata?.merged_into, targetId);
|
||||
});
|
||||
|
||||
it('queue merge skips duplicate solutions with same issue_id and solution_id', async () => {
|
||||
issueModule ??= await import(issueCommandUrl);
|
||||
assert.ok(env);
|
||||
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
const targetId = 'QUE-TARGET-DUP';
|
||||
const sourceId = 'QUE-SOURCE-DUP';
|
||||
|
||||
// Create target with a solution
|
||||
issueModule.writeQueue({
|
||||
id: targetId,
|
||||
status: 'active',
|
||||
issue_ids: ['ISS-DUP'],
|
||||
tasks: [],
|
||||
solutions: [
|
||||
{
|
||||
item_id: 'S-1',
|
||||
issue_id: 'ISS-DUP',
|
||||
solution_id: 'SOL-ISS-DUP-1',
|
||||
status: 'pending',
|
||||
execution_order: 1,
|
||||
files_touched: ['src/dup.ts'],
|
||||
task_count: 1,
|
||||
},
|
||||
],
|
||||
conflicts: [],
|
||||
});
|
||||
|
||||
// Create source with same solution (duplicate)
|
||||
issueModule.writeQueue({
|
||||
id: sourceId,
|
||||
status: 'active',
|
||||
issue_ids: ['ISS-DUP'],
|
||||
tasks: [],
|
||||
solutions: [
|
||||
{
|
||||
item_id: 'S-1',
|
||||
issue_id: 'ISS-DUP',
|
||||
solution_id: 'SOL-ISS-DUP-1', // Same issue_id + solution_id
|
||||
status: 'pending',
|
||||
execution_order: 1,
|
||||
files_touched: ['src/dup.ts'],
|
||||
task_count: 1,
|
||||
},
|
||||
],
|
||||
conflicts: [],
|
||||
});
|
||||
|
||||
const indexPath = join(env.queuesDir, 'index.json');
|
||||
writeFileSync(indexPath, JSON.stringify({ active_queue_id: targetId, queues: [] }));
|
||||
|
||||
await issueModule.issueCommand('queue', ['merge', sourceId], { queue: targetId });
|
||||
|
||||
const mergedTarget = issueModule.readQueue(targetId);
|
||||
assert.ok(mergedTarget);
|
||||
// Should still have only 1 solution (duplicate skipped)
|
||||
assert.equal(mergedTarget.solutions.length, 1);
|
||||
assert.equal(mergedTarget.solutions[0].solution_id, 'SOL-ISS-DUP-1');
|
||||
});
|
||||
|
||||
it('queue merge returns skipped reason when source is empty', async () => {
|
||||
issueModule ??= await import(issueCommandUrl);
|
||||
assert.ok(env);
|
||||
|
||||
const logs: string[] = [];
|
||||
mock.method(console, 'log', (...args: any[]) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
const targetId = 'QUE-TARGET-EMPTY';
|
||||
const sourceId = 'QUE-SOURCE-EMPTY';
|
||||
|
||||
issueModule.writeQueue({
|
||||
id: targetId,
|
||||
status: 'active',
|
||||
issue_ids: [],
|
||||
tasks: [],
|
||||
solutions: [{ item_id: 'S-1', issue_id: 'ISS-1', solution_id: 'SOL-1', status: 'pending' }],
|
||||
conflicts: [],
|
||||
});
|
||||
|
||||
issueModule.writeQueue({
|
||||
id: sourceId,
|
||||
status: 'active',
|
||||
issue_ids: [],
|
||||
tasks: [],
|
||||
solutions: [], // Empty source
|
||||
conflicts: [],
|
||||
});
|
||||
|
||||
const indexPath = join(env.queuesDir, 'index.json');
|
||||
writeFileSync(indexPath, JSON.stringify({ active_queue_id: targetId, queues: [] }));
|
||||
|
||||
await issueModule.issueCommand('queue', ['merge', sourceId], { queue: targetId });
|
||||
|
||||
assert.ok(logs.some((l) => l.includes('skipped') || l.includes('empty')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Queue Execution', () => {
|
||||
|
||||
Reference in New Issue
Block a user