Skip to content

Commit 9b9a4ca

Browse files
committed
Tooling: Virtual MTP device in dev via CMDR_VIRTUAL_MTP
Lets devs exercise MTP flows (drag&drop, transfers, conflict dialogs) in a normal `pnpm dev` session without real hardware: - `CMDR_VIRTUAL_MTP=1 pnpm dev` registers a virtual "Virtual Pixel 9" device in the volume picker, pre-populated with folders/files so drag&drop is testable immediately. `=<dir>` backs it with a custom directory tree. - Compile-time still feature-gated: the wrapper (`tauri-wrapper.js`) appends `--features virtual-mtp` to the dev build only when the env var is set, so release `pnpm build` binaries stay feature-free. First use after a plain `pnpm dev` triggers a full-ish rebuild (feature-set change). - Runtime gate unified in `activate_from_env_if_requested()`: registers when either an E2E run (`CMDR_E2E_MODE=1`) or `CMDR_VIRTUAL_MTP` is set, never when `CMDR_E2E_SKIP_VIRTUAL_MTP_SETUP` is set. A `virtual-mtp`-compiled binary launched with none of those vars stays inert, so the dev opt-in is purely additive to the E2E path (harness behavior unchanged). - Default fixtures now mirror `test/e2e-shared/mtp-fixtures.ts` (DCIM/Burst included). - Pure `decide_startup_root` gating logic is unit-tested. - Docs: new `docs/tooling/virtual-mtp.md`, plus notes in `mtp/CLAUDE.md`, the scripts `CLAUDE.md`, and the testing docs.
1 parent f977ed9 commit 9b9a4ca

8 files changed

Lines changed: 298 additions & 36 deletions

File tree

apps/desktop/scripts/CLAUDE.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ launch boundary, plus the llama-server fetch + the type-drift check.
55

66
## Files
77

8-
| File | Purpose |
9-
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10-
| `tauri-wrapper.js` | The script `pnpm dev` and `pnpm build` actually call. Resolves `CMDR_INSTANCE_ID`, reserves ephemeral ports (Vite + tauri-MCP bridge), writes the generated `tauri.instance.json` to `$TMPDIR`, exports the right env, then spawns Tauri |
11-
| `instance-id.js` | Pure helpers backing the wrapper: slug sanitization, instance resolution, per-OS data-dir computation, bundle-identifier + productName + config-payload composition, ephemeral port reservation, port-file write protocol |
12-
| `instance-id.test.js` | Vitest suite for `instance-id.js`. ~45 cases covering every helper |
13-
| `download-llama-server.go` | Build-time downloader for the llama-server binary. Invoked from `src-tauri/build.rs`. In linked git worktrees, symlinks from the main clone when the `.version` matches |
14-
| `check-type-drift.ts` | Fast-lane check that scans for hand-written types that drift from the auto-generated `bindings.ts`. Runs as part of `./scripts/check.sh --fast` |
15-
| `e2e-linux.sh` | Linux Docker E2E launcher. Builds the Tauri binary with `playwright-e2e,virtual-mtp` features, runs the suite. Single-shard; uses the legacy shared fixture path (no per-instance isolation) |
8+
| File | Purpose |
9+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
10+
| `tauri-wrapper.js` | The script `pnpm dev` and `pnpm build` actually call. Resolves `CMDR_INSTANCE_ID`, reserves ephemeral ports (Vite + tauri-MCP bridge), writes the generated `tauri.instance.json` to `$TMPDIR`, exports the right env, then spawns Tauri. Dev-only: when `CMDR_VIRTUAL_MTP` is set, appends `--features virtual-mtp` to the cargo build (see [`docs/tooling/virtual-mtp.md`](../../../docs/tooling/virtual-mtp.md)) |
11+
| `instance-id.js` | Pure helpers backing the wrapper: slug sanitization, instance resolution, per-OS data-dir computation, bundle-identifier + productName + config-payload composition, ephemeral port reservation, port-file write protocol |
12+
| `instance-id.test.js` | Vitest suite for `instance-id.js`. ~45 cases covering every helper |
13+
| `download-llama-server.go` | Build-time downloader for the llama-server binary. Invoked from `src-tauri/build.rs`. In linked git worktrees, symlinks from the main clone when the `.version` matches |
14+
| `check-type-drift.ts` | Fast-lane check that scans for hand-written types that drift from the auto-generated `bindings.ts`. Runs as part of `./scripts/check.sh --fast` |
15+
| `e2e-linux.sh` | Linux Docker E2E launcher. Builds the Tauri binary with `playwright-e2e,virtual-mtp` features, runs the suite. Single-shard; uses the legacy shared fixture path (no per-instance isolation) |
1616

