epic children/view always report 'no children' — fetch_children misparses the grouped /links response #24
Links
No links yet.
Bug
anvil epic children <N> and the children/progress section of anvil epic view <N> always report “Epic #N has no children”, even when the epic has correctly-linked parent_of children. anvil issue links <N> on the same epic lists them fine.
Root cause
fetch_children/4 (src/commands/epic.rs) fetches GET /:org/:repo/issues/:number/links and tries to parse the links field as a flat Vec<LinkEntry>:
let raw = resp.get("links").or_else(|| resp.get("data")).cloned().unwrap_or(resp);
let entries: Vec<LinkEntry> = serde_json::from_value(raw).unwrap_or_default();
entries.into_iter().filter(|l| l.kind.as_deref() == Some("parent_of")).collect()
But the server returns links as an object grouped by relationship, not a flat array:
{ "links": { "blocks": [], "blocked_by": [], "parents": [...],
"children": [ { "issue": {"number":267,...}, "kind":"parent_of", "link_id":"..." }, ... ] } }
serde_json::from_value::<Vec<LinkEntry>>(object) fails on the object, and unwrap_or_default() swallows it into an empty vec — so children silently vanish. The unwrap_or_default() masks the parse error entirely.
Fix
Read links.children (grouped shape) and deserialize each entry’s nested issue (number/org/repo/state/title) + kind + link_id. Mirror the shape issue links already parses correctly. Do NOT unwrap_or_default() a real parse failure — surface it.
Repro (verified 2026-07-07 vs fangorn/anvil#266, 25 linked children)
anvil epic children 266 --repo fangorn/anvil # -> 'Epic #266 has no children' (WRONG)
anvil issue links 266 --repo fangorn/anvil # -> lists 25 children (correct)
Test
Add a unit test over fetch_children-equivalent parsing with the real grouped payload asserting N children are returned; regression-guard that a grouped object is not silently dropped to empty.
Fixed by #48 (merged).
fetch_children now reads the grouped shape (links.children) instead of trying to deserialize the grouped object as a flat Vec<LinkEntry>, and LinkEntry/TargetIssue were realigned to the server’s actual serialization (link_id, kind, issue: {org, repo, number, title, state}) — verified against AnvilWeb.Api.V1.IssueLinkController.serialize_groups/1. The old structs read id / target_issue / repository.org_slug, so every field missed even when the outer parse was fixed; remove-child was broken by the same mismatch (it looked up a never-populated id).
Regression tests added in tests/adversarial_pr_issue_epic.rs: epic_view_includes_children_from_grouped_links and epic_children_populates_from_grouped_links drive the real grouped payload and assert the child is present.
Residual: the deserialize still ends in unwrap_or_default(), so a future shape change would again degrade to empty rather than surfacing the parse error. The shape is now correct and test-guarded, but the ticket’s “do not swallow the parse failure” instruction is only partly honoured.