Skip to content

fix(settings): Apply manual channel writes in order#6077

Merged
jamesarich merged 10 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/manual-channel-write-sequencing
Jul 6, 2026
Merged

fix(settings): Apply manual channel writes in order#6077
jamesarich merged 10 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/manual-channel-write-sequencing

Conversation

@jeremiah-k

@jeremiah-k jeremiah-k commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR makes manual channel saves deterministic by writing channel changes in order instead of allowing overlapping write batches.

Previously, multi-channel edits could enqueue several radio writes without pacing or serialization. On constrained links, that made save, delete, and reorder flows more fragile and allowed overlapping saves to interleave.

This change builds an ordered channel update plan, applies writes sequentially, keeps the response state tied to the full batch, and reconciles already-enqueued writes if a batch is interrupted.

Key Changes

  • Built manual channel update plans in channel index order.
  • Applied manual channel writes sequentially.
  • Added pacing between channel writes.
  • Serialized overlapping manual channel save batches.
  • Computed queued save plans inside the mutex against the current canonical channel list.
  • Kept batch success from firing while paced writes are still being enqueued.
  • Surfaced enqueue failures as user-visible errors.
  • Reconciled already-enqueued channel writes if a batch is interrupted or cancelled mid-plan.
  • Updated local channel state only after ordered write requests are enqueued.
  • Avoided committing partial final state when a write enqueue fails mid-plan.
  • Preserved local channel migration behavior for the local node path.
  • Added focused coverage for ordering, pacing, serialization, interrupted batches, and partial failures.

Testing

  • Added coverage proving channel writes are ordered by index.
  • Added coverage proving overlapping saves do not interleave.
  • Added coverage proving pacing happens between writes.
  • Added coverage proving no delay is added after the final write.
  • Added coverage proving batch success is held until enqueueing completes.
  • Added coverage proving enqueue failure surfaces an error.
  • Added coverage proving interrupted writes reconcile already-applied channel settings.
  • Added coverage proving queued saves diff against current canonical state.
  • Verified git diff --check passes.

Migration Notes

  • No user data migration is required.
  • Existing channel data is unchanged until the user edits and saves channels.
  • This only changes the ordering, pacing, and recovery behavior of manual channel save writes.

Summary by CodeRabbit

  • New Features

    • Channel updates are now written as a single ordered batch, ensuring consistent results when multiple channels change together.
    • Channel-save progress is more accurate during the batch lifecycle, including pacing between writes.
  • Bug Fixes

    • Prevents overlapping channel-save operations from interleaving, improving reliability.
    • Improved partial-failure handling: if a batch fails mid-sequence, settings are reconciled to the already-applied channels to keep state consistent.
  • Tests

    • Expanded coroutine and timing assertions to verify ordered write behavior, progress transitions, and interruption recovery.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

RadioConfigViewModel now serializes manual channel saves into a paced ordered batch, tracks batch progress and completion separately, and reconciles partial writes on interruption. Tests cover ordering, progress, failure handling, overlap serialization, and interruption behavior.

Changes

Manual Channel Write Batching

Layer / File(s) Summary
Batch primitives and write plan
feature/settings/.../RadioConfigViewModel.kt
Adds pacing, synchronization, batch-state flags, and internal helpers for sequential channel writes, packet-id registration, and partial-write interruption reporting.
Serialized updateChannels flow
feature/settings/.../RadioConfigViewModel.kt
updateChannels builds an ordered manual plan, runs it under a mutex, starts and finalizes batch loading, and reconciles canonical or partial local settings; packet-response completion now uses batch-aware progress handling.
Tests for ordering and interruption
feature/settings/.../RadioConfigViewModelTest.kt
Adds coroutine test utilities and coverage for ordered writes, progress completion, write failure reconciliation, overlapping updates, pacing, and interruption callbacks.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: ordered manual channel writes in settings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bugfix PR tag label Jul 2, 2026
@jeremiah-k
jeremiah-k force-pushed the bugfix/manual-channel-write-sequencing branch from fe56793 to 48db0f7 Compare July 2, 2026 23:13
@jeremiah-k jeremiah-k changed the title fix(settings): Apply manual channel writes sequentially fix(settings): Apply manual channel writes in order Jul 2, 2026
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 3, 2026 00:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt (1)

397-418: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No guard against overlapping updateChannels invocations.

Each call to updateChannels launches its own independent safeLaunch coroutine that sequentially writes the whole plan. If it's invoked twice in quick succession (e.g., a double-tap save, or a save triggered while a prior save is still pacing through its 1s-per-write delays), the two write sequences can interleave on the wire, undermining the very ordering guarantee this PR introduces. Consider guarding with an in-flight Job/Mutex so a new save either cancels or awaits the previous one before starting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`
around lines 397 - 418, The updateChannels flow in RadioConfigViewModel can
start multiple overlapping safeLaunch saves, which can interleave
setRemoteChannels writes and break the intended ordering. Add an in-flight guard
around updateChannels—such as a Job check/cancel or a Mutex/serialization
mechanism—so a new call either waits for or cancels the previous channel-save
coroutine before applying applyManualChannelUpdatePlan and the subsequent
packetRepository/radioConfigRepository updates.
feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt (1)

392-425: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

verifySuspend(exactly(0)) assertions for migrate/replaceAllSettings don't actually exercise the failure-gating behavior.

destNum resolves to 123 but myNodeInfo is never set in this test, so destNum == myNodeNum is false regardless of whether the write plan succeeds or fails — migrateChannelsByPSK/replaceAllSettings would never be called either way. Only the assertEquals(old, viewModel.radioConfigState.value.channelList) line meaningfully proves the failure path. To actually validate that a write failure prevents migration/replace, set myNodeInfo so the branch would be taken on success, proving the failure suppresses it.

✅ Suggested fix
     fun `updateChannels keeps canonical channel list unchanged when ordered write fails`() = runTest {
         val node = Node(num = 123, user = User(id = "!123"))
         val channelA = ChannelSettings(name = "A")
         val channelB = ChannelSettings(name = "B")
         val channelC = ChannelSettings(name = "C")
         val channelD = ChannelSettings(name = "D")
         val old = listOf(channelA, channelB, channelC, channelD)
         val new = listOf(channelA, channelD)
         val writtenIndexes = mutableListOf<Int>()

         every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet(settings = old))
         nodeRepository.setNodes(listOf(node))
