Skip to content

refactor(logic-simplifier): the duplicate-device check answers with the match, not a flag beside it - #425

Merged
tylerkron merged 2 commits into
mainfrom
logic-simplifier-duplicate-check-20260920
Sep 21, 2026
Merged

tylerkron merged 2 commits into
mainfrom
logic-simplifier-duplicate-check-20260920

Conversation

@tylerkron

Copy link
Copy Markdown
Contributor

Not merging — for review.

The simplification, in one line

The duplicate-device check now is its answer — it returns the match, or null — instead of
returning an object with an IsDuplicate flag sitting beside the ExistingDevice it matched,
which forced both call sites to hedge against the two disagreeing.

ConnectionManager.cs: +57 / −58 raw, and +31 / −46 counting only non-comment lines
(net −15 of code)
. The raw total is near-neutral because the deleted machinery was replaced
with documentation, not with more machinery — the code-line figure is the one that describes
the change.

Why it is simpler, not just shorter

CheckForDuplicateDevice has exactly one producer of a positive result, and it sets both
halves in the same object initializer:

return new DuplicateDeviceCheckResult
{
    IsDuplicate = true,
    ExistingDevice = existingDevice,
    ...
};

So IsDuplicate was never anything but ExistingDevice != null. Both callers nevertheless
wrote:

return duplicateResult.ExistingDevice != null
    ? DAQiFiConnectionStatus.AlreadyConnected
    : DAQiFiConnectionStatus.Error;

Those two Error arms are unreachable. They are the cost of a type that lets the flag and
the device disagree. Returning DuplicateDeviceCheckResult? makes "this is a duplicate" and
"this is the device it duplicates" the same fact, and both branches delete themselves.

Four consequences, all deletions:

before after
"is it a duplicate" IsDuplicate flag + a device that may or may not be set the result is non-null, or there isn't one
unreachable Error arms 2 0
pre-connect nesting if (dup) { if (handler != null) { switch } else { return } } if (dup) { if (handler == null) return; switch } — the switch drops a level
DuplicateDeviceCheckResult class, 5 settable properties, built by object initializer positional sealed record, every member set by the one constructor
DuplicateDeviceHandler Func<…> assigned null! with a four-line apology, null-checked anyway Func<…>? — the type says what the code already did

Nothing was extracted, no helper, no parameter, no seam. NewDevice,
NewDeviceInterface and ExistingDeviceInterface are unchanged and still read by
ConnectionDialogViewModel.HandleDuplicateDevice, which needs no edit.

Behaviour preservation — how it was checked

This is behaviour-preserving, so "how many tests fail unfixed" is zero and says nothing.
Mutation testing is the evidence instead.

1. Tests written first, and confirmed green on unchanged code. Three of the four
duplicate-device outcomes had no test. Added
Switching_to_the_new_device_replaces_the_one_already_connected,
A_duplicate_refused_without_a_prompt_is_reported_as_already_connected and
A_duplicate_only_visible_after_connecting_is_reported_as_already_connected, and ran all
8 rows in the class against unmodified origin/main production code — 8/8 pass. That
is the baseline; the refactor came after.

2. Mutation-tested origin/main. Four mutations, applied and reverted one at a time
against a production tree git status confirmed byte-identical to origin/main. Each
produced exactly one failure, and each hit the test whose name claims that behaviour:

mutation on origin/main test killed
no-handler duplicate returns Error A_duplicate_refused_without_a_prompt_is_reported_as_already_connected
post-connect device.Disconnect() removed A_duplicate_only_visible_after_connecting_is_reported_as_already_connected
post-connect duplicate returns Error (same)
SwitchToNew's Disconnect(existing) removed Switching_to_the_new_device_replaces_the_one_already_connected

No row asserts only a default value: the two teardown assertions are Assert.False(…​.IsConnected)
on a double that reports true after its pretend connect, so a dead assertion would not pass.

3. Full suite: 1119 passed, 0 failed on this branch.

4. Build gate green — desktop head and iOS Release/iossimulator-arm64 both exit 0
(checked by exit code, not by grepping -tl:on output). packages.lock.json untouched.
Android was not built — no workload on this Mac; CI covers it.

5. Warnings: 5 fewer, all in this file. ConnectionManager.cs carried 5 distinct
CS8618 — 4 on DuplicateDeviceCheckResult's settable properties, 1 on
DuplicateDeviceHandler — and now carries none. They were an artifact of the very shape
this PR removes: an object initializer the compiler cannot prove fills every member.
Measured on the Daqifi.Avalonia library compile that dotnet test performs, before and
after, de-duplicated on (file, line, col, message); the File.cs(line,col) extraction was
positive-controlled against a warning known to be present in the after log. That is the
local figure — CI's all-project total runs a small constant above it.

