Repository navigation
refactor(logic-simplifier): the Channels pane walked the device list twice - #426
Conversation
ChannelsPaneViewModel.Rebuild builds the entire Channels pane — the device chips, the three tile shelves, the channel -> owning-device map the settings drawer resolves through, and the header counts — and had no behavioural coverage at all. Every collection it fills is bound as an ItemsSource, so a change that reorders or mis-shelves a tile builds green and is visible only on screen. These 11 tests pass against unmodified production code. They assert the resulting SEQUENCE, not just membership. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Draining Dispatcher.UIThread from an arbitrary xUnit worker throws VerifyAccess once another class has claimed the dispatcher thread first: the test passed in isolation and failed in the full suite. The remaining ten reach the same Rebuild through the synchronous constructor path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…twice Rebuild ran two back-to-back 'foreach (var device in devices)' passes over the same list: the first added each device's name to ConnectedDeviceNames, the second built its tiles. Nothing sat between them and the second pass read nothing the first produced -- ChannelTileViewModel's constructor takes the device name and HasMultipleDevices, both already in hand before either loop starts -- so the split was accidental, not load-bearing. One loop now does both. Order is unchanged in both directions: names still follow connection order, tiles still run device by device with each device's channels sorted, because the merged loop walks the same list in the same order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Round 1. Head |
|
/agentic_review |
PR Summary by QodoSimplify Channels pane rebuild into a single device pass
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit f8966b0 |
|
Shepherd round: firing an independent Qodo round on the unchanged head Also re-deriving the claims independently: the mutation controls, the reordering risk in collapsing the double |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit f8966b0 |
|
Qodo-clean, CI green — ready for review. Head Independent Qodo round (round 3, on the unchanged head). Trigger timestamp recorded before posting (20:10:55Z); summary Re-proof of the claims. Every figure in the body re-derives:
Seven-way merge ( Counts reconcile against raw per-PR deltas I measured individually (
1116 +5 −3 +24 +0 +0 +3 +10 = 1155, matching the measured seven-way total. Quoting Passed; on macOS Skipped is 0 at every step, so Passed == Total and #423's Passed→Skipped conversion is a Windows-side effect invisible here — which is why its delta is +0 rather than negative. Removal hazards (the #189 Only #418 branched before current Not merging. |
Not merging — for review.
The simplification, in one line
ChannelsPaneViewModel.Rebuildran two back-to-backforeach (var device in devices)passes over the same list; one loop now does both jobs.That is the whole production diff, and it is the same figure with or without comments — no comment was touched, so there is no second number to report for it. The rest of the PR is the test file this routine required before the file could be touched at all:
Daqifi.Avalonia.Tests/ViewModels/ChannelsPaneRebuildTests.cs, +376 raw / +229 excluding comments and blanks (147 lines of XML doc). Whole PR: +376/−3 raw, +229/−3 code-only.So this diff is net-additive overall and I am not going to pretend otherwise: the logic shrank by three lines, and the reason the PR is nonetheless worth its size is the second half —
Rebuildhad no behavioural coverage at all before this, and it is the method that builds the entire Channels pane.Why it is simpler, not just shorter
Before:
Nothing sat between the two loops, and the second pass read nothing the first produced. The tile constructor takes
(channel, this, device.Name, HasMultipleDevices)— the device name comes off the device in hand, andHasMultipleDevicesis assigned fromdevices.Countbefore either loop starts. So the split was accidental, not load-bearing: a reader had to check for a dependency between the passes to find out there wasn't one, and the list was walked twice to no end.The merged loop states the actual rule directly — for each connected device: chip it, then shelve its channels — which is one sentence instead of two that have to be related by the reader.
Behaviour preservation
This is a behaviour-preserving change, so "tests that fail unfixed" is zero and says nothing. What backs it instead:
1. The tests landed first and passed against unmodified code. Commit
0b4ec35(tests) precedesf8966b0(the refactor) in this branch's history, and the 10 tests were green against production code byte-identical toorigin/main.2. Mutation controls on pristine
origin/main— 10 mutations, 10 correct kills. Each mutation's anchor text was asserted present before rewriting, so a mutation that silently did nothing could not be misread as a surviving one; the file was restored from a backup andtouched after every round so MSBuild never handed the runner a stale assembly.origin/mainHasMultipleDevicesassigned after the tile loop — the exact hazard merging the loops createsEvery_tile_labels_its_device_when_more_than_one_is_connected.OrderBy(c => c.Name, ChannelNameComparer.Instance)droppedTiles_run_device_by_device_and_numerically_within_eachTiles_run_device_by_device_and_numerically_within_eachThe_pane_chips_every_connected_device_in_connection_order_channelOwners[channel] = devicedeletedThe_settings_drawer_opens_on_the_device_that_owns_the_channelEach_channel_shelves_by_type_direction_and_pwmRecomputeCounts()droppedThe_header_counts_the_tiles_and_the_active_ones_separatelyHasConnectedDevice = falseOne_device_names_it…+Two_devices_report_multiple…HasConnectedDevice = trueWith_nothing_connected_the_pane_is_emptyHasMultipleDevices = devices.Count > 0One_device_names_it…+A_single_device_leaves_the_tile_labels_offThe last two exist specifically because two tests assert a
boolat its default (HasConnectedDevice == falseon an empty pane,ShowDeviceLabel == falseon a single device). An assertion on a default value passes when the assignment that should produce it is deleted, so each of those needed a mutation forcing the value the other way before it could be called a guard. All 10 tests are now demonstrably killable.3. A double loop is a reordering risk, so the tests pin the sequence, not the contents.
ConnectedDeviceNames,AnalogInputs,DigitalInputsandDigitalOutputsare all bound asItemsSource(ChannelsPanePrototype.axaml,ChannelsMobileView.axaml), so their order is what the user reads.Tiles_run_device_by_device_and_numerically_within_eachasserts the fulldevice/channelsequence across two devices, and feeds both devices their channels in the byte-wise order (AI0, AI10, AI2) so a pane that stopped sorting would produce exactly the wrong sequence rather than accidentally the right one.4. The rendered result is byte-identical.
tools/parity-audit/run.sh --check-baseline→ 39/39 screens matching the committedbaselines/macos-arm64.sha256, which itself holds 39 lines. That includes all 9desktop-min-*screens and all four that render this view-model directly (desktop-3-channels,desktop-min-3-channels,mobile-landscape-2-channels,mobile-portrait-2-channels).5. Build + suite. Desktop build, iOS
Release/iossimulator-arm64build, anddotnet testall exit 0; suite 1126/1126 (1116 onmain+ the 10 added here).packages.lock.jsonuntouched..github/scripts/check_test_sqlite_pooling.py --globpasses over the new file.Scope notes
.axamlbinding is affected.Handed forward, deliberately not folded in
The orphaned doc block in
DeviceLogsViewModel(lines 342–361) is still there and I left it alone: it belongs to a different view-model and a different feature, and pairing it with this would make a PR whose title cannot state one thing. One correction for whoever picks it up — it is a MOVE, not a deletion. The block accurately documentsRaiseSdGateChanged(declared at line 437 with no docs of its own) and records a real upstream limitation (daqifi-core#394: a silent TCP death leavesIsConnectedstale). It has merely drifted to sit aboveWatchSelectedDeviceConnectivity's own doc block. Deleting it would destroy accurate information.🤖 Generated with Claude Code