+        nodeRepository.setMyNodeInfo(myNodeInfo(myNodeNum = 123))
         viewModel = createViewModel()
         runCurrent()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`
around lines 392 - 425, The failure-path test in RadioConfigViewModelTest is not
proving that a failed channel write blocks downstream work because `myNodeInfo`
is never set, so `updateChannels` can’t reach the
`migrateChannelsByPSK`/`replaceAllSettings` branch even on success. Update the
test setup in `updateChannels keeps canonical channel list unchanged when
ordered write fails` to initialize `myNodeInfo` (so `destNum == myNodeNum` would
allow the success path), then keep the simulated `setRemoteChannel` failure and
assert that `radioConfigState.channelList` stays unchanged while the
migration/replacement calls are skipped.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`:
- Around line 397-418: The updateChannels flow in RadioConfigViewModel can start
multiple overlapping safeLaunch saves, which can interleave setRemoteChannels
writes and break the intended ordering. Add an in-flight guard around
updateChannels—such as a Job check/cancel or a Mutex/serialization mechanism—so
a new call either waits for or cancels the previous channel-save coroutine
before applying applyManualChannelUpdatePlan and the subsequent
packetRepository/radioConfigRepository updates.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`:
- Around line 392-425: The failure-path test in RadioConfigViewModelTest is not
proving that a failed channel write blocks downstream work because `myNodeInfo`
is never set, so `updateChannels` can’t reach the
`migrateChannelsByPSK`/`replaceAllSettings` branch even on success. Update the
test setup in `updateChannels keeps canonical channel list unchanged when
ordered write fails` to initialize `myNodeInfo` (so `destNum == myNodeNum` would
allow the success path), then keep the simulated `setRemoteChannel` failure and
assert that `radioConfigState.channelList` stays unchanged while the
migration/replacement calls are skipped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 74714fdc-e6fb-4bbd-9422-017cf45b1a1e

📥 Commits

Reviewing files that changed from the base of the PR and between d8afe84 and 48db0f7.

📒 Files selected for processing (2)
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
  • feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt

@jeremiah-k
jeremiah-k marked this pull request as draft July 3, 2026 00:56
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 3, 2026 01:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt (1)

392-467: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated channel/old-list setup across tests.

The channelA/channelB/channelC/channelD + old list boilerplate is duplicated verbatim across the failure test and the overlapping-saves test. Consider extracting a small helper (e.g. fun fourChannelFixture(): List<ChannelSettings>) to reduce duplication as more manual-channel tests are added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`
around lines 392 - 467, The `updateChannels` test setup repeats the same
four-channel fixture (`channelA`/`channelB`/`channelC`/`channelD` and the `old`
list) in multiple tests, so extract that shared setup into a small helper in
`RadioConfigViewModelTest` and reuse it from both `updateChannels keeps
canonical channel list unchanged when ordered write fails` and `updateChannels
serializes overlapping channel saves` to keep the tests DRY and easier to
extend.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`:
- Around line 392-467: The `updateChannels` test setup repeats the same
four-channel fixture (`channelA`/`channelB`/`channelC`/`channelD` and the `old`
list) in multiple tests, so extract that shared setup into a small helper in
`RadioConfigViewModelTest` and reuse it from both `updateChannels keeps
canonical channel list unchanged when ordered write fails` and `updateChannels
serializes overlapping channel saves` to keep the tests DRY and easier to
extend.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ea5a8f0-6f76-4da9-8e55-adba1f3dca58

📥 Commits

Reviewing files that changed from the base of the PR and between 48db0f7 and d2a03c6.

📒 Files selected for processing (2)
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
  • feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sequencing fix itself is right — the concurrent per-slot safeLaunch writes were the real bug — but the 1s pacing opens response-state races the concurrent code never had, and the failure paths don't do what the PR body says yet (details inline).

Minor notes:

  • sortedBy { it.index } in getManualChannelUpdatePlan is a no-op — getChannelList iterates 0..max ascending, so the output is already index-sorted.
  • ManualChannelUpdateResult's return value is ignored by the only production caller; the tests could assert on the registerRequestId capture instead.

@jeremiah-k
jeremiah-k marked this pull request as draft July 5, 2026 20:32
@jeremiah-k
jeremiah-k force-pushed the bugfix/manual-channel-write-sequencing branch from e2e5d73 to 7ee77b9 Compare July 6, 2026 12:13
@jeremiah-k

Copy link
Copy Markdown
Contributor Author

@jamesarich Thanks, this was a good catch.

I pushed a rebased update that tightens the ordered-save path around the cases you called out. The save plan is now computed inside the mutex against current canonical state, overlapping saves are serialized, batch success is held until enqueueing completes, enqueue failures surface an error, and interrupted batches reconcile the writes that were already sent instead of leaving app/device state silently divergent.

I also added focused coverage for the premature-success path, overlapping saves, enqueue failure, stale-baseline saves, and interrupted reconciliation.

@jeremiah-k
jeremiah-k marked this pull request as ready for review July 6, 2026 13:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt (1)

508-536: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a direct unit test for the interrupted/partial-write path of applyManualChannelUpdatePlan.

This test only covers the happy-path pacing behavior. The onInterrupted branch (partial appliedSettings/appliedWriteCount on mid-plan failure) is currently only exercised indirectly through the updateChannels integration test at Line 438. A focused unit test calling applyManualChannelUpdatePlan directly with a failing writeChannel would pin down that contract independently of the view model's mutex/state-machine plumbing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`
around lines 508 - 536, Add a focused unit test for the interrupted path of
applyManualChannelUpdatePlan by making writeChannel fail on a mid-plan channel
and asserting the onInterrupted result. Verify the partial appliedSettings and
appliedWriteCount returned by applyManualChannelUpdatePlan when the write stops
early, so this contract is covered independently of updateChannels and the view
model mutex/state-machine logic.
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt (1)

1091-1091: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Misleading field: appliedSettings holds finalSettings, not what was applied.

The success-path result sets appliedSettings = finalSettings, while the tracked appliedSettings mutable list (and InterruptedManualChannelUpdate.appliedSettings) means "settings actually enqueued so far." A future caller reading ManualChannelUpdateResult.appliedSettings would get the intended target, not the applied state. Since the return value is currently unused by updateChannels, consider renaming this field (e.g. finalSettings) to avoid a latent footgun.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`
at line 1091, The success result from ManualChannelUpdateResult is misleading
because appliedSettings is populated with finalSettings rather than the settings
actually applied so far. Update the ManualChannelUpdateResult type and its
construction in RadioConfigViewModel so the field name reflects its meaning,
such as finalSettings, and keep the
InterruptedManualChannelUpdate.appliedSettings naming only for the in-flight
applied state. Ensure all references to ManualChannelUpdateResult are updated
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`:
- Around line 646-649: The backup guard in RadioConfigViewModel is showing the
restore-failure string during a backup/save flow, which is misleading to users.
Update the snackbar call in the empty-key check inside the backup path to use a
backup-specific or neutral resource instead of key_backup_restore_failed. If no
suitable string exists, add one and use it in this branch so the message matches
the action being performed.

