fix(api): stop discarding links written with a bare element id - #725
fix(api): stop discarding links written with a bare element id#725wagenet wants to merge 1 commit into
Conversation
POST /api/flows answered 201 Created and stored "links": [] when the links
used the bare form the Link doc documents as valid ({"from": "src0", "to":
"caps0"}). is_pad_valid rejected any pad reference without a colon and
prepare_flow then dropped those links, so the caller only found out when
starting the flow failed with a GStreamer "not-linked" error. update_flow
discarded them the same way.
The code was wrong, not the doc: ElementPadRef::from_string already parses a
bare id, the "id::" spelling already passed validation, and the pipeline
already implements element-level linking. A bare element reference is now
accepted and stored as sent; a bare block reference resolves to the block's
only output ("from") or input ("to") pad, since block expansion resolves
external pads by name.
prepare_flow now reports the links it cannot use, and create_flow and
update_flow answer 400 naming the offending link instead of storing a trimmed
flow. Pruning survives for the one case it is meant for: a pad the block's own
properties no longer produce, which is drift rather than a caller mistake.
Also honour the destination pad when the source is element-level - that
combination used to fall into src.link(sink) and let GStreamer pick both ends.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wagenet
left a comment
There was a problem hiding this comment.
There's more code here that I have limited context around, but it appears to make sense to me.
There was a problem hiding this comment.
Verdict: Approve — correct mechanism, replaces a silent-data-loss failure mode with a rejecting one, and the new tests genuinely exercise create_flow/update_flow rather than reimplementing them.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
resolve_link_endpoint is exhaustive: bare element (kept), bare block (resolved/rejected-if-ambiguous), named pad on computed block (checked), named pad on static block (trusted) |
CONFIRMED | backend/src/api/flows.rs:96-181; four branches on (node_id.is_empty, element_ids.contains, block_pads.get, pad_name) cover every case |
New linking.rs branch is reached exactly for (element-level from, named-pad to), doesn't shadow the other three |
CONFIRMED | backend/src/gst/pipeline/linking.rs:46,79,279: if let (Some,None), else if let (Some,Some), else if let Some(to_pad) — mutually exclusive, the new arm is the only remaining combination |
| New tests call the real endpoint handlers, not a reimplementation | CONFIRMED | backend/tests/bare_link_pads_test.rs:83,122,153,186,225,257,295 all call create_flow(State(...), JsonBody(...)) / update_flow(...) directly |
Frontend's parse_pad_ref gap is rendering-only; links preserved verbatim across load/save |
CONFIRMED | frontend/src/graph/mod.rs:271-278 returns None for a bare ref, used only for pad discovery (data.rs:463-496); GraphState.links is set verbatim from the loaded Vec<Link> at mod.rs:257, not filtered by parse_pad_ref |
| New tests actually executed in this repo's CI, not just locally | CONFIRMED | .github/workflows/ci.yml:128-129 cargo test --package strom ... runs in Check (Linux), green at 00ec80fe; API Contract Check (openapi snapshot) also green |
Diagnosis — root cause matches the PR's account: is_pad_valid treated "fewer than two colon-separated parts" as invalid regardless of node kind, so prepare_flow's retain() silently dropped the link. The fix surfaces a 400 instead of storing a flow with links missing — closes the reported symptom and the whole class (any bare-element or ambiguous-bare-block link), not just the one form. EndpointProblem::StaleBlockPad keeps the one legitimate auto-prune case (a block property lowered, e.g. num_inputs) distinct from a caller mistake, so loading an older flow doesn't turn into a hard 400.
Radius — SHARED, explicitly argued: types/src/element.rs's Link struct is structurally unchanged (doc comments only), no wire-format break — confirmed by the openapi.json diff being additive only. The behavior change (400 vs. silent drop) is contract-visible on POST /api/flows and POST /api/flows/{id}, and is the intended fix. update_flow's #[utoipa::path] already declared a generic 400 pre-existing (flows.rs:577), so the schema isn't stale, though its text wasn't synced to create_flow's new wording — cosmetic.
Tests & CI — Build (Linux x86_64/ARM64), Check (Linux), Check & Build (WASM), API Contract Check all pass at 00ec80fe. Build (macOS)/Build (Windows) skip per the usual gate, but this diff adds no platform-cfg code, so that skip isn't a coverage gap here.
Confidence: HIGH
Problem
POST /api/flowsanswered 201 Created and stored the flow with"links": []when the linksused the bare element form that the
Linkdoc comment documents as valid:{"from": "src0", "to": "caps0"}is_pad_validsplit the pad reference on:and returnedfalsefor anything with fewer thantwo parts;
prepare_flowthen dropped those links withflow.links.retain(...). The callerlearned nothing until
POST /api/flows/{id}/startfailed with a GStreamer "not-linked" error.POST /api/flows/{id}(update) discarded them the same way.Decision: support the bare form (option a)
The code was wrong, not the doc:
ElementPadRef::from_stringalready parses a bare id intopad_name: None, andto_string_formatalready round-trips it as"id::".try_link_elements_refsalready implements element-level linking (src.link(sink)), and the"id::"spelling already passed validation. Only the bare spelling was rejected.gst-launchdoes (videotestsrc ! x264enc): GStreamer pickscompatible pads, request pads included.
A bare reference to an element is now accepted and stored exactly as sent. A bare reference
to a block cannot be left alone — block expansion resolves external pads by name — so it is
resolved to the block's only output pad (
from) or only input pad (to) and stored in theexplicit
block_id:pad_nameform. A block with several pads on that side is ambiguous and isrejected with 400 naming the candidates.
No more silent drops
prepare_flownow returnsErrwith a caller-facing description of every link it cannot use,and both
create_flowandupdate_flowanswer 400 with that detail instead of storing atrimmed flow. The offending link is named in the message, e.g.
Pruning is kept, but only where it is legitimate
A block's external pads follow its own properties: lower a compositor's
num_inputsandb0:video_in_2stops existing. The frontend does not prune those links itself, and an exportedflow from before such a change can be imported later, so this drift is not a caller mistake.
That one case — the node exists, is a block, has computed pads, and does not have the named pad
— is still pruned (logged as
Pruning stale link ...) and the request succeeds. Everything else(unknown node, unresolvable bare reference) is a 400. The two paths are distinct enum variants,
EndpointProblem::StaleBlockPadvsRejected, so they cannot be conflated again.Other paths checked
update_flow: runs the sameprepare_flow, so it gets the same fix (covered by tests).POST /api/gst-launch/parse): always emitsid:padfor both ends, so itnever produced bare links itself. It hands elements and links to the frontend, which creates
the flow through
create_flow— covered by the fix.start_flowrecompute external pads but never prune links, sonothing was being discarded there.
parse_pad_refreturnsNonefor a bare reference, so such alink is not drawn in the graph editor. It is preserved across load/save (links are stored
verbatim), so this is a rendering gap, not data loss.
Also fixed
try_link_elements_refsignored the destination pad when the source was element-level:{"from": "src0", "to": "mux0:sink_0"}fell into thesrc.link(sink)branch and let GStreamerpick both ends. Now that the bare source form is reachable through the API, that combination
links with
link_pads(None, sink, Some(pad)).Tests
Ran locally on this machine, against this branch as rebased onto
main(1c06c37):cargo test --test bare_link_pads_test- new file, 7 tests, all pass. They callcreate_flow/update_flowdirectly rather than reimplementing validation inline.the link" behaviour in
resolve_link_endpointturns 5 of the 7 red(
create_flow_keeps_links_written_with_bare_element_ids,update_flow_keeps_links_written_with_bare_element_ids,create_flow_resolves_a_bare_block_reference_to_its_only_pad,create_flow_rejects_an_ambiguous_bare_block_reference,update_flow_rejects_a_link_to_an_unknown_node). The temporary patch was reverted afterwardsand is not part of this branch.
cargo test --lib api::flows- 11 tests pass. The oldis_pad_validunit tests were rewrittenagainst
resolve_link_endpoint, keeping the gst-launch-imported-id cases they were written forand adding the bare-form, ambiguity and staleness cases.
cargo test --test openapi_test- failed first, diff reviewed,openapi.jsonupdatedintentionally (
create_flownow documents its 400; theLinkfield descriptions changed),then re-run and passing.
cargo fmt --alland the repo's pre-commit clippy run pass.All three suites were re-run after the rebase, so the numbers above are from the exact tree
proposed here.
Not run: the rest of the backend test suite and the pipeline/GStreamer integration tests. This
change does not touch pipeline construction apart from the
link_padsbranch above, and themachine was under disk pressure.
🤖 Generated with Claude Code