fix: missing subgraph contents lost when moved - #2703
Conversation
🎩 PreviewA preview build has been created at: |
morgan-wowk
left a comment
There was a problem hiding this comment.
🤖 Automated review
Reviewed both fixes against the head; the design is right and I traced the parts that could bite.
Fix 1 — nested contents preserved on move (verified correct). The switch from t.componentRef to t.resolvedComponentRef in snapshotTask/addInnerTasks, then routing through promoteInlineSubgraph, is exactly the right seam:
resolvedComponentRefreturnsthis.componentRefverbatim for a non-subgraph and only serializes the livesubgraphSpecfor a subgraph — so ordinary library/container tasks are untouched (no accidental spec-inlining), and a subgraph task now carries the contents that previously lived only in the runtimesubgraphSpecand never made it intocomponentRef.spec. That missing serialization is precisely why the inner graph came out hollow.promoteInlineSubgraphgates onisGraphImplementation(ref.spec.implementation), so it promotes a real nested subgraph into a livesubgraphSpecand leaves everything else as a plain ref. Both new tests assert the inner task, its position, and that the promoted task is a subgraph withcomponentRef.specstripped — good.
Fix 2 — binding-endpoint validation (verified correct, incl. the risky edges). One shared findBindingEndpointProblems feeding both the validator and the CSOM bridge is the right call — the agent is refused on exactly the grounds the validator would report. The edges that could produce false errors all hold:
- Silent while ports are unknown: non-subgraph
resolvedComponentSpeciscomponentRef.spec, which isundefineduntil the component resolves, so thesourcePorts &&/targetPorts &&guards short-circuit and an unresolved library task is never flagged. (Tested.) - Aggregator:
createAggregatorInputyields a realInputSpec(agg_N) that lands in the spec'sinputs, so legitimate aggregator connections resolve and pass; only the synthetic__add_aggregator_input__handle — never a real port — is caught. (Tested.) - Direction checks fire only on genuinely reversed connections: source-is-output matches only when the source id is a pipeline output, target-is-input only when the target id is a pipeline input; the valid task→output and input→task shapes don't match either
.find. AndIS_ENABLED_PORT_NAMEis whitelisted on targets. - No double-report with the orphaned-binding check: those require the entity id to be absent, while these require it to be present, so they're mutually exclusive per endpoint.
INVALID_BINDING_SOURCE/TARGET wire to a "Delete Connection" resolution, and the error-severity choice is justified (a bad endpoint serializes to nothing, so warning wouldn't prevent the silent data loss). Nice touch validating all 16 shipped pipelines against the new error before shipping it.
NIT (test gap, not a defect)
Fix 1's promote path round-trips through serializeComponentSpec(subgraphSpec) → YamlDeserializer.deserialize, and both regression tests assert the inner tasks and a position survive — but not the inner bindings (a connection between two nested tasks). Since the bug was "contents silently lost," a nested task→task binding surviving the group/unpack is the other half of "contents," and it's the part that leans on the serializer round-trip rather than a direct field copy. Low risk (that serializer is heavily exercised elsewhere), but adding one binding to the nested fixture and asserting it's still there would fully close the regression.
Approving — both fixes are correct and the coverage is strong; the note above is a nice-to-have.
`promoteInlineSubgraph` moves an inline graph off `componentRef.spec` and into `Task.subgraphSpec`, so a relocation that copies `componentRef` alone carries a ref whose spec has already been stripped. Both `unpackSubgraph` and `createSubgraph` did exactly that, silently flattening any subgraph nested inside the task being moved. Snapshot through `resolvedComponentRef` and re-promote, matching the copy/paste path in `taskManifestBase`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A binding to a port the component does not declare, or one sourced from a pipeline output or targeted at a pipeline input, serializes to nothing at all — the argument is simply absent from the saved YAML. Nothing caught either case, so `connect_nodes` accepted both and the pipeline looked connected in the editor while silently dropping the value on save. `findBindingEndpointProblems` is the single predicate for this, shared by `validateSpec` (so every route is covered, not just the agent) and by the AI bridge (so the agent is refused on the same grounds, with the available port names in the refusal). Port names are checked on task endpoints only: for graph inputs and outputs the serializer reads the entity's own name and ignores the binding's port name, so a rename leaves that field stale by design. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0e05cdb to
8dda8ef
Compare
Description
Two bug fixes, both cases where a pipeline quietly lost part of itself.
1. A subgraph inside a subgraph was emptied when you moved the task holding it
If a subgraph contained another subgraph, two everyday actions destroyed the inner one:
Afterwards the inner subgraph task was still sitting on the canvas with its name and position, but it was hollow — double-clicking no longer opened it, and its ports were gone. Nothing warned you; you only found out when you went looking inside.
Both actions now bring the nested contents along with the task. Copy/paste already handled this correctly — these two paths simply weren't doing the same thing.
2. Connections to ports that don't exist were accepted
It was possible to end up with a connection that looked fine on the canvas but disappeared the moment the pipeline was saved, because it pointed at a port the component doesn't actually have, or ran the wrong way (out of a pipeline output, or into a pipeline input). Nothing flagged it, so the pipeline would happily submit and then behave as if the value had never been connected.
These are now caught in two places:
Dragging on the canvas could never produce these, so in practice they came from the AI assistant or from imported YAML.
Related Issue and Pull requests
Follows up on the two review comments left on #2684 that were deferred out of that stack.
Type of Change
Checklist
Test Instructions
Nested subgraphs (fix 1)
Connections (fix 2)
foooutput to Evaluate"). It should refuse and list the real port names rather than creating a connection that goes nowhere.Additional Comments
Regression tests were added for both fixes, and each new test was confirmed to fail without its fix. The full unit suite passes (2362 tests).
The new connection check is error-severity, which blocks submission, so all 16 shipped example and tour pipelines were checked against it — 95 connections, no new errors. Nothing that ships starts showing problems.
The check deliberately only looks at connections between tasks. Connections to a pipeline input or output are named after the input or output itself when saved, so renaming one leaves a harmless leftover name behind that would otherwise get flagged as broken.