---

Nitpick comments:
In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`:
- Line 1091: The success result from ManualChannelUpdateResult is misleading
because appliedSettings is populated with finalSettings rather than the settings
actually applied so far. Update the ManualChannelUpdateResult type and its
construction in RadioConfigViewModel so the field name reflects its meaning,
such as finalSettings, and keep the
InterruptedManualChannelUpdate.appliedSettings naming only for the in-flight
applied state. Ensure all references to ManualChannelUpdateResult are updated
consistently.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`:
- Around line 508-536: Add a focused unit test for the interrupted path of
applyManualChannelUpdatePlan by making writeChannel fail on a mid-plan channel
and asserting the onInterrupted result. Verify the partial appliedSettings and
appliedWriteCount returned by applyManualChannelUpdatePlan when the write stops
early, so this contract is covered independently of updateChannels and the view
model mutex/state-machine logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d19d5008-b4e8-46d7-b2a9-8a9758328a8b

📥 Commits

Reviewing files that changed from the base of the PR and between e2e5d73 and 7ee77b9.

📒 Files selected for processing (2)
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
  • feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🧹 Nitpick comments (2)
feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt (1)

508-536: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a direct unit test for the interrupted/partial-write path of applyManualChannelUpdatePlan.

This test only covers the happy-path pacing behavior. The onInterrupted branch (partial appliedSettings/appliedWriteCount on mid-plan failure) is currently only exercised indirectly through the updateChannels integration test at Line 438. A focused unit test calling applyManualChannelUpdatePlan directly with a failing writeChannel would pin down that contract independently of the view model's mutex/state-machine plumbing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`
around lines 508 - 536, Add a focused unit test for the interrupted path of
applyManualChannelUpdatePlan by making writeChannel fail on a mid-plan channel
and asserting the onInterrupted result. Verify the partial appliedSettings and
appliedWriteCount returned by applyManualChannelUpdatePlan when the write stops
early, so this contract is covered independently of updateChannels and the view
model mutex/state-machine logic.
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt (1)

1091-1091: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Misleading field: appliedSettings holds finalSettings, not what was applied.

The success-path result sets appliedSettings = finalSettings, while the tracked appliedSettings mutable list (and InterruptedManualChannelUpdate.appliedSettings) means "settings actually enqueued so far." A future caller reading ManualChannelUpdateResult.appliedSettings would get the intended target, not the applied state. Since the return value is currently unused by updateChannels, consider renaming this field (e.g. finalSettings) to avoid a latent footgun.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`
at line 1091, The success result from ManualChannelUpdateResult is misleading
because appliedSettings is populated with finalSettings rather than the settings
actually applied so far. Update the ManualChannelUpdateResult type and its
construction in RadioConfigViewModel so the field name reflects its meaning,
such as finalSettings, and keep the
InterruptedManualChannelUpdate.appliedSettings naming only for the in-flight
applied state. Ensure all references to ManualChannelUpdateResult are updated
consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`:
- Around line 646-649: The backup guard in RadioConfigViewModel is showing the
restore-failure string during a backup/save flow, which is misleading to users.
Update the snackbar call in the empty-key check inside the backup path to use a
backup-specific or neutral resource instead of key_backup_restore_failed. If no
suitable string exists, add one and use it in this branch so the message matches
the action being performed.

---