6. No XAML exposure. No .axaml binds IsDuplicate, DuplicateDeviceHandler or
DuplicateDeviceCheckResult (glob positive-controlled), and DuplicateDeviceDialogViewModel
— what the dialog actually binds — is untouched.

What this does not cover

The duplicate-device dialog itself was not rendered. Nothing it binds changed, so there is
no visual delta to show; the claim rests on the dialog's view-model being untouched and on the
compiler resolving ConnectionDialogViewModel's four property reads, not on a screenshot.

Two // @port: markers go with the symbols they named (IsDuplicate, and the old property
form of the others); the type-level marker and one marker per surviving member are kept.

🤖 Generated with Claude Code

tylerkron and others added 2 commits September 20, 2026 08:53
…ing them

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…he match itself, not a flag beside it

CheckForDuplicateDevice returned an object carrying IsDuplicate alongside the device it
matched, so two members had to agree about one fact. Callers hedged against them
disagreeing — twice, with a ternary whose 'no existing device' arm could never run, because
the only producer sets IsDuplicate = true and ExistingDevice = existingDevice in the same
initializer. Returning null when there is no duplicate makes the fact structural and deletes
both unreachable branches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tylerkron

Copy link
Copy Markdown
Contributor Author

Round 1. Head 081aaa1. Requesting review below.

Two things worth a reviewer's attention specifically:

  1. The claim that the two deleted Error arms were unreachable rests on there being exactly one producer of a positive DuplicateDeviceCheckResult. Worth checking that independently — CheckForDuplicateDevice is the only method that constructs one, and its positive branch is guarded by existingDevice != null.
  2. DuplicateDeviceHandler became nullable. That is what the code already assumed (it null-checked, and the test constructor assigned null!), but it is a public surface change, so a caller that relied on the non-nullable annotation would now see a warning rather than a silent lie.

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Simplify duplicate-device checks to return nullable matches

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Return nullable duplicate matches, eliminating inconsistent flags and unreachable error outcomes.
• Make callback nullability and result construction reflect the actual connection lifecycle.
• Cover replacement, handlerless rejection, and post-connect duplicate cleanup paths.
Diagram

sequenceDiagram
    actor Caller
    participant Manager as Connection Manager
    participant Check as Duplicate Check
    participant Handler as Duplicate Handler
    participant Device
    participant List as Connected Devices
    Caller->>Manager: Connect device
    Manager->>Check: Find identity match
    Check-->>Manager: Match or null
    alt Pre-connect duplicate
        alt No handler
            Manager-->>Caller: Already connected
        else Handler installed
            Manager->>Handler: Request action
            Handler-->>Manager: Keep cancel or switch
            opt Switch device
                Manager->>List: Remove existing
            end
        end
    end
    Manager->>Device: Open connection
    Manager->>Check: Recheck identity
    Check-->>Manager: Match or null
    alt Post-connect duplicate
        Manager->>Device: Close connection
        Manager-->>Caller: Already connected
    else Unique device
        Manager->>List: Add device
        Manager-->>Caller: Connected
    end
Loading
High-Level Assessment

The nullable match is the strongest representation because duplicate existence and the matched device become one fact while preserving metadata needed by the dialog. Retaining a computed flag or returning only the existing device would either preserve redundant state or discard interface details, so the PR’s focused approach is preferable.

Files changed (3) +145 / -58

Refactor (1) +57 / -58
ConnectionManager.csRepresent duplicate detection with a nullable immutable match +57/-58

Represent duplicate detection with a nullable immutable match

• Changes duplicate checks to return a nullable sealed record instead of a mutable result with an IsDuplicate flag. Simplifies pre- and post-connect branches, removes unreachable error outcomes, and marks the optional duplicate handler as nullable.

Daqifi.Avalonia/Daqifi.Desktop/ConnectionManager.cs

Tests (2) +88 / -0
ConnectionManagerConnectResultTests.csCover all duplicate-device connection outcomes +75/-0

Cover all duplicate-device connection outcomes

• Adds behavioral tests for switching to the new device, rejecting duplicates without a handler, and detecting identities revealed only after connection. The assertions verify statuses, transport cleanup, and connected-device list membership.

Daqifi.Avalonia.Tests/ConnectionManagerConnectResultTests.cs

DroppedDeviceTestDoubles.csSimulate device identity appearing during connection +13/-0

Simulate device identity appearing during connection

• Adds an optional serial number revealed by the test device during Connect. This models devices whose identity is unavailable to the pre-connect duplicate check.

Daqifi.Avalonia.Tests/Device/DroppedDeviceTestDoubles.cs

@qodo-code-review

qodo-code-review Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 081aaa1 ⚖️ Balanced

Results up to commit 081aaa1 ⚖️ Balanced


No changes from previous review

Grey Divider

Qodo Logo

@tylerkron

