a2ui-project / a2ui-project/a2ui

[BUG]: Dart and web_core DataModel diverge on 221 of 1,392 operation programs

オープン
#2,498 コメント 2 件 リアクション 0 件 担当者 1 名 @Varun-S10 が担当を希望しています GitHub で見る
P2 status: first-line-handled status: needs review type: bug
主要言語
TypeScript
スター
16.4k
フォーク
1.3k
平均マージ
2日 13時間
マージ済み PR(30日)
134

説明

- [x] I have searched the existing issues to make sure this bug has not already been reported.

## Describe the Bug

Following the same method as #2496, but pointed at `DataModel` — the state a client reports back to the agent when `sendDataModel` is on — I ran the Dart (`dart/a2ui_core`) and TypeScript (`renderers/web_core`) implementations over the same 1,392 generated operation programs and compared the resulting state, reads and errors. They disagree on 221.

Reduced to minimal cases:

| # | operations | Dart | web_core |
| --- | --- | --- | --- |
| 1 | `set('/a', 1)` then `set('/a', null)` | key removed → `{}` | `{a: null}` |
| 2 | `set('/a/b', 's')` then `set('/a/b/c', 1)` | **silently writes nothing** | `A2uiDataError` |
| 3 | `set('/', 1)` then `set('/a', 2)` | **silently writes nothing** | **`TypeError`** |
| 4 | `set('/', false)` then `set('/a', 2)` | silently writes nothing | **root discarded** → `{a: 2}` |
| 5 | `set('/__proto__/x', 1)` | stored as an ordinary key | `A2uiDataError` |
| 6 | `set('/items/100000', 1)` | `A2uiDataError` (capped at 10,000) | array grown to 100,001 |

Cases 2, 3 and 4 are one rule broken three ways, and I have sent a PR for them. The rest are decisions rather than defects, so they are here rather than in that PR.

### The `null` write (case 1) — 137 of the 221 programs

Dart removes the key; web_core stores the null. The v0.9 schema for `updateDataModel` distinguishes the two intents by whether `value` is present at all, so on the wire `{"path": "/a", "value": null}` and `{"path": "/a"}` are different messages — but Dart's `DataModel` has no way to express the difference, since `set(path, null)` is its removal path. `test/data_model_test.dart` pins this deliberately:

```dart
test('removes keys when setting null', () {
final model = DataModel({'foo': 'bar'});
model.set('/foo', null);
expect(model.get('/'), isEmpty);
});
```

So an agent sending `value: null` clears the key on Flutter and sets it to null on web, and a subsequent client data model report differs accordingly. This is the same underlying gap as #1896, one layer down and in this repository's Dart core rather than in `package:genui`.

Fixing it means splitting the setter from a remover and having `MessageProcessor` branch on whether `value` was present — an API change to `DataModel` plus a change in how `UpdateDataModelMessage` is parsed, which is why I have not sent a patch: #2439 is currently rewriting both of those files, and this seems worth agreeing on first. Which behaviour is intended?

Dart's is also internally inconsistent today: `set('/l/0', null)` on a list stores the null rather than removing anything, so the same `value: null` means "remove" or "assign" depending on the container.

### `__proto__` (case 5)

web_core rejects `__proto__`, `constructor` and `prototype` segments — prototype pollution has no analogue in Dart, so Dart stores them as ordinary keys. Nothing is unsafe on the Dart side, but a template that writes such a key produces state on Flutter that web refuses, so the two clients report different data models for identical messages. Worth deciding whether the rejection is a protocol rule or a web-only defence.

### Unbounded list index (case 6)

Already reported as #2420, and this run confirms the other half from Dart's side: Dart caps auto-vivification at `maxAutoVivifyIndex = 10000` and raises `A2uiDataError`, web_core grows the array.

## Steps to Reproduce

Each row of the table is two or three `set` calls on a fresh `DataModel`, listed in full above. The generated corpus is 1,392 programs of 2–6 operations over a fixed set of paths (nested objects, array indices, RFC 6901 escapes, the root) and values (numbers, strings, booleans, null, empty and non-empty containers), with a fixed seed; comparison normalises number types.

Reassuringly, the two agree on everything else the run covered: RFC 6901 `~0`/`~1` unescaping, sparse array fill, leading-zero index rejection, non-numeric segments on arrays, and reads of absent paths.

## Expected Behavior

Both implementations apply the same message to the same state, or refuse it the same way.

## Environment Details

- **OS**: macOS
- **Browser/Platform**: Dart 3.12.2, Node.js 25.2.1
- **SDK/Package Name & Version**: `a2ui_core` at main (676a899), `@a2ui/web_core` at main (676a899)
- **Protocol Version**: v0.9

## Additional Context

With cases 2–4 fixed, the divergence count drops from 221 to 137, and every remaining one is the `null` write in case 1.

Once `conformance/core/expressions.yaml` (#2497) has a shape you are happy with, the same treatment fits `DataModel` — a shared suite both clients run, so agreed answers to the questions above get pinned in one place instead of three. #2182 is already heading there for Python and TypeScript.

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。