Nitpick comments:
In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`:
- Line 1091: The success result from ManualChannelUpdateResult is misleading
because appliedSettings is populated with finalSettings rather than the settings
actually applied so far. Update the ManualChannelUpdateResult type and its
construction in RadioConfigViewModel so the field name reflects its meaning,
such as finalSettings, and keep the
InterruptedManualChannelUpdate.appliedSettings naming only for the in-flight
applied state. Ensure all references to ManualChannelUpdateResult are updated
consistently.

In
`@feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt`:
- Around line 508-536: Add a focused unit test for the interrupted path of
applyManualChannelUpdatePlan by making writeChannel fail on a mid-plan channel
and asserting the onInterrupted result. Verify the partial appliedSettings and
appliedWriteCount returned by applyManualChannelUpdatePlan when the write stops
early, so this contract is covered independently of updateChannels and the view
model mutex/state-machine logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d19d5008-b4e8-46d7-b2a9-8a9758328a8b

📥 Commits

Reviewing files that changed from the base of the PR and between e2e5d73 and 7ee77b9.

📒 Files selected for processing (2)
  • feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt
  • feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt
🛑 Comments failed to post (1)
feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt (1)

646-649: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wrong user-facing string on the backup-guard path.

This guard runs when a backup is attempted with empty keys, but it surfaces key_backup_restore_failed ("restore failed"), which is misleading during a save/backup action. Consider a dedicated backup-failed resource (or a more neutral message).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt`
around lines 646 - 649, The backup guard in RadioConfigViewModel is showing the
restore-failure string during a backup/save flow, which is misleading to users.
Update the snackbar call in the empty-key check inside the backup path to use a
backup-specific or neutral resource instead of key_backup_restore_failed. If no
suitable string exists, add one and use it in this branch so the message matches
the action being performed.

@jeremiah-k
jeremiah-k marked this pull request as draft July 6, 2026 13:19
Apply manual channel saves through a single ordered channel update sequence instead of launching one coroutine per changed slot. The update plan is sorted by channel index so delete, re-add, and reorder-style edits enqueue radio writes deterministically while preserving each row's ChannelSettings payload, including its PSK.

Register every returned request ID from the ordered write loop so existing packet response tracking remains in place. Local channel migration and cache replacement now run after the ordered send loop for local nodes, and the visible channel list is updated after the same sequence completes.

Add focused ViewModel coverage that records write order and verifies changed channel writes do not overlap when setRemoteChannel suspends.
Apply a one-second pace between manual channel write requests so delete and reorder batches are not enqueued faster than the radio/admin path can materialize slot changes. The delay is applied only between writes and not after the final channel write.

Extract the ordered write loop into a focused helper that keeps request ID registration beside each write and accepts a delay function for fast unit coverage. The existing updateChannels path still builds a sorted plan, writes from one coroutine, and performs local migration/cache updates after the ordered write sequence.

Add coverage for pacing behavior, write order, request ID registration, and the no-delay-after-final-write contract.
Make manual channel save semantics explicit: channel writes are treated as an ordered batch, and canonical local channel state is updated only after every write request has been enqueued. If a write enqueue throws, the safeLaunch path reports the failure and leaves saved state unchanged rather than committing a partial delete or reorder result.

Return a ManualChannelUpdateResult from the ordered write helper so callers and tests can inspect the packet IDs produced by a batch. Request IDs are still registered immediately after each write request to preserve response tracking for fast admin responses.

Add focused coverage for mid-batch failure behavior and extend the pacing helper test to assert returned packet IDs while preserving immediate registration and no final delay.
Guard manual channel save batches with a mutex so overlapping updateChannels calls cannot interleave ordered write plans on the radio link.

Keep canonical channel state updates inside the serialized batch after channel writes complete, preserving the existing all-or-no-local-commit behavior for enqueue failures.

Strengthen ViewModel coverage for failed local-node saves by setting myNodeInfo, and add coverage for overlapping manual channel saves executing as serialized batches.
Addresses review nitpick: the channelA/B/C/D + old list setup was
duplicated verbatim in two updateChannels tests. Extract into a
fourChannelFixture() helper and destructure for named access, keeping
the new/firstNew/secondNew lists test-local.
Diff queued manual channel saves against the current canonical channel list inside the serialization mutex so a second save cannot skip writes based on stale state.

Hold the save dialog in Loading while the paced write loop is still registering packet IDs, surface enqueue failures as response errors, and reconcile any already-enqueued local channel writes in NonCancellable cleanup.
The updated detekt baseline in develop no longer suppresses the
catch-all in updateChannels. Add an explicit @Suppress annotation
matching the convention used elsewhere (DatabaseManager.withDb,
BleOtaTransport.connect).

