Skip to content

test(abstraction-improver): use the SD-card importer seam instead of deleting it — 11 tests over the Import All loop - #437

Merged
tylerkron merged 2 commits into
mainfrom
claude/ecstatic-chaplygin-4658b2
Oct 27, 2026
Merged

tylerkron merged 2 commits into
mainfrom
claude/ecstatic-chaplygin-4658b2

Conversation

@tylerkron

@tylerkron tylerkron commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

The investigation

Looked at ISdCardSessionImporter and DeviceLogsViewModel's _importerOverride / ResolveImporter() as a possible twin of the firewall bootstrap seams deleted in #420. On the surface it matched: one interface, one implementation, no fake anywhere in the source tree, and a constructor parameter that every existing test passes null to.

It is not the same shape, and deleting it would have been a loss.

The seam is the only route into ImportAllFiles, which holds the decision SdCardFailure.IsCardUnavailable exists for: a failure that may be about one file is skipped and the batch carries on, while a failure that is about the card stops the run. SdCardFailureClassifier documents at length why that line is drawn where it is — and getting it backwards is silent and expensive in both directions. The listing comes back in the same order every time, so aborting on a per-file fault makes every file listed after it permanently unreachable through IMPORT ALL; carrying on past a card-wide one grinds the user through one multi-second failure per remaining file.

ImportAllSummaryTests covers the other half — how a finished ImportAllOutcome is worded — but it builds the outcome by hand. Nothing exercised the loop that fills it. Flattening the interface would have made that loop untestable for good.

So this PR uses the seam rather than deleting it. One production change, a test seam only: the internal constructor also takes the IMessageBoxService (null → AvaloniaMessageBoxService, exactly as before; the field moves from static to instance). It is how the tests read the Import All summary, which is the only place the batch's skip/abort accounting becomes observable — added in d50ef98 after Qodo R1 showed the no-session row could not see that accounting at all.

What is now covered

Through a fake importer on the existing internal constructor:

  • every listed file is asked for, in order
  • a per-file fault (SdCardTransferErrorException) is skipped and the batch carries on
  • a card-wide fault (SdCardNotPresentException) stops it
  • a stall ends the batch only for the two reasons that are evidence about the device (TransferTimeout, TransportClosed), not for the per-read NoDataReceived — the case a healthy device produces on a momentary gap
  • a result carrying no persisted session is a skip, not a stop — and is counted as one in the summary (Imported 0 of 3, Skipped 3, each name listed, guidance once)
  • a per-file fault is named in the summary's skip list; a card-wide one is reported as Import stopped at <file>: and not as a skip; a mid-batch disconnect says so
  • a device that drops mid-batch stops the run at the per-iteration connectivity re-check
  • a per-file failure leaves the card panel alone (HasFiles requires SdCardState == Ok, so painting it would hide the list the batch is still importing into) while a card-wide one paints it
  • ImportFile's device snapshot: a failure is not painted onto a device the user switched to while the download was in flight

Mutation-tested

Eight deliberate breaks, each caught by at least one test:

Mutation Result
card-wide failure no longer stops the batch 3 failed
per-file failure now stops the batch 2 failed
mid-batch connectivity re-check removed 1 failed
panel-painting guard widened past IsCardUnavailable 1 failed
ReferenceEquals(SelectedDevice, device) half of that guard dropped 1 failed
ImportFile reads the live selection instead of its snapshot 1 failed
no-session path drops outcome.RecordSkip (keeps continue) — green before d50ef98 3 failed
catch path drops outcome.RecordSkip — green before d50ef98 1 failed

What is deliberately not covered, and why

The success path. A persisted import calls AddImportedSession, which reaches LoggingManager.Instance — a singleton whose constructor resolves off App.ServiceProvider, null outside the app host — through Dispatcher.UIThread.Invoke, which never returns in a process with no pumped dispatcher. Both are global, neither is injectable here, and this test project stands up no application lifetime by design (see the comment at the top of its csproj).

A seventh mutation confirmed this rather than assuming it: dropping the skip path's continue pushes execution into AddImportedSession and hangs instead of failing. The test class documents the limitation in its remarks, so the next person does not rediscover it by watching CI time out.

Covering it honestly would mean injecting LoggingManager into the view model — a production change, and a larger one than this investigation warrants. Flagged rather than done.

Notes

Gate

Desktop build → restore → iOS head -c Release -r iossimulator-arm64 --no-restore → dotnet test. All exit 0; 1156 passed, 0 failed — re-run by the shepherd on d50ef98 (test count unchanged: the fix adds assertions to existing rows, not new rows).

🤖 Generated with Claude Code

…deleting it — 11 tests over the Import All loop

Investigated `ISdCardSessionImporter` and `DeviceLogsViewModel`'s `_importerOverride` /
`ResolveImporter()` as a possible twin of the firewall seams deleted in #420: one interface,
one implementation, no fake anywhere, and a constructor parameter every existing test passes
null to.

It is not the same shape. The seam is the only route into `ImportAllFiles`, which holds the
decision `SdCardFailure.IsCardUnavailable` exists for — a failure that may be about one file is
skipped and the batch carries on, while a failure about the card stops the run. Getting that
backwards is silent and expensive in both directions: the listing comes back in the same order
every time, so aborting on a per-file fault makes every file after it permanently unreachable
through IMPORT ALL, and carrying on past a card-wide one grinds the user through one
multi-second failure per remaining file.

`ImportAllSummaryTests` covers the other half — how a finished `ImportAllOutcome` is worded —
but it builds the outcome by hand, so nothing exercised the loop that fills it. Deleting the
seam would have made that loop untestable for good, which is a loss, not a simplification.

So this uses it. No production file changes.

Covered, via a fake importer on the existing internal constructor:
- every listed file is asked for, in order;
- a per-file fault (`SdCardTransferErrorException`) is skipped and the batch carries on;
- a card-wide fault (`SdCardNotPresentException`) stops it;
- a stall ends the batch only for the two reasons that are evidence about the device
  (`TransferTimeout`, `TransportClosed`) and not for the per-read `NoDataReceived`;
- a result carrying no persisted session is a skip, not a stop;
- a device that drops mid-batch stops the run at the iteration re-check;
- a per-file failure leaves the card panel alone (`HasFiles` requires `SdCardState == Ok`, so
  painting it would hide the list the batch is still importing into) while a card-wide one
  paints it;
- `ImportFile`'s device snapshot: a failure is not painted onto a device the user switched to
  while the download was in flight.

Mutation-tested. Six deliberate breaks of those decisions each failed at least one test:
inverting the `IsCardUnavailable` abort either way, dropping the mid-batch connectivity
re-check, widening the panel-painting guard past `IsCardUnavailable`, dropping its
`ReferenceEquals(SelectedDevice, device)` half, and reading the live selection in place of
`ImportFile`'s snapshot.

