Fix phantom cloud sync uploads after wipe + purge
Details
After wiping local data and purging the cloud workspace, stale cursor
state in the local cloud_sync_cursors table caused the sync engine to
re-upload entities that plugins recreate on startup. The server-side
purge cleared S3 batches and API cursors, but the local DuckDB cursor
table was never cleared — so when sync restarted and PushAllEntities()
ran, the engine treated the re-created entities as new changes.
Adds privstack_cloudsync_clear_cursors FFI export that calls the
existing EntityStore::clear_cloud_cursors() method, wires it through
NativeLibrary → ICloudSyncService → CloudSyncService, and calls it
in SeedDataService.PurgeCloudWorkspaceIfConnectedAsync() immediately
after the server-side purge succeeds.
Purge cloud workspace data on local wipe/reseed/reset
Details
When the user wipes data (Wipe, Reseed, or Reset Data on unlock screen),
the local DuckDB cursors are destroyed but the server-side sync_batches
and sync_cursors remain. On next sync startup the device's cursor resets
to 0, causing getPendingChanges to report the device's own old batches
as pending — triggering an infinite download loop of stale data every
poll cycle.
Fix: call the server's /api/cloud/workspace/purge endpoint before wiping
local data. This clears batches, cursors, snapshots, blobs, and S3
objects server-side so the sync engine starts clean.
- SeedDataService: add PurgeCloudWorkspaceIfConnectedAsync() as step 0
in WipeAllPluginDataAsync (covers both Wipe and Reseed paths)
- SetupWizardViewModel: add PurgeCloudWorkspaceAsync() before runtime
shutdown in ConfirmResetData (covers unlock screen Reset Data)
- Both methods are best-effort — cloud purge failure doesn't block
local wipe
Route Whisper transcriptions to Duncan when no text input is focused
Details
When Cmd+M is pressed without a text input focused, the speech recording
now proceeds normally and routes the transcription to Duncan's AI tray
instead of silently discarding it. This enables voice commands:
- MainWindow.Speech.cs: removed early return when no text control is
focused — always start recording regardless of focus state
- MainWindow.axaml.cs: OnTranscriptionReady routes to
AiTrayVM.SendVoiceMessageAsync() when _speechTargetControl is null,
and auto-opens the AI tray panel
- AiSuggestionTrayViewModel.Chat.cs: added SendVoiceMessageAsync() that
sets ChatInputText and triggers the standard send pipeline (RAG +
intent classification)
Flow: Cmd+M → record → Whisper transcribe → no text field? → Duncan
processes via RAG + intent engine → shows response/action in AI tray.
Move inline tests to integration tests for crdt, crypto, ffi
Details
Migrate remaining #[cfg(test)] inline test modules to tests/ directories.
Crates migrated:
- privstack-crdt: 18 pn_counter tests → tests/pn_counter_tests.rs
- privstack-crypto: 5 recovery tests → tests/recovery_tests.rs
- privstack-ffi: 337 tests → tests/ffi_tests.rs (biggest migration)
FFI crate changes:
- Added "rlib" to Cargo.toml crate-type for integration test linking
- Made DTOs pub: SyncEventDto, DiscoveredPeerInfo, SyncStatus,
CloudFileInfo, LicenseInfo, ActivationInfo, FfiDeviceInfo,
SdkRequest, SdkResponse (with pub fields)
- Made functions pub: init_core, init_with_plugin_host_builder,
license_error_to_ffi, nullable_cstr_to_str, to_c_string
- Made EntityRegistry methods pub: new, register_schema, get_schema,
get_handler, has_schema
- Made HANDLE static pub, plugin_ffi module pub
Remaining #[cfg(test)] in cloud/config.rs and crypto/key.rs are
test-only constructors (CloudConfig::test, KdfParams::test), not
inline test modules.
Total: 360 integration tests, 0 inline test modules remaining.
Get notified about new releases