Copy link
Copy Markdown
Contributor Author

Round 2, same head 081aaa1 — no code change. Round 1 came back Bugs (0) / Rule violations (0) / Requirement gaps (0) with zero inline threads, and settled clean on a re-check three minutes later. Re-running because a round on an unchanged head is not a repeat of the last one: it has raised a real finding on three of the last four PRs in this repo where it was tried.

CI is green on all five heads including Android (which this Mac cannot build locally).

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 081aaa1

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo loop complete — two clean rounds on head 081aaa1, both settled.

round trigger review landed anchored to result
1 19:08:21Z 19:09:56Z 081aaa1 Bugs (0) / Rule violations (0) / Requirement gaps (0)
2 19:20:27Z 19:22:08Z 081aaa1 Bugs (0) / Rule violations (0) / Requirement gaps (0)

Both confirmed by updated_at advancing past the trigger, not merely by a qodo comment existing — the round-2 advance at 19:20:54Z was the Qodo is busy working placeholder and was not counted. Each round was re-checked after a >3-minute settle: summary unchanged, and 0 unresolved inline review threads (GraphQL reviewThreads, author matched by startswith("qodo-code-review"); the author leg was validated against control PR #196, which returns the expected 3 qodo threads / 0 unresolved).

Depth grade: not observable. Neither round's output carried a depth grade, so I am not reporting one — that is an absence of a signal, not a Skipped.

Numbers re-derived after the final commit (git diff --numstat origin/main...HEAD at 081aaa1, no commits since the body was written): ConnectionManager.cs +57/−58 raw, +31/−46 excluding comments. Tests +88 across two files.

CI green on all five heads, including the Android head this Mac cannot build locally.

@tylerkron

Copy link
Copy Markdown
Contributor Author

Round 3 — independent shepherd round on the unchanged head 081aaa1 (no code change; CI run 35531287956 is green on this exact SHA).

Rounds 1 and 2 were the builder's own. An independent round on a byte-identical head has raised a real finding on three of the five PRs it has been tried on in this loop, so it is worth one dispatch here regardless of the two clean rounds already recorded.

While it runs I am re-deriving the behaviour-preservation claims by hand: the two Error arms' unreachability at every call site, the record conversion's equality semantics, the absence of any .axaml binding to the removed settable properties, and a spot-check of one of the four mutation controls.

@tylerkron

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 081aaa1

@tylerkron

Copy link
Copy Markdown
Contributor Author

Shepherd round 3 — Qodo-clean, CI green on 081aaa1 — ready for review

Round 3 (independent, unchanged head 081aaa1): 🐞 Bugs (0), 📘 Rule violations (0), 📎 Requirement gaps (0). Trigger 19:32:29Z → summary re-rendered 19:35:43Z with real review content anchored to 081aaa1 (the Qodo is busy working placeholder landed at 19:32:44Z as a separate comment and was correctly not counted). Settle re-check at 19:53:08Z: summary updated_at unchanged, and 0 qodo-authored inline threads, 0 unresolved via GraphQL reviewThreads — author leg validated against control PR #196, which returns 3 qodo threads.

Correction to the round summary above: the depth grade IS observable, and it is ⚖️ Balanced. It sits in the folded Previous reviews block — Review updated until commit 081aaa1 ⚖️ Balanced. The earlier note that no grade was observed on rounds 1–2 should not be read as "there was no grade"; I found one on the same unchanged head.

Behaviour preservation, re-proved rather than re-read

1. The two Error arms really are unreachable — with a positive control, not by absence. IsDuplicate = true has exactly one producer, which sets ExistingDevice in the same initializer; the type name has only four mentions repo-wide on main (return type, Func<> parameter, class declaration, HandleDuplicateDevice parameter), so there is no target-typed new(...) producer hiding. Two runs on main’s production code with this PR’s tests:

control on origin/main production code result
unmodified 1119 / 1119 pass — the three new tests are not written to the refactor
both … : DAQiFiConnectionStatus.Error arms replaced with throw new InvalidOperationException(…) 1119 / 1119 pass — the false arm is never taken
pre-connect no-handler branch forced to return Error (builder’s mutation 1) exactly 1 failure, A_duplicate_refused_without_a_prompt_is_reported_as_already_connected

The third row is what makes the second row a proof rather than a vacuous pass: the same ternary is executed under test, and only its true arm is. Spot-check of the builder’s mutation table reproduced exactly.

2. The record conversion. Nothing compares, hashes or stores a DuplicateDeviceCheckResult — its only uses are construction, the != null tests, and four property reads in ConnectionDialogViewModel.HandleDuplicateDevice, which reads them by name, so positional order is irrelevant there. The != null checks now bind the record’s synthesized operator !=, which is !((object)x == null || …) and is therefore null-equivalent to the old reference compare. Residual worth knowing (not a defect — the order in the one constructor call is correct): ExistingDeviceInterface and NewDeviceInterface are both string, so a future swap of those two arguments would compile silently. The IStreamingDevice pair is protected — Switching_to_the_new_device_replaces_the_one_already_connected would fail if they were transposed.

3. No XAML exposure. IsDuplicate, ExistingDevice, NewDevice, NewDeviceInterface, ExistingDeviceInterface and DuplicateDeviceCheckResult: zero hits across every .axaml, with the glob positive-controlled (27 .axaml files match Binding on the same tree).

4. Both diffstat figures reproduce exactly — ConnectionManager.cs raw +57 / −58, comment-stripped +31 / −46 (net −15). Stating both, with the less flattering one first, is the honest form. ConnectionManagerConnectResultTests holds 8 [Fact] rows (5 on main + 3), so "8/8 against unmodified main" checks out.

5. The 5 CS8618 are exactly attributable. Two --no-incremental library builds, warnings de-duplicated on (file, line, col, message):

ConnectionManager.cs(237,13)   CS8618  DuplicateDeviceHandler
ConnectionManager.cs(1282,29)  CS8618  ExistingDevice
ConnectionManager.cs(1284,29)  CS8618  NewDevice
ConnectionManager.cs(1286,19)  CS8618  NewDeviceInterface
ConnectionManager.cs(1288,19)  CS8618  ExistingDeviceInterface

All five gone after; nothing else added or removed in the whole first-party set. Positive control: the after log still contains 132 CS8618 lines elsewhere, so the zero for this file is real and not a broken pattern. (Note the null! was on a different constructor from the one at :237 that the warning names — the "four-line apology" suppressed one site, not the warning.)

Six-way merge — main + #418 + #419 + #420 + #422 + #423 + #425

Merged tree fd7c5e1b03c8918d89bc6d3fabe272e2bb33a75c, identical in two orders (octopus in PR order, and sequential in reverse order 425→423→422→420→419→418). The five-way sub-build without #425 reproduces the earlier shepherd’s 8599ae34 exactly.

Local gate on the merged tree, all by exit code (never by grepping -tl:on output): desktop head exit 0 (150 warnings), iOS Release/iossimulator-arm64 exit 0 (151 warnings, 111.9 s), packages.lock.json untouched. Suite 1145 passed / 0 failed / 0 skipped.

Reconciliation, each delta measured by test-merging that PR into current main on its own rather than quoting a standalone total:

tree Passed delta
main (4716ce1) 1116 —
main + #418 1121 +5
main + #419 1113 −3 (test-negative)
main + #420 1140 +24
main + #422 1116 0
main + #423 1116 0
main + #425 1119 +3
six-way 1145

1116 + 5 − 3 + 24 + 0 + 0 + 3 = 1145, measured — no interaction term. #425’s +3 is its three new [Fact]s; its standalone 1119 happens to equal main+#425 only because its merge-base is current main (as it is for #419, #420, #422 and #423 — only #418 branched earlier, at ef7d18c). On #423: 0 Skipped here because macOS is a Unix host, so the UnixOnlyFact rows run; its Passed→Skipped conversion only bites on the Windows leg, which CI covers.

Removed-member and added-type audit on the merged tree (the #189 CS0115 shape). Every member deleted by the six — PauseForFlashAsync, StopKeepAliveAsync(bool), SendScpiMessage, ImportFromStreamAsync, EnsureAnyDeviceConnected, SwitchToNewInterface, AdminChecker, NoOpMessageBoxService, IsDuplicate — searched across .cs and .axaml: the only survivors are a prose comment mentioning PauseForFlashAsync and the reshaped no-arg StopKeepAliveAsync() with matching call sites. Added top-level types (CreateForManualEndpointAsync factory, UnixOnlyFactAttribute, ManualWifiEndpointTests, FirewallConfigurationTests) introduce no duplicate definition — the merged tree’s duplicate-type-name multiset is byte-identical to main’s (all pre-existing: Program, App, EF migrations, AbstractStreamingDevice).

Visual gate on the merged tree: 39 / 39. Denominator read off the committed manifest in that same tree (macos-arm64.sha256 = 39 lines on both main and the merge), and the run captured and matched all 39 — no desktop-min-* shortfall. One non-fatal [WARN] between screens (drawer pane settled at 3 px while closed) affected no captured screen and is pre-existing harness behaviour, not this PR’s.

Provenance

I pushed nothing, so the head is still 081aaa1 and the verification behind this note is CI’s own run 35531287956 on that exact SHA — Android head, Desktop head on macOS, Library + Desktop head + capture harness, iOS head, version lockstep and CI script self-tests all pass, read off the run rather than taken from the body. The local builds and test runs above were on the six-way merged tree and on control trees, not on the PR head. Not merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant