Skip to content
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# cargo-nextest configuration
# See https://nexte.st/docs/configuration/ for full documentation

# Tests in the `virtual-mtp` group share a single backing-dir fixture root at
# `/tmp/cmdr-mtp-e2e-fixtures` (see `mtp::virtual_device::setup_virtual_mtp_device`),
# so running them concurrently races on a shared filesystem resource and one
# wins with `DirectoryNotEmpty`. Cap concurrency at 1 to serialize them across
# nextest's per-process forks.
[test-groups]
virtual-mtp = { max-threads = 1 }

[[profile.default.overrides]]
filter = 'test(mtp_scan) + test(listing_is_watched_flips_with_connection)'
test-group = 'virtual-mtp'

[profile.default]
# Show test output for failing tests only
failure-output = "immediate"
Expand Down
10 changes: 9 additions & 1 deletion apps/desktop/src-tauri/src/commands/file_system/write_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,15 @@ pub async fn start_scan_preview(
};

let progress_interval = progress_interval_ms.unwrap_or(500);
ops_start_scan_preview(app, sources, source_volume, sort_column, sort_order, progress_interval)
ops_start_scan_preview(
app,
sources,
source_volume,
volume_id,
sort_column,
sort_order,
progress_interval,
)
}

#[tauri::command]
Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/src-tauri/src/commands/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ pub fn show_breadcrumb_context_menu<R: Runtime>(window: Window<R>, shortcut: Str
#[tauri::command]
#[specta::specta]
pub fn show_main_window<R: Runtime>(window: Window<R>) -> Result<(), String> {
// E2E: on macOS, use `orderFront:` instead of `makeKeyAndOrderFront:` so the
// window appears without stealing focus from whatever the user is currently
// working in. `window.show()` calls the latter on macOS, which always grabs
// OS focus. Linux/Windows test runs happen in headless containers, so the
// standard show is fine there.
#[cfg(target_os = "macos")]
if crate::test_mode::is_e2e_mode() {
use objc2::msg_send;
use objc2::runtime::AnyObject;
let ns_window = window.ns_window().map_err(|e| e.to_string())? as *mut AnyObject;
if ns_window.is_null() {
return Err("NSWindow pointer is null".into());
}
unsafe {
let _: () = msg_send![ns_window, orderFront: std::ptr::null_mut::<AnyObject>()];
}
return Ok(());
}
window.show().map_err(|e| e.to_string())
}

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/file_system/listing/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ since it only does cache lookups and `stat` calls via `get_single_entry`.
Used by the watcher's incremental path and synthetic mkdir to patch listings without full re-reads:
- `find_listings_for_path(path)`: returns all listing IDs whose directory matches the given path (multiple panes/tabs may show the same directory)
- `find_listings_for_path_on_volume(volume_id, path)`: same, but also filters by volume ID. Prevents false matches when two volumes serve overlapping paths.
- `try_get_watched_listing(volume_id, path)`: the **fresh-listing oracle** for write-op pre-flight scans. Returns `Some(entries)` when a cached listing exists for `(volume_id, path)` and the volume reports `listing_is_watched(path) == true` (delegated to the backend via the `Volume` trait). Otherwise `None`. When multiple listings exist for the same `(volume_id, path)` pair (two panes), picks the most-recently-updated one deterministically: highest `sequence` (an `AtomicU64` on `CachedListing`), ties broken by latest `created_at`. Entries are cloned out under the cache `RwLock`, then the lock is released before the volume call (cheap clone for a flat `Vec<FileEntry>` — < 5 ms for 15k entries; matters because the volume call would otherwise hold the listing-cache lock across an await and block pane navigation). See the freshness-contract section in `volume/CLAUDE.md` for the per-backend debounce windows callers must tolerate.
- `insert_entry_sorted(listing_id, entry)`: inserts an entry in sorted position, returns the insertion index
- `remove_entry_by_path(listing_id, path)`: removes an entry by its file path, returns the removed index and entry
- `update_entry_sorted(listing_id, entry)`: updates an existing entry (remove + re-insert if sort position changed), returns `ModifyResult`
Expand Down
66 changes: 66 additions & 0 deletions apps/desktop/src-tauri/src/file_system/listing/caching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,69 @@ pub(crate) fn increment_sequence(listing_id: &str) -> Option<u64> {
let seq = listing.sequence.fetch_add(1, Ordering::Relaxed) + 1;
Some(seq)
}

/// Returns cached entries for `(volume_id, path)` when the volume reports that this
/// listing is being kept in sync by an active watcher. Otherwise `None`.
///
/// **Freshness contract (read carefully)**: a `Some(_)` result means the volume has
/// an active change-notification channel and the cache reflects the volume's most
/// recently observed state. It does NOT mean the cache is byte-perfect with the
/// device right now: every backend has a debounce or settling window between a real
/// change and the cache reflecting it.
///
/// - Local FS: FSEvents coalesce window (~10 ms).
/// - SMB: 200 ms watcher debounce; > 50 events per directory triggers a
/// `FullRefresh` which arrives via a real re-read.
/// - MTP: 500 ms event debouncer plus per-device polling. Many MTP devices
/// (cameras especially) never emit per-object events, so "watched" there means
/// only "the device is reachable and would forward changes if it sent any."
///
/// Callers must treat the result as "fresh as our most recent observation," which
/// is the same guarantee a `list_directory` call gives: it sees the device's state
/// at the moment the call returned, not at the moment the caller reads its result.
/// The contract intentionally accepts this window; a tighter one would force us to
/// re-validate every walk, defeating the whole point of the oracle.
///
/// When multiple cached listings exist for the same `(volume_id, path)` pair (two
/// panes browsing the same directory), the picker is deterministic: highest
/// `sequence`, ties broken by the latest `created_at`. Both listings receive watcher
/// events, so they're equally fresh; the tiebreaker is just to keep the result
/// stable across calls.
#[allow(dead_code, reason = "M1 plumbing: callers (scan walker, scan-preview) wire up in M2")]
pub fn try_get_watched_listing(volume_id: &str, path: &Path) -> Option<Vec<FileEntry>> {
// Step 1: find all listings on this (volume_id, path) and pick the most-recently-updated
// one (highest sequence, ties broken by latest created_at). Read the entries out
// under the cache lock and drop the lock before crossing any async / volume boundary.
let entries: Vec<FileEntry> = {
let cache = LISTING_CACHE.read().ok()?;
let mut best: Option<(&String, &CachedListing, u64, Instant)> = None;
for (id, listing) in cache.iter() {
if listing.volume_id != volume_id || listing.path != path {
continue;
}
let seq = listing.sequence.load(Ordering::Relaxed);
let created = listing.created_at;
best = match best {
None => Some((id, listing, seq, created)),
Some((_, _, best_seq, best_created))
if seq > best_seq || (seq == best_seq && created > best_created) =>
{
Some((id, listing, seq, created))
}
Some(other) => Some(other),
};
}
let (_, listing, ..) = best?;
listing.entries.clone()
};

// Step 2: ask the volume whether this listing is being kept fresh by a watcher.
// VolumeManager::get returns an Arc<dyn Volume> which we hold for the duration of
// the sync `listing_is_watched` call. No await between this and the entries return.
let volume = crate::file_system::get_volume_manager().get(volume_id)?;
if volume.listing_is_watched(path) {
Some(entries)
} else {
None
}
}
Loading
Loading