a2ui-project / a2ui-project/a2ui

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

Abierto
#2,498 2 comentarios 0 reacciones 1 asignado Reclamado por @Varun-S10 Ver en GitHub
P2 status: first-line-handled status: needs review type: bug
Lenguaje dominante
TypeScript
Estrellas
16.4k
Forks
1.3k
Merge medio
2 d 13 h
PR fusionados (30 d)
134

Descripción

- [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.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.