Skip to content

fix: semantics crash on the system-cell tooltips, and a group deleted mid-sync (#350, #356) - #357

Merged
yvanvds merged 2 commits into
developfrom
fix/issue-350-accessibility-bridge-axtree
Aug 25, 2026
Merged

yvanvds merged 2 commits into
developfrom
fix/issue-350-accessibility-bridge-axtree

Conversation

@yvanvds

@yvanvds yvanvds commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Summary

Two unrelated robustness fixes, one commit each. Both came out of failures
observed during this batch rather than from code reading.

fix(ui) — give each system-cell tooltip its own semantics container (#350)

The Windows app died with an engine-level error and took the session with it:

[ERROR:flutter/shell/platform/common/accessibility_bridge.cc(114)] Failed to
update ui::AXTree, error: 1140 will not be in the tree and is not the new root
Lost connection to device.

Root cause, established rather than guessed. It is upstream
flutter/flutter#182444
(open, P2), diagnosed in flutter/flutter#190344 and #190431 — both still open,
so no Flutter release contains the fix and upgrading would not have helped.
A
Tooltip is an OverlayPortal whose overlay is joined to its anchor by a
traversal-parent identifier. That identifier is silently dropped when the
anchor's SemanticsConfiguration is absorbed into a neighbour's; the overlay is
then serialized as an unreachable child, and the Windows embedder rejects the
entire update rather than the one node.

SystemIndicatorCell writes that recipe exactly — three adjacent Tooltips in
a ListView row — and it is on the Personeel details pane where the crash was
seen. Semantics(container: true) gives each anchor a node of its own so
nothing absorbs it.

Reproduced offline before being fixed. The regression test is a harness that
records real SemanticsUpdateBuilder traffic and replays it through the
engine's own reachability rule. It reproduces the failure deterministically
(~90 rejected updates per five-row mouse sweep) and discriminates cleanly:

variant rejected updates
Tooltips in a ListView row (ours, before) ~90
Tooltips without a ListView 0
ListView without Tooltips 0
one Tooltip per row 0
IconButton(tooltip:) 0
with Semantics(container: true) (this fix) 0
excludeFromSemantics no effect
ExcludeSemantics 135 — worse

The last two match the upstream reporter's own measurements, which is the check
that says the harness is modelling the real defect and not something adjacent.

Deliberately left alone: ReadOnlyLock and the settings MenuAnchors /
IconButtons. A lone anchor cannot reach this state and button-based anchors
already get their own node, so changing them would have been unverifiable churn.

fix(azure) — tolerate a group deleted between listing and member read (#356)

listGroups fans out one loadMemberIds call per group. A group deleted in the
tenant inside that window returned 404 Request_ResourceNotFound and the
exception propagated out of AzureConnector.sync, failing the whole pull.
In production that means an operator's sync dies because somebody deleted a Team
while it was running.

Both per-group member fan-outs (listGroups, loadByMailNicknames) now drop
such a group and continue, logging it in Dutch by displayName + object id with
a "N overgeslagen" clause on the summary.

The tolerance is deliberately narrow, because the failure mode of getting
this wrong is worse than the bug: a blanket catch would report an expired
token as a tenant with no groups, and the action engine reads "no Azure group"
as "create it" / "it is gone" — turning an auth error into a pile of destructive
proposals. Matching is on the Graph error code via a new
GraphException.isResourceNotFound, not on the status. Still throwing: 401,
403, 429, 5xx, a 404 with no Graph error envelope, a 404 carrying any other
code, a 404 from the listing leg itself, and a direct loadMemberIds call.

The other multi-item reads were checked and are not of this shape:
loadByEmployeeIds and loadByMailNicknames' chunk loop are $filter
collection queries where a vanished object is simply absent from the result set;
the $batch membership writes already surface per-member outcomes; getUser
already returns null on 404.

Also plumbed GraphClient.getCollection's expected predicate so a handled 404
logs as a detail rather than an error (#229) — without it a fully recovered pull
still painted the operator's log red.

How #356 was found, and why it is not a regression from #355

The develop post-merge sweep after #355 went red on the live Azure job. It was
not caused by that PR, and the evidence is worth recording because the next
reader will assume it was:

So a group was enumerated and then deleted inside the sync window — live
directory state moving under a read, not repo history. The test was right to go
red; the connector was wrong to die. #356 is that fix.

Test plan

Run on the branch tip (894efc6):

Both fixes were verified to fail without them: #350 via the harness table above,
#356 by stashing lib/ and confirming 6 behavioural failures including the
connector sync aborting.

No live API calls were made by either worker — #356's verification is unit-level
against a fake transport, which is what its acceptance criteria asked for.

Caveats worth carrying forward

Closes #350
Closes #356

yvanvds and others added 2 commits August 25, 2026 15:36
)

The app died mid-session with an engine-level error and no Dart stack:

    accessibility_bridge.cc(114) Failed to update ui::AXTree,
    error: 1140 will not be in the tree and is not the new root

This is upstream flutter/flutter#182444, diagnosed in flutter/flutter#190344
and flutter/flutter#190431, both still open and in neither 3.44.0 nor any
later stable. A Tooltip is an OverlayPortal whose overlay half is joined to
its anchor by a traversal-parent identifier; that identifier is silently
dropped when the anchor's SemanticsConfiguration is absorbed into a
neighbour's. The orphaned node is then serialized as a child nobody can
reach, and the Windows embedder rejects the whole update rather than the one
node. The accessibility tree freezes for the life of the window, and the
process has been observed to die outright.

SystemIndicatorCell writes the exact recipe: three adjacent tooltips in a row
of a ListView, whose viewport describes itself with two-pane semantics.
Asking for a semantics container leaves nothing to absorb the anchor's
config, so the identifier survives.

The new test replays the real SemanticsUpdateBuilder traffic through the
engine's reachability rule, so it fails on Windows' terms without needing
Windows: ~90 rejected updates for a five-row mouse sweep before this change,
none after.

Deliberately not changed: ReadOnlyLock, the app's other bare tooltip in a
scrolling row. Measured against the same harness a lone tooltip anchor
records no rejection — the fault needs a second overlay showing while the
first is still dismissing — so it is left alone and swept by the test
instead. IconButton(tooltip:) is immune for the same reason it already gives
its anchor a node of its own.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…356)

`GroupManager.listGroups` fans out one member read per row the listing
returned, so it is walking a list of a live directory that other writers
keep changing. A group deleted inside that window answered its member
read with `404 Request_ResourceNotFound`, which propagated out of
`AzureConnector.sync` and killed the entire Azure pull: no snapshot at
all, and an operator left with nothing but an object id to look up.

That group is now dropped from the snapshot — which is what the directory
now says about it — with a Dutch log line naming it (#266) and a closing
summary that says how many were skipped rather than reporting a short pull
as a complete one.

The tolerance is deliberately narrow, and that is the whole risk of the
change. `GraphException.isResourceNotFound` matches on Graph's own error
code and not on the status, so a 401/403, a 429, a 5xx, a 404 with no
Graph error envelope, and a 404 from the listing leg itself all still
propagate and still fail the sync. A blanket catch here would report an
expired token as a tenant with no groups, and downstream an absent Azure
group reads as one to create or as a class that is gone — a quiet failure
would come back as a screen full of destructive proposals.

`loadByMailNicknames` (#280) has the same fan-out and gets the same
treatment. Nothing else does: the other multi-item reads in
`group_manager.dart`/`user_manager.dart` are `$filter` collection queries,
where a vanished object simply is not in the result set, `$batch`
membership writes that already report per-member outcomes, or
`getUser`, which has always returned null on a 404.

The member read also tells the transport this reply is expected (#229), so
a pull that recovered does not leave a red line in the operator's log.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@yvanvds
yvanvds merged commit 9c2623a into develop Aug 25, 2026
5 checks passed
@yvanvds
yvanvds deleted the fix/issue-350-accessibility-bridge-axtree branch August 25, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant