Repository navigation
refactor(layering): the Logged Data pane's markup code-behind was opening a database - #428
Conversation
…a database (WIP) Move the session-rename write out of LoggedDataPanePrototype.axaml.cs and into LoggingManager, which already owns the sessions the pane lists and the context factory they live in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoMove session rename persistence out of Logged Data code-behind
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Code Review by Qodo
1.
|
Qodo round 1, Medium: the summary said a blank name is stored as null and restores the default label on reload. Sessions.Name is nullable: false, so that save throws — the very behaviour this PR's own test pins and #427 tracks. The doc now states what the method does and points at the issue. Comment lines only; the executable body is byte-identical, so round 1's mutation controls still hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 453323e |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 453323e |
|
Qodo-clean and CI green on Three rounds, stated honestly rather than rounded up:
Round 2 is graded What I read by hand to cover the Settled before saying so: the summary has been unchanged since CI green on this exact head, all six jobs: Desktop head on macOS, iOS head, Android head, |
Shepherd pass on
|
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 453323e |
Eight-way merge build —
|
| after | tree | previously recorded |
|---|---|---|
| +#418 #419 #420 #422 #423 | 8599ae34 |
8599ae34 ✅ |
| +#425 | fd7c5e1b |
fd7c5e1b ✅ |
| +#426 | 1369c47c |
1369c47c ✅ |
| +#428 | 2fb26b57 |
new |
Gate on the merged tree
- Desktop head:
0 Error(s),succeeded with 150 warning(s). - iOS head, Release
iossimulator-arm64:0 Error(s),succeeded with 151 warning(s). Exit 0. packages.lock.jsonunchanged after the restore (git status --short -- '**/packages.lock.json'empty).- Suite: 1159 passed / 0 failed / 0 skipped.
- Visual gate: 39/39 byte-identical,
all 39 expected screens captured. The denominator is the
committed manifest's own line count —git show origin/main:tools/parity-audit/baselines/macos-arm64.sha256 | wc -l
→ 39 — so this is the whole manifest, not a subset. refactor(layering): the Logged Data pane's markup code-behind was opening a database #428 is the only one of the eight that
touches a.axaml.cs, and no.axamlmarkup changed in it at all. - Android not built (no SDK on this host); CI covers that head per-PR.
Suite reconciliation, re-derived rather than netted
main = 1116 (CI's own log on 4716ce1, not a local run). Each delta counted off the diff by
xunit row, then closed against the measured merged total:
| PR | rows | delta |
|---|---|---|
| #418 | ManualWifiEndpointTests 0 → 5 |
+5 |
| #419 | BootloaderHoldServiceTests 20 → 17 |
−3 (test-negative) |
| #420 | FirewallConfigurationTests 0 → 24 |
+24 |
| #422 | no test file touched | 0 |
| #423 | GraphImageSaveTests 13 → 13 |
0 |
| #425 | ConnectionManagerConnectResultTests 5 → 8 |
+3 |
| #426 | ChannelsPaneRebuildTests 0 → 10 |
+10 |
| #428 | SessionRenamePersistenceTests 0 → 4 |
+4 |
1116 + 5 − 3 + 24 + 0 + 0 + 3 + 10 + 4 = **1159**, which is exactly the measured merged total.
It also closes from the other end: #428 standalone is 1120 in CI's log on 453323e, and
1116 + 4 = 1120.
Two traps worth naming because I walked into one of them:
- test(#421): three rows that reported Passed without running now report Skipped #423 really is 0 on macOS. It converts three
[Fact]rows to[UnixOnlyFact]; on Unix they
still run, soSkippedis 0 throughout this run and the total does not move. A first count
read it as13 → 16—git grep -ccounts matching lines, and each converted row carries a
comment containing the literal text[UnixOnlyFact]. Three real attributes, three comments
mentioning them. The conversion is Windows-side and invisible here; the absence is not the change
failing to work. - A PR's standalone total is not its delta. refactor(layering): the Logged Data pane's markup code-behind was opening a database #428 reports 1120 against its own base; subtracting
that from anything is meaningless.
Removal audit — what no file-overlap check can see
#419, #420, #422 and #425 all remove something. Every removed member and type name searched across
the whole merged tree (.cs and .axaml, since a reflection binding is not compile-checked
here), each search positive-controlled against origin/main so a zero is a real zero:
| removed by | symbol | live references in the merged tree |
|---|---|---|
| #419 | IBootloaderHoldService.PauseForFlashAsync |
0 (one mention, in a test file's doc comment) |
| #420 | AdminChecker, IAdminChecker, NoOpMessageBoxService, IFirewallHelper, FirewallConfiguration.Set{AdminChecker,MessageBoxService,FirewallHelper} |
0 (one mention, in a code comment) |
| #422 | SerialStreamingDevice.SendScpiMessage, SdCardSessionImporter.ImportFromStreamAsync, DaqifiViewModel.EnsureAnyDeviceConnected, DuplicateDeviceDialogViewModel.SwitchToNewInterface |
0 (control on main: 2 hits in each of the 4 files) |
| #425 | DuplicateDeviceCheckResult.IsDuplicate |
0 (control on main: 7 hits) |
Note #425 did not delete DuplicateDeviceCheckResult — it converts the class to a
sealed record with positional members and makes DuplicateDeviceHandler nullable, dropping
IsDuplicate in favour of returning null. Its one out-of-file consumer,
ConnectionDialogViewModel.HandleDuplicateDevice, is in #418's file — the one genuine
cross-PR coupling in this set, and it compiles. WindowsFirewallWrapper likewise survives #420 as
a concrete class; only the interface went.
Added top-level types — #423's UnixOnlyFactAttribute and the four new test classes — collide with
nothing. (#418 adds no type: CreateForManualEndpointAsync is a new public static member on the
existing DaqifiStreamingDevice, and DaqifiDeviceFactory.DefaultTcpDataPort is Core's, adopted
rather than declared here.) Two green compiles across the merged tree are the check that proves it.
Conclusion: all eight are mutually mergeable in any order, and the combination builds, tests and
renders clean. Nothing here asks for a change to any of them.
|
Qodo-clean, CI green — ready for review.
Not merging. |
The
|
| Tree | first-party warnings | of which CS8601 |
|---|---|---|
origin/main (4716ce1, this PR's merge-base) |
135 | 15 |
this PR (453323e) |
135 | 15 |
So this PR's "135 = 135" is correct, and so is the sweep's "the CS8601 is #428's, not the
merge's". Both, because the instance moved. Diffing the two CS8601 sets rather than their
counts:
- View/Prototype/LoggedDataPanePrototype.axaml.cs(93,40): Possible null reference assignment.
+ Loggers/LoggingManager.cs(916,28): Possible null reference assignment.
That is the same assignment either side. sessionToUpdate.Name = newName in the pane code-behind
became tracked.Name = name in PersistSessionNameAsync; the parameter is string? and
LoggingSession.Name is a non-nullable string, exactly as it was before the move. One site
disappeared, one appeared, the total did not budge.
Three corrections to how it was handed forward:
- It is not a genuinely new first-party warning. It is the pre-existing one, carried along by
the code it annotates. Nothing on this PR made the null-assignment reachable that was not
already reachable onmain. - It is not a cross-PR interaction. It reproduces on this PR standalone against its own
merge-base, so there is nothing here the user needs to weigh before merging the stack. - The method is
PersistSessionNameAsync, notRenameSessionAsync.
The part worth keeping is why a count-based gate cannot see this: a one-for-one swap is
net-zero on every total, on either key, on this PR and in the merged tree. The only thing that
catches it is diffing the warning set, not the number — which is the same habit this file's
own history keeps arriving at from the other direction ("never quote a constant"). Worth adding
to the warning check rather than fixing here.
The underlying nullability is still worth an eventual fix, and the PR already documents the
behaviour it relays (#427, the null name that does not save). That is the right ticket for it;
a settled, green PR is not the place.
— shepherd for #432
Not merging — for review.
What the violation was costing
LoggedDataPanePrototype.axaml.cs— a markup code-behind — resolvedIDbContextFactory<LoggingContext>offApp.ServiceProviderin its constructor, opened aDbContext, found the row by primary key, and saved it. It was the only.axaml.csin the repo that referenced EF Core or the service locator at all:Three concrete costs, not a style objection:
Daqifi.Avalonia.Testscan construct a view, so the session-rename write was the one session write with no reachable test. Writing the tests below found that clearing a session's name has never worked —Sessions.Nameisnullable: false, EF writes the backing field rather thanLoggingSession.Name's substituting getter, and the save throwsNOT NULL constraint failed: Sessions.Name. The code-behind logged that as an Error and showed the user nothing, so it has been filing Sentry issues while the old name quietly came back at the next launch. Filed as Clearing a logged session's name never saves — the old name comes back at the next launch #427, deliberately not fixed here (see below).GetRequiredServicemeant the pane could not be built without a composed container.graph LR subgraph after V2["LoggedDataPanePrototype<br/>.axaml.cs"] -->|PersistSessionNameAsync| LM2["LoggingManager"] LM2 --> DB2[("LoggingContext")] end subgraph before V1["LoggedDataPanePrototype<br/>.axaml.cs"] -->|GetRequiredService| SP["App.ServiceProvider"] V1 -->|"DbContext + Sessions.FindAsync<br/>+ SaveChangesAsync"| DB1[("LoggingContext")] LM1["LoggingManager"] --> DB1 endThe fix
Move the write to
LoggingManager.PersistSessionNameAsync. That is not a new layer —LoggingManageralready ownsLoggingSessions(the collection this very pane lists), already holds the context factory, and already hasPersistSessionSampleCount, which is the same shape against the same table. The view keeps what is genuinely view work: theTextChangedhandler, the 250 ms debounce, theCancellationTokenSourceit cancels on the next keystroke, and the immediate in-memory update that keeps the row label live.The simplification in one line: the view layer now has zero database knowledge — two
usings, a field, a container lookup and a six-line EF block out, one call in.Daqifi.Avalonia/)+33 / −11+12 / −11Both are worth stating: raw, this is net-additive by 22 lines, and 21 of those 22 are not code — of the 33 raw added lines, 17 are
///XML doc, 3 are//comment, 1 is a blank separator and 12 are executable; all 11 removed lines are executable. So the logic is+12 / −11: one net line, in a better place. Tests add 160 lines in one new file.Behaviour is unchanged, and here is the evidence
The moved block is the same block.
CreateDbContextAsync→Sessions.FindAsync([id], ct)→ assign →SaveChangesAsync(ct): same order, same token, same null guard. The view'stry/catch/finallyis untouched, so a failure is still logged exactly where it was.Visual gate: 39/39 byte-identical against the committed
tools/parity-audit/baselines/macos-arm64.sha256, whose manifest holds 39 lines (git show origin/main:tools/parity-audit/baselines/macos-arm64.sha256 | wc -l→ 39) — so the denominator is the whole manifest, including all ninedesktop-min-*screens. That run is also what exercises the pane's construction without the removedGetRequiredService:desktop-2-loggeddataanddesktop-min-2-loggeddataboth render and both match.Warnings: no change, measured against my own merge-base. Both sides built
--no-incremental, first-party only (third_party/excluded), de-duplicated on (file, line, col, code, message). 135 instances on each side, on both heads. The only movement is two pre-existingCS8601s relocating with the code they annotate:mainLoggedDataPanePrototype.axaml.cs(77,24),(93,40)(73,24)LoggingManager.cs(916,28)Nothing added, nothing removed. (A distinct-by-file count reads 103 → 104 purely because that weaker key drops the line number while the file changed; the instance-level diff above is the honest one.)
Build gate green: desktop head, and iOS Release
iossimulator-arm64.packages.lock.jsonunchanged after restore. Android not built on this host — CI covers it.Tests: 1120 passed, 0 failed, whole suite, not a
--filtersubset.About the tests
Four rows in
Daqifi.Avalonia.Tests/Loggers/SessionRenamePersistenceTests.cs. They are characterization, not regression: the behaviour is unchanged by the move, so the usual "run them againstorigin/mainfirst" check cannot apply — they namePersistSessionNameAsync, which does not exist there. That is "does not compile", not "0 failed", and none of them is evidence that the move fixed anything.So they were mutation-tested instead, on this branch, each mutation applied to the new method alone. Four mutations, four kills, each hitting exactly the row whose name claims that behaviour:
SaveChangesAsyncremovedA_typed_name_reaches_the_sessions_row+Clearing_the_box_fails_to_persist_today…name→name ?? "Session"Clearing_the_box_fails_to_persist_today…onlyif (tracked != null)guard removedRenaming_a_session_that_is_no_longer_there…onlyCancellationTokendropped from both awaitsA_cancelled_write_leaves_the_row_aloneonlyThe seeded row starts named
"Session", so every assertion moves the value off a value it already held — an assertion that only ever sees a default proves nothing.StoredName()reads straight out of SQLite rather than back through EF, so a change that only ever reached a change-tracker cannot pass. The file was restored from backup andtouched after the mutation run, and the full suite re-run green on the restored tree.Clearing_the_box_fails_to_persist_today_and_leaves_the_old_name_in_placedeliberately asserts the broken behaviour and names #427. Fixing it is a schema-or-storage decision (nullable column vs storing"") and would make this a behaviour-changing PR; when that lands, that row is the one to rewrite.Not touched
DaqifiViewModel.csandConnectionDialogViewModel.csare both under open PRs and neither is in this diff. The obvious alternative fix — routing the rename through aDaqifiViewModelcommand — would have needed one of them, which is why the write lands onLoggingManagerinstead.LoggingManageris arguably the better home regardless: it is what the mobile head would reach for.Survey note
The rest of the layering sweep came back clean, which is worth recording so nobody re-runs it. The UI trees contain no protobuf or SCPI message construction at all —
MessageProducer/MessageConsumerappear only inConnectionManagerand the three device classes (positive-controlled: the same search finds them there). EverySCPI/Protobufhit in.axamland in the view-models is a user-facing label, a comment, orSdCardLogFormat.Protobuf, a Core enum the user picks — Core's public surface working as intended.SdCardFailureClassifiertranslates Core's typed exceptions into user guidance, which is the correct shape rather than a violation.Review
Round 1 was graded
⚖️ Balancedand raised one Medium, which was correct and conceded: the XMLdoc on
PersistSessionNameAsyncpromised that a blank name is stored asnulland restores thedefault label on reload — the exact behaviour this PR's own test proves does not work. I wrote that
line before running the tests that found #427 and never went back to it. The doc now states the real
contract and points at the issue.
That fix is XML doc comment lines only — the executable body is byte-identical, so round 1's
mutation controls still hold verbatim. Re-verified on the new head anyway: both builds green, full
suite 1120/0, warning set unchanged (135 each side, both heads). The visual gate was not re-run for
a comment-only change, and no rendered surface is reachable from it.
🤖 Generated with Claude Code