Skip to content

Commit 00b31a4

Browse files
authored
Merge branch 'main' into qa-report
2 parents 35752e3 + 2964cbd commit 00b31a4

41 files changed

Lines changed: 577 additions & 337 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent_sdks/python/src/a2ui/parser/streaming.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __new__(cls, catalog: "A2uiCatalog" = None, *args, **kwargs):
7676
return super().__new__(cls)
7777

7878
def __init__(self, catalog: "A2uiCatalog" = None):
79+
self._version = getattr(catalog, "version", None) if catalog else None
7980
self._ref_fields_map = extract_component_ref_fields(catalog) if catalog else {}
8081
self._required_fields_map = (
8182
extract_component_required_fields(catalog) if catalog else {}
@@ -1007,11 +1008,12 @@ def traverse(obj, parent_key=None):
10071008
):
10081009
path = obj["path"]
10091010
key = path.lstrip("/")
1010-
if "componentId" not in obj:
1011-
obj.clear()
1012-
obj.update({"path": "/" + key})
1013-
else:
1014-
# If not in data model, still ensure path has leading slash if it's a bindable object
1011+
if self._version != VERSION_0_9:
1012+
if "componentId" not in obj:
1013+
obj.clear()
1014+
obj.update({"path": "/" + key})
1015+
elif self._version != VERSION_0_9:
1016+
# If not in data model, still ensure path has leading slash if it's a bindable object (v0.8 only)
10151017
current_path = obj.get("path")
10161018
if current_path is not None:
10171019
if not isinstance(current_path, str) or not current_path.startswith("/"):

agent_sdks/python/tests/parser/test_streaming_v08.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,36 @@ def test_streaming_msg_type_deduplication(mock_catalog):
273273

274274
# After completion, msg_types is reset
275275
assert parser.msg_types == []
276+
277+
278+
def test_v08_path_heuristic_adds_slash(mock_catalog):
279+
"""Tests that v0.8 adds a leading slash to relative paths."""
280+
parser = A2uiStreamParser(catalog=mock_catalog)
281+
# Disable validation for simplicity
282+
parser._validator = None
283+
284+
# 1. Send beginRendering first to avoid buffering
285+
chunk_br = (
286+
A2UI_OPEN_TAG
287+
+ '[{"beginRendering": {"surfaceId": "s1", "root": "root"}}]'
288+
+ A2UI_CLOSE_TAG
289+
)
290+
list(parser.process_chunk(chunk_br))
291+
292+
# 2. Send surfaceUpdate with a relative path
293+
chunk_su = (
294+
A2UI_OPEN_TAG
295+
+ '[{"surfaceUpdate": {"surfaceId": "s1", "components": [{"id": "root",'
296+
' "component": {"Text": {"text": {"path": "some/relative/path"}}}}]}}]'
297+
+ A2UI_CLOSE_TAG
298+
)
299+
300+
messages = []
301+
for part in parser.process_chunk(chunk_su):
302+
if part.a2ui_json:
303+
messages.extend(part.a2ui_json)
304+
305+
# The path should have been prefixed with a slash
306+
assert len(messages) > 0
307+
comp = messages[0][MSG_TYPE_SURFACE_UPDATE]["components"][0]
308+
assert comp["component"]["Text"]["text"]["path"] == "/some/relative/path"

agent_sdks/python/tests/parser/test_streaming_v09.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,67 @@ def test_streaming_msg_type_deduplication(mock_catalog):
388388