Also restore the blank line between the last test and the
fourChannelFixture helper that was lost during conflict resolution.
Rename ManualChannelUpdateResult.appliedSettings to finalSettings so
the field name matches its meaning — the intended final settings
after a successful ordered update. InterruptedManualChannelUpdate.
appliedSettings keeps its name since it genuinely means "partial
settings already applied before interruption."

Add a focused unit test for applyManualChannelUpdatePlan's interrupted
path: writeChannel fails on a mid-plan channel, the exception
propagates, onInterrupted fires with the partial appliedSettings and
appliedWriteCount reflecting only the writes completed before failure.

Skipped: CodeRabbit flagged key_backup_restore_failed being shown in
the backup guard (L646-649). That code is from develop's security-key
backup feature, not part of this PR's channel-sequencing scope.
@jeremiah-k
jeremiah-k force-pushed the bugfix/manual-channel-write-sequencing branch from 7ee77b9 to 4f6484a Compare July 6, 2026 14:01
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 6, 2026 14:17

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the force-push — all four of my earlier findings are fixed, with tests:

  • Premature Success — batch seeds Loading(total) up front and routes mid-batch ACKs through completeSetRequestOrProgressBatch()/incrementCompleted(); Success deferred to batch end.
  • Silent failuretry/catch now sendErrors → ResponseState.Error (asserted by a test).
  • Partial reconciliationreconcileInterruptedManualChannelUpdate in a withContext(NonCancellable) block, matching applyReplacementChannelSet.
  • Stale baseline — plan now computed inside withLock from live channelList, each batch commits unconditionally.

Requesting changes on a set of related issues the new batch state-machine introduced: it reaches into the shared response-state and can corrupt a concurrent non-batch admin flow (inline). If channel saves can never overlap another admin request on this screen these are benign, but that isn't enforced anywhere — worth scoping the batch bookkeeping to its own request-ids.

Minor carryovers: sortedBy { it.index } is still a no-op (getChannelList is already ascending), ManualChannelUpdateResult is still discarded by the only caller, and applyManualChannelUpdatePlan duplicates applyReplacementChannelSet's paced-write engine (incl. a second 1s constant).

@jeremiah-k
jeremiah-k marked this pull request as draft July 6, 2026 15:15
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 6, 2026 16:23
@jamesarich
jamesarich dismissed their stale review July 6, 2026 16:50

Both blocking findings addressed: batch request-ids are now isolated (only the batch's own ids/timeouts are cleared on abort) and beginManualChannelBatch guards against clobbering a concurrent flow. Posting new non-blocking observations on the queued-saves commit separately.

@jamesarich jamesarich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 6fbbddc — both things I blocked on are fixed, with tests:

  • abortManualChannelBatch(batchRequestIds) now removes/cancels only the batch's own request-ids and timeouts; unrelated in-flight requests survive (aborted manual batch preserves unrelated pending request).
  • beginManualChannelBatch now returns false and bails when a non-batch request is pending/registering, instead of clobbering its route/Loading state.

Dismissing my changes-requested. Three smaller things the allow queued manual channel saves commit introduced — none blocking, but worth a look:

  1. Silent save drop (worth checking). hasPendingRequestRegistration() is true whenever requestIds.isEmpty() && manualChannelBatchRequestIds.isEmpty() && responseState is Loading, so a Save tapped while the pane is in a bare Loading state (e.g. right after ensureLoadingForRemote(), before any request registers) is dropped with only a log — no user feedback. If that state is reachable with Save enabled, the user retries blind.
  2. Progress-bar mix (cosmetic). Queue a second save while the first batch's ACKs are still outstanding: the first batch's un-acked ids count as 'related', so the second beginManualChannelBatch proceeds and its Loading(total) absorbs the leftover ids — completed/total then mixes both batches. Writes stay ordered under the mutex, so no data loss, just a wrong count.
  3. No commit rollback (low likelihood). If replaceAllSettings throws after migrateChannelsByPSK succeeds in the commit, packetRepository is migrated while radioConfigRepository/state aren't, with no reconciliation. Both are local DB writes so unlikely, but there's no rollback.

@jamesarich
jamesarich added this pull request to the merge queue Jul 6, 2026
Merged via the queue into meshtastic:main with commit 9b5e3bb Jul 6, 2026
17 checks passed
@jeremiah-k
jeremiah-k deleted the bugfix/manual-channel-write-sequencing branch July 6, 2026 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants