feat: implement rewireConnection operation (Phase 1, Task 1)

Added intuitive rewireConnection operation for changing connection targets
in a single semantic step: "rewire from X to Y"

Changes:
- Added RewireConnectionOperation type with from/to semantics
- Implemented validation (checks source, from, to nodes and connection existence)
- Implemented operation as remove + add wrapper
- Added 8 comprehensive tests covering all scenarios
- All 134 tests passing (126 Phase 0 + 8 new)

Test Coverage:
- Basic rewiring
- Rewiring with sourceOutput specified
- Preserving parallel connections
- Error handling (source/from/to not found, connection doesn't exist)
- IF node branch rewiring

Expected Impact: 4/10 → 9/10 rating for rewiring tasks

Related: Issue #272 Phase 1 implementation
Phase 0 PR: #274

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-10-05 23:10:10 +02:00
parent 2a85000411
commit f9194ee74c
3 changed files with 377 additions and 7 deletions

View File

@@ -21,6 +21,7 @@ import {
AddConnectionOperation,
RemoveConnectionOperation,
UpdateConnectionOperation,
RewireConnectionOperation,
UpdateSettingsOperation,
UpdateNameOperation,
AddTagOperation,
@@ -225,6 +226,8 @@ export class WorkflowDiffEngine {
return this.validateRemoveConnection(workflow, operation);
case 'updateConnection':
return this.validateUpdateConnection(workflow, operation);
case 'rewireConnection':
return this.validateRewireConnection(workflow, operation as RewireConnectionOperation);
case 'updateSettings':
case 'updateName':
case 'addTag':
@@ -271,6 +274,9 @@ export class WorkflowDiffEngine {
case 'updateConnection':
this.applyUpdateConnection(workflow, operation);
break;
case 'rewireConnection':
this.applyRewireConnection(workflow, operation as RewireConnectionOperation);
break;
case 'updateSettings':
this.applyUpdateSettings(workflow, operation);
break;
@@ -458,20 +464,20 @@ export class WorkflowDiffEngine {
private validateUpdateConnection(workflow: Workflow, operation: UpdateConnectionOperation): string | null {
const sourceNode = this.findNode(workflow, operation.source, operation.source);
const targetNode = this.findNode(workflow, operation.target, operation.target);
if (!sourceNode) {
return `Source node not found: ${operation.source}`;
}
if (!targetNode) {
return `Target node not found: ${operation.target}`;
}
// Check if connection exists to update
const existingConnections = workflow.connections[sourceNode.name];
if (!existingConnections) {
return `No connections found from "${sourceNode.name}"`;
}
// Check if any connection to target exists
let hasConnection = false;
Object.values(existingConnections).forEach(outputs => {
@@ -481,11 +487,57 @@ export class WorkflowDiffEngine {
}
});
});
if (!hasConnection) {
return `No connection exists from "${sourceNode.name}" to "${targetNode.name}"`;
}
return null;
}
private validateRewireConnection(workflow: Workflow, operation: RewireConnectionOperation): string | null {
// Validate source node exists
const sourceNode = this.findNode(workflow, operation.source, operation.source);
if (!sourceNode) {
const availableNodes = workflow.nodes
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
.join(', ');
return `Source node not found: "${operation.source}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
}
// Validate "from" node exists (current target)
const fromNode = this.findNode(workflow, operation.from, operation.from);
if (!fromNode) {
const availableNodes = workflow.nodes
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
.join(', ');
return `"From" node not found: "${operation.from}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
}
// Validate "to" node exists (new target)
const toNode = this.findNode(workflow, operation.to, operation.to);
if (!toNode) {
const availableNodes = workflow.nodes
.map(n => `"${n.name}" (id: ${n.id.substring(0, 8)}...)`)
.join(', ');
return `"To" node not found: "${operation.to}". Available nodes: ${availableNodes}. Tip: Use node ID for names with special characters.`;
}
// Validate that connection from source to "from" exists
const sourceOutput = operation.sourceOutput || 'main';
const connections = workflow.connections[sourceNode.name]?.[sourceOutput];
if (!connections) {
return `No connections found from "${sourceNode.name}" on output "${sourceOutput}"`;
}
const hasConnection = connections.some(conns =>
conns.some(c => c.node === fromNode.name)
);
if (!hasConnection) {
return `No connection exists from "${sourceNode.name}" to "${fromNode.name}" on output "${sourceOutput}"`;
}
return null;
}
@@ -702,6 +754,36 @@ export class WorkflowDiffEngine {
});
}
/**
* Rewire a connection from one target to another
* This is a semantic wrapper around removeConnection + addConnection
* that provides clear intent: "rewire connection from X to Y"
*
* @param workflow - Workflow to modify
* @param operation - Rewire operation specifying source, from, and to
*/
private applyRewireConnection(workflow: Workflow, operation: RewireConnectionOperation): void {
// First, remove the old connection (source → from)
this.applyRemoveConnection(workflow, {
type: 'removeConnection',
source: operation.source,
target: operation.from,
sourceOutput: operation.sourceOutput,
targetInput: operation.targetInput
});
// Then, add the new connection (source → to)
this.applyAddConnection(workflow, {
type: 'addConnection',
source: operation.source,
target: operation.to,
sourceOutput: operation.sourceOutput,
targetInput: operation.targetInput,
sourceIndex: operation.sourceIndex,
targetIndex: 0 // Default target index for new connection
});
}
// Metadata operation appliers
private applyUpdateSettings(workflow: Workflow, operation: UpdateSettingsOperation): void {
if (!workflow.settings) {