389389
# After completion, msg_types is reset
390390
assert not parser.msg_types
391+
392+
393+
def test_v09_path_heuristic_relative_path(mock_catalog):
394+
"""Tests that v0.9 allows relative paths (no leading slash)."""
395+
parser = A2uiStreamParser(catalog=mock_catalog)
396+
# Disable validation to avoid needing full catalog for this test
397+
parser._validator = None
398+
399+
# 1. Create surface
400+
chunk_cs = (
401+
A2UI_OPEN_TAG
402+
+ '[{"version": "v0.9", "createSurface": {"surfaceId": "s1", "catalogId": "c1"}}]'
403+
+ A2UI_CLOSE_TAG
404+
)
405+
list(parser.process_chunk(chunk_cs))
406+
407+
# 2. Update components with a relative path
408+
chunk_uc = (
409+
A2UI_OPEN_TAG
410+
+ '[{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components":'
411+
' [{"id": "root", "component": "Text", "text": {"path":'
412+
' "some/relative/path"}}]}}]'
413+
+ A2UI_CLOSE_TAG
414+
)
415+
416+
messages = []
417+
for part in parser.process_chunk(chunk_uc):
418+
if part.a2ui_json:
419+
messages.extend(part.a2ui_json)
420+
421+
assert len(messages) > 0
422+
comp = messages[0][MSG_TYPE_UPDATE_COMPONENTS]["components"][0]
423+
assert comp["text"]["path"] == "some/relative/path"
424+
425+
426+
def test_v09_path_heuristic_absolute_path(mock_catalog):
427+
"""Tests that v0.9 still supports absolute paths (leading slash)."""
428+
parser = A2uiStreamParser(catalog=mock_catalog)
429+
parser._validator = None
430+
431+
# 1. Create surface
432+
chunk_cs = (
433+
A2UI_OPEN_TAG
434+
+ '[{"version": "v0.9", "createSurface": {"surfaceId": "s1", "catalogId": "c1"}}]'
435+
+ A2UI_CLOSE_TAG
436+
)
437+
list(parser.process_chunk(chunk_cs))
438+
439+
# 2. Update components with an absolute path
440+
chunk_uc = (
441+
A2UI_OPEN_TAG
442+
+ '[{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components":'
443+
' [{"id": "root", "component": "Text", "text": {"path": "/absolute/path"}}]}}]'
444+
+ A2UI_CLOSE_TAG
445+
)
446+
447+
messages = []
448+
for part in parser.process_chunk(chunk_uc):
449+
if part.a2ui_json:
450+
messages.extend(part.a2ui_json)
451+
452+
assert len(messages) > 0
453+
comp = messages[0][MSG_TYPE_UPDATE_COMPONENTS]["components"][0]
454+
assert comp["text"]["path"] == "/absolute/path"

renderers/angular/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

renderers/angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@a2ui/angular",
3-
"version": "0.9.0-alpha.1",
3+
"version": "0.9.0-alpha.3",
44
"license": "Apache-2.0",
55
"homepage": "https://a2ui.org/",
66
"repository": {

renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { ComponentFixture, TestBed } from '@angular/core/testing';
1818
import { Component, input, signal } from '@angular/core';
1919
import { ButtonComponent } from './button.component';
20+
import { ComponentModel } from '@a2ui/web_core/v0_9';
2021
import { A2uiRendererService } from '../../core/a2ui-renderer.service';
2122
import { ComponentBinder } from '../../core/component-binder.service';
2223
import { By } from '@angular/platform-browser';
@@ -32,7 +33,7 @@ describe('ButtonComponent', () => {
3233
mockSurface = {
3334
dispatchAction: jasmine.createSpy('dispatchAction'),
3435
componentsModel: new Map([
35-
['child1', { id: 'child1', type: 'Text', properties: { text: 'Child Content' } }],
36+
['child1', new ComponentModel('child1', 'Text', { text: 'Child Content' })],
3637
]),
3738
catalog: {
3839
id: 'test-catalog',

renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { ComponentFixture, TestBed } from '@angular/core/testing';
1818
import { Component, input, signal } from '@angular/core';
1919
import { ColumnComponent } from './column.component';
20+
import { ComponentModel } from '@a2ui/web_core/v0_9';
2021
import { A2uiRendererService } from '../../core/a2ui-renderer.service';
2122
import { ComponentBinder } from '../../core/component-binder.service';
2223
import { By } from '@angular/platform-browser';
@@ -44,9 +45,9 @@ describe('ColumnComponent', () => {
4445
beforeEach(async () => {
4546
mockSurface = {
4647
componentsModel: new Map([
47-
['child1', { id: 'child1', type: 'Child', properties: {} }],
48-
['child2', { id: 'child2', type: 'Child', properties: {} }],
49-
['template1', { id: 'template1', type: 'Child', properties: {} }],
48+
['child1', new ComponentModel('child1', 'Child', {})],
49+
['child2', new ComponentModel('child2', 'Child', {})],
50+
['template1', new ComponentModel('template1', 'Child', {})],
5051
]),
5152
catalog: {
5253
id: 'test-catalog',
@@ -94,10 +95,9 @@ describe('ColumnComponent', () => {
9495

9596
it('should apply flex styles from props', () => {
9697
fixture.detectChanges();
97-
const div = fixture.debugElement.query(By.css('.a2ui-column'));
98-
expect(div.styles['justify-content']).toBe('start');
99-
expect(div.styles['align-items']).toBe('stretch');
100-
expect(div.styles['gap']).toBe('4px');
98+
const style = window.getComputedStyle(fixture.debugElement.nativeElement);
99+
expect(style.justifyContent).toBe('flex-start');
100+
expect(style.alignItems).toBe('stretch');
101101
});
102102

103103
it('should render non-repeating children', () => {
@@ -161,7 +161,7 @@ describe('ColumnComponent', () => {
161161
},
162162
});
163163
fixture.detectChanges();
164-
const div = fixture.debugElement.query(By.css('.a2ui-column'));
164+
const div = fixture.debugElement;
165165
expect(div.styles['justify-content']).toBeFalsy();
166166
expect(div.styles['align-items']).toBeFalsy();
167167
});

renderers/angular/src/v0_9/catalog/basic/complex-components.spec.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { SliderComponent } from './slider.component';
2222
import { DateTimeInputComponent } from './date-time-input.component';
2323
import { ListComponent } from './list.component';
2424
import { TabsComponent } from './tabs.component';
25+
import { ComponentModel } from '@a2ui/web_core/v0_9';
2526
import { ModalComponent } from './modal.component';
2627
import { BoundProperty } from '../../core/types';
2728
import { A2uiRendererService } from '../../core/a2ui-renderer.service';
@@ -37,30 +38,12 @@ describe('Complex Components', () => {
3738
surfaceGroup: {
3839
getSurface: jasmine.createSpy('getSurface').and.returnValue({
3940
componentsModel: new Map([
40-
[
41-
'child-1',
42-
{ id: 'child-1', type: 'Text', properties: { text: { value: 'Child 1' } } },
43-
],
44-
[
45-
'child-2',
46-
{ id: 'child-2', type: 'Text', properties: { text: { value: 'Child 2' } } },
47-
],
48-
[
49-
'content-1',
50-
{ id: 'content-1', type: 'Text', properties: { text: { value: 'Content 1' } } },
51-
],
52-
[
53-
'content-2',
54-
{ id: 'content-2', type: 'Text', properties: { text: { value: 'Content 2' } } },
55-
],
56-
[
57-
'trigger-btn',
58-
{ id: 'trigger-btn', type: 'Text', properties: { text: { value: 'Open' } } },
59-
],
60-
[
61-
'modal-content',
62-
{ id: 'modal-content', type: 'Text', properties: { text: { value: 'Modal' } } },
63-
],
41+
['child-1', new ComponentModel('child-1', 'Text', { text: { value: 'Child 1' } })],
42+
['child-2', new ComponentModel('child-2', 'Text', { text: { value: 'Child 2' } })],
43+
['content-1', new ComponentModel('content-1', 'Text', { text: { value: 'Content 1' } })],
44+
['content-2', new ComponentModel('content-2', 'Text', { text: { value: 'Content 2' } })],
45+
['trigger-btn', new ComponentModel('trigger-btn', 'Text', { text: { value: 'Open' } })],
46+
['modal-content', new ComponentModel('modal-content', 'Text', { text: { value: 'Modal' } })],
6447
]),
6548
catalog: {
6649
id: 'mock-catalog',

renderers/angular/src/v0_9/catalog/basic/list.component.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Component, input, computed, ChangeDetectionStrategy } from '@angular/co
1818
import { ComponentHostComponent } from '../../core/component-host.component';
1919
import { BoundProperty } from '../../core/types';
2020
import { BasicCatalogComponent } from './basic-catalog-component';
21+
import { Child } from '../../core/component-binder.service';
2122

2223
/**
2324
* Angular implementation of the A2UI List component (v0.9).
@@ -37,7 +38,7 @@ import { BasicCatalogComponent } from './basic-catalog-component';
3738
@switch (listTag()) {
3839
@case ('ol') {
3940
<ol [class]="'a2ui-list ' + orientation()" [style.list-style-type]="styleType()">
40-
@for (child of children(); track child.id) {
41+
@for (child of children(); track trackBy($index, child)) {
4142
<li>
4243
<a2ui-v09-component-host
4344
[componentKey]="child"
@@ -50,7 +51,7 @@ import { BasicCatalogComponent } from './basic-catalog-component';
5051
}
5152
@case ('ul') {
5253
<ul [class]="'a2ui-list ' + orientation()" [style.list-style-type]="styleType()">
53-
@for (child of children(); track child.id) {
54+
@for (child of children(); track trackBy($index, child)) {
5455
<li>
5556
<a2ui-v09-component-host
5657
[componentKey]="child"
@@ -63,7 +64,7 @@ import { BasicCatalogComponent } from './basic-catalog-component';
6364
}
6465
@default {
6566
<div [class]="'a2ui-list ' + orientation()" style="list-style-type: none;">
66-
@for (child of children(); track child.id) {
67+
@for (child of children(); track trackBy($index, child)) {
6768
<div class="a2ui-list-item-none">
6869
<a2ui-v09-component-host
6970
[componentKey]="child"
@@ -123,8 +124,6 @@ export class ListComponent extends BasicCatalogComponent {
123124
return Array.isArray(raw) ? raw : [];
124125
});
125126

126-
127-
128127
listTag = computed(() => {
129128
const style = this.listStyle();
130129
if (style === 'ordered') return 'ol';
@@ -137,4 +136,10 @@ export class ListComponent extends BasicCatalogComponent {
137136
if (style === 'none') return 'none';
138137
return '';
139138
});
139+
140+
/**
141+
* Track-by function to ensure stable change detection for list items.
142+
* Uses the full resolved path (`basePath/id`) to uniquely identify items.
143+
*/
144+
readonly trackBy = (index: number, item: Child) => `${item.basePath}/${item.id}`;
140145
}

renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { ComponentFixture, TestBed } from '@angular/core/testing';
1818
import { Component, input, signal } from '@angular/core';
1919
import { RowComponent } from './row.component';
20+
import { ComponentModel } from '@a2ui/web_core/v0_9';
2021
import { A2uiRendererService } from '../../core/a2ui-renderer.service';
2122
import { ComponentBinder } from '../../core/component-binder.service';
2223
import { By } from '@angular/platform-browser';
@@ -44,9 +45,9 @@ describe('RowComponent', () => {
4445
beforeEach(async () => {
4546
mockSurface = {
4647
componentsModel: new Map([
47-
['child1', { id: 'child1', type: 'Child', properties: {} }],
48-
['child2', { id: 'child2', type: 'Child', properties: {} }],
49-
['template1', { id: 'template1', type: 'Child', properties: {} }],
48+
['child1', new ComponentModel('child1', 'Child', {})],
49+
['child2', new ComponentModel('child2', 'Child', {})],
50+
['template1', new ComponentModel('template1', 'Child', {})],
5051
]),
5152
catalog: {
5253
id: 'test-catalog',
@@ -94,10 +95,9 @@ describe('RowComponent', () => {
9495

9596
it('should apply flex styles from props', () => {
9697
fixture.detectChanges();
97-
const div = fixture.debugElement.query(By.css('.a2ui-row'));
98-
expect(div.styles['justify-content']).toBe('center');
99-
expect(div.styles['align-items']).toBe('baseline');
100-
expect(div.styles['gap']).toBe('4px');
98+
const style = window.getComputedStyle(fixture.debugElement.nativeElement);
99+
expect(style.justifyContent).toBe('center');
100+
expect(style.alignItems).toBe('baseline');
101101
});
102102

103103
it('should render non-repeating children', () => {
@@ -161,7 +161,7 @@ describe('RowComponent', () => {
161161
},
162162
});
163163
fixture.detectChanges();
164-
const div = fixture.debugElement.query(By.css('.a2ui-row'));
164+
const div = fixture.debugElement;
165165
expect(div.styles['justify-content']).toBeFalsy();
166166
expect(div.styles['align-items']).toBeFalsy();
167167
});

0 commit comments

Comments
 (0)