fix: store pendingReconcileTask handle; skip real Carbon hotkey binding on self-test runs

AppModel gains pendingReconcileTask (the most recent watcher-reconcile Task
spawned by chooseTransport/chooseOneDriveFolder), which send() now awaits —
see the SendController.swift commit in this series. chooseTransport/
chooseOneDriveFolder store their Task's handle into it instead of firing an
untracked `Task { }`.

bootstrap() now also skips binding the real, process-wide Carbon global
capture hotkey on the same env-var-flagged self-test/headless runs that
already skip the update-check schedule (PickerSelfTest's phases,
PanelSnapshot, and the new ShotdeckTests launch-wiring regression test).
Real Carbon hotkey registration is not safe to exercise in an automated test
process — it can collide with ShotdeckCoreTests' own
HotkeyCenterCarbonTests running in the same test binary — and bootstrap()'s
hotkey step had never actually been exercised by any self-test before (none
of them call bootstrap() directly) until the new real-wiring test in this
series does. A real user launch never sets these env vars, so production
behavior is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
This commit is contained in:
2026-09-05 09:55:16 +04:00
co-authored by Claude Fable 5.1
parent d7984add2d
commit 04d1e73532
2 changed files with 31 additions and 10 deletions
+5 -2
View File
@@ -255,6 +255,9 @@ extension AppModel: SettingsWindowPresenting {
/// the live value before every mutating step, so rapid toggling (this function or
/// chooseOneDriveFolder, in any order) always lets the LAST choice win instead of an
/// earlier, superseded call applying its stale folder/flag after a later one already won.
/// The Task's handle is stored in `pendingReconcileTask` so send() can await its
/// completion before snapshotting transport/folder closing the OTHER race, where a
/// toggle is immediately followed by Send before this reconcile has settled.
func chooseTransport(_ value: SendTransport) {
guard !isSending else {
setStatus("Finish the current send first.")
@@ -274,7 +277,7 @@ extension AppModel: SettingsWindowPresenting {
reconcileGeneration += 1
let generation = reconcileGeneration
Task {
pendingReconcileTask = Task {
guard generation == self.reconcileGeneration else { return }
await watcher.setRecordUncommented(value == .airDrop)
guard generation == self.reconcileGeneration else { return }
@@ -306,7 +309,7 @@ extension AppModel: SettingsWindowPresenting {
reconcileGeneration += 1
let generation = reconcileGeneration
Task {
pendingReconcileTask = Task {
guard generation == self.reconcileGeneration else { return }
do {
try await watcher.updateWatchFolder(url)