1717
## The wrapper architecture in one paragraph
1818

apps/desktop/scripts/tauri-wrapper.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,31 @@ const isBuild = args.includes('build')
4141
// anything after `--` (Tauri / cargo args like `--features virtual-mtp`) intact.
4242
const { slug: rawWorktreeSlug, rest: forwardedArgs } = extractWorktreeFlag(args)
4343

44+
// Dev-only virtual MTP opt-in. `CMDR_VIRTUAL_MTP=1 pnpm dev` (or `=<dir>` for a custom
45+
// backing dir) registers a fake Android device so MTP flows (drag&drop, transfers,
46+
// conflict dialogs) are testable without real hardware. The Rust side is feature-gated
47+
// (`#[cfg(feature = "virtual-mtp")]`), so we must compile the feature in AND let the
48+
// matching env var through. The var is already in `env` (inherited); here we just append
49+
// the cargo feature to the dev build. Adding a feature changes the feature set, so the
50+
// first `CMDR_VIRTUAL_MTP=1 pnpm dev` after a plain run triggers a full-ish rebuild.
51+
// Release `pnpm build` never reads this, so prod binaries stay feature-free.
52+
// See docs/tooling/virtual-mtp.md and src-tauri/src/mtp/virtual_device.rs.
53+
const wantsVirtualMtp = isDev && !!process.env.CMDR_VIRTUAL_MTP && process.env.CMDR_VIRTUAL_MTP.trim() !== ''
54+
if (wantsVirtualMtp && !forwardedArgs.includes('virtual-mtp')) {
55+
// Cargo features live after the `--` separator that splits Tauri-CLI args from
56+
// `cargo run` args. Reuse an existing `--features <list>` (append, comma-joined) so we
57+
// don't clobber a user-passed feature set; otherwise add a fresh `-- --features` block.
58+
const dashDash = forwardedArgs.indexOf('--')
59+
const featuresIdx = dashDash >= 0 ? forwardedArgs.indexOf('--features', dashDash) : -1
60+
if (featuresIdx >= 0 && featuresIdx + 1 < forwardedArgs.length) {
61+
forwardedArgs[featuresIdx + 1] = `${forwardedArgs[featuresIdx + 1]},virtual-mtp`
62+
} else if (dashDash >= 0) {
63+
forwardedArgs.push('--features', 'virtual-mtp')
64+
} else {
65+
forwardedArgs.push('--', '--features', 'virtual-mtp')
66+
}
67+
}
68+
4469
const env = { ...process.env }
4570
/** @type {string | null} */
4671
let instanceTmpDir = null