The success path is deliberately absent, and the test class says so: a persisted import calls
`AddImportedSession`, which reaches `LoggingManager.Instance` — a singleton whose constructor
resolves off a null `App.ServiceProvider` outside the app host — through
`Dispatcher.UIThread.Invoke`, which never returns in a process with no pumped dispatcher. Both
are global, neither is injectable, and this project stands up no application lifetime by
design. A seventh mutation (dropping the skip path's `continue`) confirmed it: it hangs rather
than fails.

`RecordingStreamingDevice` gains settable `IsConnected` and `SdCardFiles`, both defaulting to
what they returned before.

Gate: desktop build, restore, iOS head Release build, `dotnet test` — all green, 1156 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Test SD-card Import All batch decisions through importer seam

🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Exercises Import All ordering, skip, abort, disconnect, and stall decisions through importer seam.
• Verifies card errors remain scoped to the originating device and failure severity.
• Extends the test device with configurable connectivity and SD-card listings.
Diagram

graph TD
  Tests["Batch tests"] --> Device["Configurable device"] --> VM["Device Logs VM"] --> Connected{"Still connected?"}
  Connected -->|Yes| Importer["Fake importer"] --> Decision{"Batch decision"}
  Decision -->|Skip or file fault| Continue["Continue batch"] --> Connected
  Decision -->|Card fault| Stop["Stop batch"]
  Connected -->|No| Stop
Loading
High-Level Assessment

Using the existing importer injection seam is the best approach because it directly exercises production batch orchestration while avoiding database, application-host, and UI-dispatcher globals. Full integration tests would be slower and less deterministic, while deleting the seam would leave the loop’s critical stop-versus-continue behavior isolated from regression testing.

Files changed (2) +335 / -2

Tests (2) +335 / -2
RecordingStreamingDevice.csMake connection state and SD listings configurable in tests +12/-2

Make connection state and SD listings configurable in tests

• Replaces fixed connected and empty-listing properties with internally settable test state. Defaults preserve existing behavior while enabling mid-batch disconnects and populated Device Logs scenarios.

Daqifi.Avalonia.Tests/Device/RecordingStreamingDevice.cs

DeviceLogsImportBatchTests.csCover Import All batch control flow through a fake importer +323/-0

Cover Import All batch control flow through a fake importer

• Adds 11 cases covering ordered requests, file-scoped skips, card-wide aborts, stall classifications, non-persisted results, and mid-batch disconnects. It also verifies card-panel state and prevents an in-flight failure from being attributed to a newly selected device.

Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs

@qodo-code-review

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Empty-log miscounting stays undetected ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
A_file_that_produced_no_session_is_skipped_and_the_batch_carries_on only asserts that all three
importer calls occur, without observing the ImportAllOutcome populated by RecordSkip. Removing
that accounting while retaining continue would leave the test green but make the completion
summary omit every empty log, so the central distinction between skipped and imported files remains
unprotected.
Code

Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs[R135-138]

+        await vm.ImportAllFilesCommand.ExecuteAsync(null);
+
+        // Every response this fake gives is a non-persisted one, so all three took the skip path.
+        Assert.Equal([FileA, FileB, FileC], importer.Requested);
Relevance

●●● Strong

Accepted precedents strengthen tests to verify specific outcomes, not merely counts or call order.

PR-#247
PR-#338

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The fake's default response explicitly sets SessionPersisted to false, while the production loop
is supposed to call RecordSkip for that value before continuing. The new assertion checks only
importer request order, so it cannot distinguish correct skip accounting from merely continuing
without recording the files.

Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs[131-139]
Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs[312-319]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[684-695]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The no-session batch test verifies only that later files are attempted, not that the no-session result is recorded as skipped. A regression that removes `outcome.RecordSkip` but keeps the loop running would therefore pass while producing an incorrect completion summary.

## Fix Focus Areas
- Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs[131-139]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[684-695]

## Recommended Fix
Refactor the import loop into an internal asynchronous method that returns its populated `ImportAllOutcome`, while leaving the command responsible for displaying the resulting summary. Exercise that method from this test and assert that all three file names are present in `SkippedFiles`, `ImportedCount` remains zero, and no abort is recorded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Cross-repo context — repo relationships
  Explored: repo: daqifi/daqifi-core (sha: 2f3e095c)
Review mode: ⚖️ Balanced: This push changes production constructor/message-box behavior and adds assertions across multiple import outcome paths, creating real behavioral risk despite its localized scope.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit d50ef98 ⚖️ Balanced

Results up to commit 0a4a285 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Remediation recommended
1. Empty-log miscounting stays undetected ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
A_file_that_produced_no_session_is_skipped_and_the_batch_carries_on only asserts that all three
importer calls occur, without observing the ImportAllOutcome populated by RecordSkip. Removing
that accounting while retaining continue would leave the test green but make the completion
summary omit every empty log, so the central distinction between skipped and imported files remains
unprotected.
Code

Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs[R135-138]

+        await vm.ImportAllFilesCommand.ExecuteAsync(null);
+
+        // Every response this fake gives is a non-persisted one, so all three took the skip path.
+        Assert.Equal([FileA, FileB, FileC], importer.Requested);
Relevance

●●● Strong

Accepted precedents strengthen tests to verify specific outcomes, not merely counts or call order.

PR-#247
PR-#338

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The fake's default response explicitly sets SessionPersisted to false, while the production loop
is supposed to call RecordSkip for that value before continuing. The new assertion checks only
importer request order, so it cannot distinguish correct skip accounting from merely continuing
without recording the files.

Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs[131-139]
Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs[312-319]
Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[684-695]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The no-session batch test verifies only that later files are attempted, not that the no-session result is recorded as skipped. A regression that removes `outcome.RecordSkip` but keeps the loop running would therefore pass while producing an incorrect completion summary.

## Fix Focus Areas
- Daqifi.Avalonia.Tests/ViewModels/DeviceLogsImportBatchTests.cs[131-139]
- Daqifi.Avalonia/Daqifi.Desktop/ViewModels/DeviceLogsViewModel.cs[684-695]

## Recommended Fix
Refactor the import loop into an internal asynchronous method that returns its populated `ImportAllOutcome`, while leaving the command responsible for displaying the resulting summary. Exercise that method from this test and assert that all three file names are present in `SkippedFiles`, `ImportedCount` remains zero, and no abort is recorded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit d50ef98 ⚖️ Balanced


No changes from previous review

Grey Divider

Qodo Logo

…asked for

Qodo R1 (Medium): the no-session test checked only that all three files were requested, so dropping outcome.RecordSkip while keeping the continue left it green. The summary dialog is the only place the batch's skip/abort accounting becomes observable, so the internal test-seam constructor now also takes the IMessageBoxService (defaulting to AvaloniaMessageBoxService, as before) and the batch tests read the Import Complete text.

Mutations: dropping the no-session RecordSkip fails 3; dropping the catch's RecordSkip fails 1. Both were green before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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 d50ef98

@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 d50ef98

@tylerkron

Copy link
Copy Markdown
Contributor Author

Qodo-clean, CI green — ready for review. Head d50ef98255b621ffbf839ff108ed4ec7a1e33727. Not merging.

What the builder left. One Qodo round on 0a4a285 (⚖️ Balanced), carrying a valid Medium that nobody had answered: the no-session row asserted only which files were requested, so dropping outcome.RecordSkip while keeping the continue left all 11 rows green. I checked this before conceding it. The catch path's RecordSkip had the same hole.

Fix, d50ef98. The internal test-seam constructor now also takes an IMessageBoxService. Passing null still gives AvaloniaMessageBoxService, so production behaviour does not change; the field moves from static to instance. The batch rows now assert on the Import Complete dialog text. Two mutations, both green before this commit: dropping the no-session RecordSkip now fails 3 rows, and dropping the catch's RecordSkip fails 1. I updated the PR body in place: it used to say "no production file changes", and the mutation table now has 8 rows.

Qodo on d50ef98:

Round Trigger Review landed Result Grade
R2 20:15:15Z 20:17:13Z Bugs 0 / Rule violations 0; R1 finding marked ✓ Resolved ⚖️ Balanced (rationale describes this push)
R3 (unchanged head) 20:20:13Z 20:21:23Z Bugs 0 / Rule violations 0 ⚖️ Balanced (new rationale; Previous reviews now lists d50ef98)

Both reviews name d50ef98, and both are real reviews, not the placeholder. I re-checked at 20:24:27Z, after the settle wait: same state. Thread surface: 1 qodo thread, resolved, 0 unresolved. The author filter was validated against #196 and returns 3.

Gate, run locally on d50ef98 under build.1: desktop build, iOS restore, iOS Release -r iossimulator-arm64 --no-restore, and the full dotnet test all exit 0, with 1156 passed / 0 failed. The count did not change because the fix adds assertions, not rows.

CI run 35649810309 ran on d50ef98, merged into current main 97be4df. The checkout line reads Merge d50ef98… into 97be4df…. All six jobs pass and the dependabot job skips. Tests: 1190 = main 1179 + this PR's 11.

The three-way merge with #435 and #436 is in the next comment.

🤖 Generated with Claude Code

@tylerkron

Copy link
Copy Markdown
Contributor Author

Three-way merge: main + #435 + #436 + #437

Built on a throwaway branch on top of origin/main 97be4df. The branches were merged in this order: #435 2bacbf3, #436 b36c347, #437 d50ef98. All three merged with no conflicts. Result: commit 81ff5dc, tree bd5c5110b4d5058b414293b8a8879f660dcb0987.

Gate on the merged tree: desktop build, iOS restore, iOS Release -r iossimulator-arm64 --no-restore, and the full dotnet test all exit 0. I did not build the Android head locally.

Test count reconciles: 1199 passed / 0 failed / 0 skipped = main 1179 (I measured it) +3 (#435) +6 (#436) +11 (#437).

Ready notes against current heads:

PR Ready-note SHA Head now CI green on head CI merge base
#435 2bacbf3 2bacbf3 ✓ yes (run 35545404407) 4716ce1, before #418 #419 #420 #422 #423 #425 #426 #428 #431 #432
#436 b36c347 b36c347 ✓ yes (run 35547335403) 4716ce1, same 10 merges missed
#437 this PR's note above d50ef98 ✓ yes (run 35649810309) 97be4df = current main

So the greens for #435 and #436 come from before the last ten merges to main. This merged tree is the build that covers them against current main. It passes on desktop, iOS and tests, but not Android.

Collision audit (what changed across the three PRs):

🤖 Generated with Claude Code

@tylerkron

Copy link
Copy Markdown
Contributor Author

Sweep: five-way merge of main + #435 + #436 + #437 + #443 + #446 is clean

I built all five PRs merged onto origin/main 97be4df, using the heads 2bacbf3, b36c347, d50ef98, 9e29bb4 and 40bfef0. Nothing was pushed and nothing was merged.

@tylerkron

Copy link
Copy Markdown
Contributor Author

Sweep update: the six-way merge of main 97be4df + #435 + #436 + #437 + #443 + #446 + #449 is clean. The tree is b5f3d4d7, and the five-way 27e0b21c reproduced exactly. The full gate, HeadlessBench --no-incremental, 1208/1208 tests and parity 39/39 all pass, and this PR's head is unchanged. Details are on #449.

@tylerkron
tylerkron added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 6dff1d8 Oct 27, 2026
7 checks passed
@tylerkron
tylerkron deleted the claude/ecstatic-chaplygin-4658b2 branch October 27, 2026 03:56
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