Skip to content

fix(core): read arrays of ComponentId as child references - #3346

Open
nan-yu wants to merge 6 commits into
mainfrom
fix/component-id-array-references
Open

nan-yu wants to merge 6 commits into
mainfrom
fix/component-id-array-references

Conversation

@nan-yu

@nan-yu nan-yu commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

In the Python core, analyze_child_ref_schema did not recognize a property declared directly as an array of component ids, {"type": "array", "items": {"$ref": ".../ComponentId"}}, as opposed to the shared ChildList type. It resolved the items $ref to its plain string type before checking it, so the property's reference spec came out empty and the reader fell back to the v0.8 heuristic, which only treats an item as a reference when it names a component the surface already holds. Under a strict config:

  • a root with slots: ["missing"] was accepted, with no dangling-reference error;
  • with one held and one missing item, the error path read slots.[1] instead of slots[1].

Two sample catalogs declare Column.children this way: samples/community/agent/adk/file_upload_summarizer/catalogs/0.9/file_upload_catalog.json and samples/community/agent/adk/mcp_app_proxy/catalogs/0.9/mcp_app_catalog.json. An agent checking its tree at the end of a response would not catch a Column naming a child that does not exist.

Changes

  • Conformance: test(conformance): cover reference reading and composition cost in the core suites #3344 added two cases for this property shape to each of conformance/core/validator_v0_9.yaml and conformance/core/validator_v1_0.yaml, on an inline catalog with a Stack.slots property: test_v0X_topology_component_id_array_dangling_reference_error (one missing item, a dangling reference naming missing_slot) and test_v0X_topology_component_id_array_child_reachable (a held item is reachable). This PR adds two more to each suite: ..._partial_dangling_reference_error (one held and one missing item, a dangling reference at slots[1]) and ..._circular_reference_error (a cycle through slots is detected). Its own dangling and reachable cases, which duplicated test(conformance): cover reference reading and composition cost in the core suites #3344's, were dropped when merging main.
  • Skip lists: test(conformance): cover reference reading and composition cost in the core suites #3344 listed main's two dangling cases in Python's SKIP_TEST_NAMES; this PR removes them, since the fix passes them, and keeps the v0.8 pair, which is a known Python difference (it reads a v0.8 string property as a reference when it names a held component). Kotlin's A2uiValidator reads these arrays as references but reports items under the bare field name, so its skip list keeps only test_v09_topology_component_id_array_partial_dangling_reference_error, which checks slots[1]; main's v0.9 dangling case checks only the missing id, which Kotlin passes (Kotlin conformance: 174 passed).
  • Python (a2ui_core): analyze_child_ref_schema checks the array items for a ComponentId/Child/ChildList ref before resolving it. New test_reference_map.py case.
  • Swift (swift/core): it already read these arrays as references, but recorded every item under the bare property name (slots). It now records the index (slots[1]), as the other SDKs do, so the partial dangling case's slots[1] check holds. test(conformance): cover reference reading and composition cost in the core suites #3344's cases check only the missing id or no error, so the path change does not affect them. This also applies to ChildList arrays such as children. Every message that names a reference's field changes the same way, since they all read reference.field in GraphTopologyValidator.swift: the dangling-reference message, the self-reference message, and the UNALLOWED_CHILD detail path (root.children becomes root.children[0]). New PayloadValidatorTests case.
  • TypeScript (web_core) and Dart (a2ui_core) already pass all of these cases, so they are unchanged.

Behavior changes (Python)

Such arrays are now read as lists of references under every config. A strict config reports an item naming a missing component as a dangling reference, and error paths read name[i]. Recorded in python/a2ui_core/CHANGELOG.md.