apps/desktop/src-tauri/src/lib.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -427,14 +427,14 @@ pub fn run() {
427427
#[cfg(target_os = "linux")]
428428
volumes_linux::watcher::start_volume_watcher(app.handle());
429429

430-
// Register virtual MTP device for E2E testing (before watcher so it's in the initial snapshot).
431-
// Under parallel E2E sharding the MTP backing dir is shared across Tauri instances, so
432-
// non-MTP shards opt out via CMDR_E2E_SKIP_VIRTUAL_MTP_SETUP to avoid the startup
433-
// wipe-and-recreate race on the shared dir.
430+
// Register the virtual MTP device (before the watcher so it's in the initial
431+
// snapshot) when requested. Two activation paths, unified in
432+
// `activate_from_env_if_requested`: an E2E run (CMDR_E2E_MODE=1) or a dev opt-in
433+
// (CMDR_VIRTUAL_MTP=1, or =<dir> for a custom backing dir). Non-MTP E2E shards opt
434+
// out via CMDR_E2E_SKIP_VIRTUAL_MTP_SETUP to avoid racing the shared backing dir.
435+
// See `mtp/virtual_device.rs::decide_startup_root` and `docs/tooling/virtual-mtp.md`.
434436
#[cfg(feature = "virtual-mtp")]
435-
if std::env::var("CMDR_E2E_SKIP_VIRTUAL_MTP_SETUP").is_err() {
436-
mtp::virtual_device::setup_virtual_mtp_device();
437-
}
437+
mtp::virtual_device::activate_from_env_if_requested();
438438

439439
// Ensure ptpcamerad is re-enabled in case a previous session crashed
440440
// while it was suppressed. No-op if it was already enabled.

apps/desktop/src-tauri/src/mtp/CLAUDE.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ storage picker, and reactive volume state.
1717
| `watcher.rs` | `start_mtp_watcher()`: nusb hotplug watcher; 500 ms delay on connect before re-checking; auto-connects detected devices via `MtpConnectionManager::connect()` and auto-disconnects removed ones |
1818
| `macos_workaround.rs` | macOS-only (`#[cfg(target_os = "macos")]`). Auto-suppresses `ptpcamerad` via `launchctl disable` + `pkill`; restores on disconnect/exit; `ensure_ptpcamerad_enabled()` on startup for crash recovery. Falls back to manual `PTPCAMERAD_WORKAROUND_COMMAND` dialog if suppression fails |
1919
| `connection/` | Per-device session layer: `MtpConnectionManager` singleton, connect / disconnect (with `MtpDisconnectReason` so logs and UI distinguish explicit toggle-off from hotplug-loss), event-loop task, list / read / write / mutate / bulk ops. See [`connection/CLAUDE.md`](connection/CLAUDE.md) for the file-by-file breakdown, lock semantics, caches, and gotchas. |
20-
| `virtual_device.rs` | Virtual MTP device for E2E testing; creates backing dirs + registers device via `mtp-rs`. Gated behind `virtual-mtp` feature. Run with: `pnpm dev -- --features virtual-mtp` (pass `--worktree <slug>` first for an isolated data dir). |
20+
| `virtual_device.rs` | Virtual MTP device for E2E testing and dev sessions; creates backing dirs + registers device via `mtp-rs`. Gated behind `virtual-mtp` feature. Dev opt-in: `CMDR_VIRTUAL_MTP=1 pnpm dev` (the wrapper adds the feature; `=<dir>` backs it with a custom dir). See [`docs/tooling/virtual-mtp.md`](../../../../../docs/tooling/virtual-mtp.md). |
21+
22+
### Virtual MTP device (dev + E2E activation)
23+
24+
The `virtual-mtp` feature compiles in `virtual_device.rs`; whether the device actually registers at startup is decided
25+
at runtime by `activate_from_env_if_requested()` (called from `lib.rs`). It registers when **either** `CMDR_E2E_MODE=1`
26+
(an E2E run) **or** `CMDR_VIRTUAL_MTP` is set (the dev opt-in), and never when `CMDR_E2E_SKIP_VIRTUAL_MTP_SETUP` is set
27+
(the override non-MTP E2E shards use to avoid racing the shared backing dir). So a `virtual-mtp`-compiled binary launched
28+
with none of those env vars stays inert and matches a plain build — the dev opt-in is purely additive to the E2E path.
29+
The fixture tree mirrors `test/e2e-shared/mtp-fixtures.ts`. The gating logic (`decide_startup_root`) is pure and
30+
unit-tested in `virtual_device.rs::tests`.
2131

2232
## Architecture / data flow
2333

0 commit comments

Comments
 (0)