ref:main

epic children/view always report 'no children' — fetch_children misparses the grouped /links response #24

closed Opened by cole.christensen@gmail.com

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.

colechristensen cole.christensen@gmail.com commented 2026-07-26 20:07

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.