Testing

  • Python: pyink --check ., mypy ., and pytest (all packages) pass. The test_reference_map.py case, the partial dangling cases and main's two dangling cases fail on main and pass with the fix (the ComponentId array conformance cases: 8 passed). Loading the two sample catalogs now gives Column.children as a list reference.
  • TypeScript web_core: build, then the conformance runner passes, new cases included.
  • Dart a2ui_core: validator_conformance_test.dart passes, new cases included.
  • Swift: not built locally, because A2UICore imports Combine and does not build on Linux. CI runs it on macOS.

This came out of review on #3326 (#3326 (comment)), where the fix was taken out to keep that PR a pure performance change.

A component property declared directly as an array of ComponentId refs
was not recognized by analyze_child_ref_schema, because the items ref was
resolved to its plain string type before the check. Such properties fell
back to the v0.8 heuristic, so a strict config accepted an item naming a
missing component, and error paths read 'name.[i]'.

Adds shared validator conformance cases for v0.9 and v1.0.

Signed-off-by: Nan Yu <nanyu@google.com>
Child references read from an array now carry the item index in their
field path ('slots[1]' rather than 'slots'), matching the other SDKs and
the new validator conformance cases.

Signed-off-by: Nan Yu <nanyu@google.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the validation behavior for component properties declared directly as an array of component IDs to ensure they are consistently read as a list of child references across all validation configurations. It updates the Python implementation in reference_map.py to identify these references before resolving schemas, and updates the Swift implementation in GraphTopologyValidator.swift to report index-based paths (e.g., name[i] instead of name.[i]). Conformance tests for both v0.9 and v1.0, along with language-specific unit tests and a changelog entry, have been added to verify these changes. I have no feedback to provide.

The legacy A2uiValidator reports an item of a list reference under the bare
field name ('slots'), not with its index ('slots[1]'), so the two new
dangling-reference cases fail on the message. They join the topology cases
the harness already skips for wording differences.

Signed-off-by: Nan Yu <nanyu@google.com>
Signed-off-by: Nan Yu <nanyu@google.com>
#3344 added a dangling reference case and a reachable child case for
arrays of ComponentId to the v0.9 and v1.0 validator suites. Drop this
branch's duplicates of those two and keep its partial dangling and
circular reference cases. Python now passes main's dangling cases, so
they leave its skip list, and Kotlin passes main's v0.9 dangling case,
which checks only the missing id.

Signed-off-by: Nan Yu <nanyu@google.com>

@polina-c polina-c left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addresses #3326 (comment): the CHANGELOG line and the test_reference_map.py case are both here.

What I checked at 1f354a2:

  • Python: with the base commit's reference_map.py, the new unit test fails (slots is missing from list_refs), and so do the four dangling and partial dangling conformance cases. With the fix, all eight ComponentId array cases pass. For the two sample catalogs in the description, Column.children is now a list reference; on the base commit it is missing.
  • Swift: swift test passes (575 tests). With the base commit's GraphTopologyValidator.swift, the new PayloadValidatorTests case and both validator conformance suites fail, so the index change is needed.
  • TypeScript web_core, Dart a2ui_core and the Kotlin legacy SDK pass the new cases without changes.

One addition for the description: the Swift change also changes the UNALLOWED_CHILD detail path (root.children becomes root.children[0]) and the self-reference message, since both read reference.field in GraphTopologyValidator.swift. That matches the new paths, but it is a visible change, and the description only mentions dangling-reference messages.

Two small comments below on test readability.

🤖 Agent-generated

Comment thread python/a2ui_core/tests/test_reference_map.py
Comment thread conformance/core/validator_v0_9.yaml Outdated
… test entry covers the fix

- Move the partial-dangling and circular ComponentId array cases to just
  after #3344's dangling and child-reachable cases in validator_v0_9.yaml and
  validator_v1_0.yaml, so the four cases sit together. The cases are
  unchanged.
- test_reference_map.py: say that `external` passed before the fix (only
  `#/` pointers are resolved) and that `slots` is the shape a loaded catalog
  has, which the fix covers.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu requested a review from polina-c October 20, 2026 21:54

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants