victauri-plugin 0.9.0

Tauri plugin for Victauri — embedded MCP server with full-stack introspection
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
use std::sync::Arc;
use tauri::{Manager, Runtime, State};
use victauri_core::{IpcCall, WindowState};

use crate::VictauriState;

/// Id prefix for evals started by page JS through these Tauri commands (vs. by an agent). The
/// full id is `page:<uuid>:<window label>` (the uuid is fixed-length, so any label parses back).
pub const PAGE_EVAL_PREFIX: &str = "page:";
/// Pending-eval slots page-originated evals may hold at once, across all windows. They share
/// the pending map with the agent's (MCP) evals; without their own small budget, page script
/// could park never-resolving evals in every slot and starve the agent with "too many
/// concurrent evals".
pub const MAX_PAGE_PENDING_EVALS: usize = 25;
/// Page-originated eval slots ONE window may hold, so one window cannot starve the others.
pub const MAX_PAGE_PENDING_EVALS_PER_WINDOW: usize = 10;

/// Page-callable window queries (`victauri_list_windows` / `victauri_get_window_state`) allowed
/// in flight at once, across all windows. Beyond it a query is refused at once, like a page eval
/// over its budget.
///
/// While both are synchronous commands this budget never binds: Tauri runs a sync command on the
/// main thread, the bridge then runs the query inline (`bridge::on_main` — no round trip, no
/// dispatch lock), and the main thread runs one command at a time, so at most one slot is ever
/// held. It is kept as a guard for the path it was written for (0.9.0 round 3, when these were
/// `async` commands whose queries were main-thread round trips queued behind the agent's): should
/// they ever run off the main thread again, a page cannot queue hundreds of them.
pub const MAX_PAGE_WINDOW_QUERIES: usize = 4;
pub static PAGE_WINDOW_QUERY_SLOTS: tokio::sync::Semaphore =
    tokio::sync::Semaphore::const_new(MAX_PAGE_WINDOW_QUERIES);

/// Run page-originated window query `f` within [`MAX_PAGE_WINDOW_QUERIES`].
pub fn page_window_query<T>(f: impl FnOnce() -> T) -> Result<T, String> {
    let _slot = PAGE_WINDOW_QUERY_SLOTS.try_acquire().map_err(|_| {
        format!(
            "too many concurrent page-originated window queries (limit \
             {MAX_PAGE_WINDOW_QUERIES}); retry when one has finished"
        )
    })?;
    Ok(f())
}

/// IPC calls `victauri_get_ipc_log` returns when the page gives no `limit`.
pub const PAGE_IPC_LOG_DEFAULT: usize = 100;
/// Most IPC calls `victauri_get_ipc_log` returns, whatever `limit` the page asks for.
pub const PAGE_IPC_LOG_MAX: usize = 1_000;
/// Most `stale_calls` / `error_calls` entries (each the newest) `victauri_check_ipc_integrity`
/// returns; the counts always cover the whole log.
pub const PAGE_INTEGRITY_MAX_ENTRIES: usize = 100;

/// The window a page-originated eval id belongs to.
fn page_eval_window(id: &str) -> Option<&str> {
    let rest = id.strip_prefix(PAGE_EVAL_PREFIX)?;
    rest.get(36..)?.strip_prefix(':')
}

/// Reserve a pending-eval slot for a page-originated eval from window `label`, within the
/// per-window budget, the page budget and the global ceiling. The slot is released when the
/// returned guard drops.
pub async fn reserve_page_eval(
    state: &VictauriState,
    label: &str,
    tx: tokio::sync::oneshot::Sender<String>,
) -> Result<crate::PendingSlot, String> {
    let id = format!("{PAGE_EVAL_PREFIX}{}:{label}", uuid::Uuid::new_v4());
    let mut pending = state.pending_evals.lock().await;
    let mut page_pending = 0;
    let mut window_pending = 0;
    for key in pending.keys().filter(|k| k.starts_with(PAGE_EVAL_PREFIX)) {
        page_pending += 1;
        if page_eval_window(key) == Some(label) {
            window_pending += 1;
        }
    }
    if window_pending >= MAX_PAGE_PENDING_EVALS_PER_WINDOW {
        return Err(format!(
            "too many concurrent page-originated evals from window '{label}' \
             (limit {MAX_PAGE_PENDING_EVALS_PER_WINDOW})"
        ));
    }
    if page_pending >= MAX_PAGE_PENDING_EVALS {
        return Err(format!(
            "too many concurrent page-originated evals (limit {MAX_PAGE_PENDING_EVALS})"
        ));
    }
    if pending.len() >= crate::mcp::MAX_PENDING_EVALS {
        return Err(format!(
            "too many concurrent evals (limit {})",
            crate::mcp::MAX_PENDING_EVALS
        ));
    }
    let slot = crate::PendingSlot::insert(&state.pending_evals, &mut pending, id, tx);
    slot.bind_window(Some(label));
    Ok(slot)
}

#[tauri::command]
pub async fn victauri_eval_js<R: Runtime>(
    webview: tauri::WebviewWindow<R>,
    state: State<'_, Arc<VictauriState>>,
    code: String,
) -> Result<String, String> {
    let (tx, rx) = tokio::sync::oneshot::channel();
    // Released on every exit, including this future being dropped mid-wait.
    let slot = reserve_page_eval(&state, webview.label(), tx).await?;
    let id = serde_json::to_string(slot.id()).map_err(|e| e.to_string())?;

    let inject = crate::js_bridge::page_eval_script(&id, &code);

    // Through the bridge, i.e. on the main thread and serialized with every other
    // round trip — not a direct `webview.eval` from this tokio worker thread.
    if let Err(e) = crate::bridge::WebviewBridge::eval_webview(
        webview.app_handle(),
        Some(webview.label()),
        &inject,
    ) {
        return Err(format!("eval failed: {e}"));
    }

    match tokio::time::timeout(state.eval_timeout, rx).await {
        Ok(Ok(result)) => Ok(result),
        Ok(Err(_)) => Err("eval callback channel closed".to_string()),
        Err(_) => Err(format!(
            "eval timed out after {}s",
            state.eval_timeout.as_secs()
        )),
    }
}

/// Deliberately a SYNCHRONOUS command. Every eval, drain tick, snapshot and bridge-ready signal
/// comes back through here. Tauri runs an `async` command on a tokio worker and drops its
/// resolver — which holds a `Webview`, i.e. a Tauri handle — there; on Linux that is an
/// off-main-thread drop of a non-atomic `Rc` (see `bridge::on_main`), and at this call rate it
/// corrupted the host's heap. A sync command runs on the main thread, where its `webview`
/// argument and its resolver are created and dropped. Result delivery never blocks that thread:
/// an uncontended map is updated in place, otherwise owned data (no Tauri handle) is handed to
/// a tokio task.
#[tauri::command]
pub fn victauri_eval_callback<R: Runtime>(
    webview: tauri::Webview<R>,
    state: State<'_, Arc<VictauriState>>,
    id: String,
    result: String,
) -> Result<(), String> {
    if id == "__victauri_bridge_ready__" {
        state
            .bridge_ready
            .store(true, std::sync::atomic::Ordering::Release);
        state.bridge_notify.notify_waiters();
        // A (re)initialized bridge means this window loaded a new page: any eval still pending
        // in its previous page can never answer. The signal carries the new page's nonce; the
        // label comes from Tauri (the calling webview), so page script cannot touch another
        // window's record. (A forged signal from the same page is weeded out by the eval,
        // which asks the page for its current nonce before treating a signal as a reload.)
        let nonce = (!result.is_empty()).then_some(result.as_str());
        state.page_loads.record_load(webview.label(), nonce);
        return Ok(());
    }
    accept_eval_result(
        Arc::clone(state.inner()),
        webview.label(),
        || crate::bridge::default_window_label(webview.app_handle()),
        id,
        result,
    );
    Ok(())
}

/// Deliver an eval result from webview `caller` — only if the eval was sent to that window
/// (see [`crate::eval_result_accepted`]); a result from any other window is dropped. Returns
/// whether it was accepted.
fn accept_eval_result(
    state: Arc<VictauriState>,
    caller: &str,
    default_label: impl FnOnce() -> Option<String>,
    id: String,
    result: String,
) -> bool {
    if !crate::eval_result_accepted(&id, caller, default_label) {
        tracing::debug!(
            window = caller,
            "ignored an eval result from a window the eval was not sent to (or for no pending              eval)"
        );
        return false;
    }
    deliver_eval_result(state, id, result);
    true
}

/// Hand an eval result to the caller waiting on `id` without blocking the calling (main)
/// thread: in place when the pending map is free, else from a tokio task holding only owned
/// data.
fn deliver_eval_result(state: Arc<VictauriState>, id: String, result: String) {
    if let Ok(mut pending) = state.pending_evals.try_lock() {
        if let Some(tx) = pending.remove(&id) {
            let _ = tx.send(result);
        }
        return;
    }
    tauri::async_runtime::spawn(async move {
        if let Some(tx) = state.pending_evals.lock().await.remove(&id) {
            let _ = tx.send(result);
        }
    });
}

#[tauri::command]
pub async fn victauri_dom_snapshot<R: Runtime>(
    webview: tauri::WebviewWindow<R>,
    state: State<'_, Arc<VictauriState>>,
) -> Result<String, String> {
    let (tx, rx) = tokio::sync::oneshot::channel();
    // Released on every exit, including this future being dropped mid-wait.
    let slot = reserve_page_eval(&state, webview.label(), tx).await?;
    let id = serde_json::to_string(slot.id()).map_err(|e| e.to_string())?;

    // Same delivery as a page eval: serialized by the bridge with its init-time JSON.
    let inject = crate::js_bridge::page_eval_script(&id, "return window.__VICTAURI__?.snapshot()");

    // Through the bridge, i.e. on the main thread and serialized with every other
    // round trip — not a direct `webview.eval` from this tokio worker thread.
    if let Err(e) = crate::bridge::WebviewBridge::eval_webview(
        webview.app_handle(),
        Some(webview.label()),
        &inject,
    ) {
        return Err(format!("snapshot eval failed: {e}"));
    }

    match tokio::time::timeout(state.eval_timeout, rx).await {
        Ok(Ok(result)) => Ok(result),
        Ok(Err(_)) => Err("snapshot callback channel closed".to_string()),
        Err(_) => Err(format!(
            "snapshot timed out after {}s",
            state.eval_timeout.as_secs()
        )),
    }
}

// The page-callable commands below are SYNCHRONOUS on purpose (none of them waits for anything):
// Tauri runs an `async` command on a tokio worker and creates and drops its arguments and its
// resolver there, and every Tauri handle among them carries a non-atomic `Rc` on Linux (see
// `bridge::on_main`). As sync commands they run on the main thread, where those handles belong.
// `victauri_eval_js` / `victauri_dom_snapshot` must stay `async` — they wait for the page's
// callback — so a page that calls them still takes Tauri's async-command path.
#[tauri::command]
pub fn victauri_get_window_state<R: Runtime>(
    app: tauri::AppHandle<R>,
    label: Option<String>,
) -> Result<Vec<WindowState>, String> {
    page_window_query(|| crate::bridge::WebviewBridge::get_window_states(&app, label.as_deref()))
}

#[tauri::command]
pub fn victauri_list_windows<R: Runtime>(app: tauri::AppHandle<R>) -> Result<Vec<String>, String> {
    page_window_query(|| crate::bridge::WebviewBridge::list_window_labels(&app))
}

#[tauri::command]
pub fn victauri_get_ipc_log(
    state: State<'_, Arc<VictauriState>>,
    limit: Option<usize>,
) -> Result<Vec<IpcCall>, String> {
    Ok(page_ipc_log(&state, limit))
}

/// The newest `limit` IPC calls (default [`PAGE_IPC_LOG_DEFAULT`], at most
/// [`PAGE_IPC_LOG_MAX`]), oldest first.
///
/// These page-callable commands are synchronous, i.e. they run on the UI thread (see the note
/// above `victauri_get_window_state`), and page script can call them in a loop. So each does
/// work bounded by its reply, not by the log: only the returned calls are cloned (a call carries
/// its whole request/response body; cloning the full 10,000-event log per call janked the UI).
fn page_ipc_log(state: &VictauriState, limit: Option<usize>) -> Vec<IpcCall> {
    let limit = limit.unwrap_or(PAGE_IPC_LOG_DEFAULT).min(PAGE_IPC_LOG_MAX);
    state.event_log.recent_ipc_calls(limit)
}

#[tauri::command]
pub fn victauri_get_registry(
    state: State<'_, Arc<VictauriState>>,
    query: Option<String>,
) -> Result<serde_json::Value, String> {
    let commands = match query {
        Some(q) => state.registry.search(&q),
        None => state.registry.list(),
    };
    serde_json::to_value(commands).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn victauri_get_memory_stats() -> Result<serde_json::Value, String> {
    Ok(crate::memory::current_stats())
}

#[tauri::command]
pub fn victauri_verify_state(
    _state: State<'_, Arc<VictauriState>>,
    frontend_state: serde_json::Value,
    backend_state: serde_json::Value,
) -> Result<serde_json::Value, String> {
    let result = victauri_core::verify_state(frontend_state, backend_state);
    serde_json::to_value(result).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn victauri_detect_ghost_commands(
    state: State<'_, Arc<VictauriState>>,
) -> Result<serde_json::Value, String> {
    serde_json::to_value(page_ghost_report(&state)).map_err(|e| e.to_string())
}

/// Ghost-command report from the command NAMES in the log (no call is cloned).
fn page_ghost_report(state: &VictauriState) -> victauri_core::GhostCommandReport {
    let frontend_commands: Vec<String> = state.event_log.ipc_command_names().into_iter().collect();
    victauri_core::detect_ghost_commands(&frontend_commands, &state.registry)
}

#[tauri::command]
pub fn victauri_check_ipc_integrity(
    state: State<'_, Arc<VictauriState>>,
    stale_threshold_ms: Option<i64>,
) -> Result<serde_json::Value, String> {
    serde_json::to_value(page_integrity_report(&state, stale_threshold_ms))
        .map_err(|e| e.to_string())
}

fn page_integrity_report(
    state: &VictauriState,
    stale_threshold_ms: Option<i64>,
) -> victauri_core::IpcIntegrityReport {
    // Scans the log in place (no call is cloned); the lists in the reply are capped to the
    // newest entries, the counts still cover the whole log.
    let mut report =
        victauri_core::check_ipc_integrity(&state.event_log, stale_threshold_ms.unwrap_or(5000));
    keep_newest(&mut report.stale_calls, PAGE_INTEGRITY_MAX_ENTRIES);
    keep_newest(&mut report.error_calls, PAGE_INTEGRITY_MAX_ENTRIES);
    report
}

/// Drop all but the last `max` entries of `v` (the log is oldest-first).
fn keep_newest<T>(v: &mut Vec<T>, max: usize) {
    let excess = v.len().saturating_sub(max);
    v.drain(..excess);
}

#[cfg(test)]
mod tests {
    use super::*;

    /// G-2: `victauri_list_windows` / `victauri_get_window_state` are callable by page script.
    /// Beyond the budget a query is refused at once, and a slot is released when its query
    /// finishes. (As sync commands they cannot contend for it today — see
    /// [`MAX_PAGE_WINDOW_QUERIES`]; this pins the guard itself.)
    #[test]
    fn page_window_queries_are_budgeted() {
        let held: Vec<_> = (0..MAX_PAGE_WINDOW_QUERIES)
            .map(|_| PAGE_WINDOW_QUERY_SLOTS.try_acquire().unwrap())
            .collect();
        let refused = page_window_query(|| 1);
        assert!(
            refused
                .as_ref()
                .is_err_and(|e| e.contains("page-originated window queries")),
            "{refused:?}"
        );
        drop(held);
        assert_eq!(page_window_query(|| 7), Ok(7));
        assert_eq!(
            PAGE_WINDOW_QUERY_SLOTS.available_permits(),
            MAX_PAGE_WINDOW_QUERIES
        );
    }

    fn busy_state(calls: usize) -> VictauriState {
        let mut state = VictauriState::for_tests();
        state.event_log = victauri_core::EventLog::new(calls);
        let body = serde_json::json!({ "payload": "x".repeat(1024) });
        for i in 0..calls {
            let result = if i % 2 == 0 {
                victauri_core::IpcResult::Ok(body.clone())
            } else {
                victauri_core::IpcResult::Err(format!("failed: {}", "e".repeat(256)))
            };
            state
                .event_log
                .push(victauri_core::AppEvent::Ipc(IpcCall::new(
                    format!("c{i}"),
                    format!("cmd{}", i % 20),
                    chrono::Utc::now(),
                    result,
                    Some(1),
                    1024,
                    "main",
                )));
        }
        state
    }

    /// R5B-PAGEQ1: `victauri_get_ipc_log`, `victauri_detect_ghost_commands` and
    /// `victauri_check_ipc_integrity` are page-callable SYNC commands, i.e. they run on the UI
    /// thread, and each cloned the whole Rust IPC log (every call with its body) per call — a
    /// page calling them in a loop janked the UI. The work and the replies are now bounded.
    #[test]
    fn page_ipc_queries_do_bounded_work_on_a_full_log() {
        let state = busy_state(10_000);
        let time = |f: &dyn Fn()| {
            let started = std::time::Instant::now();
            for _ in 0..5 {
                f();
            }
            started.elapsed() / 5
        };
        let full_clone = time(&|| drop(state.event_log.ipc_calls()));
        let log = time(&|| drop(page_ipc_log(&state, None)));
        let ghosts = time(&|| drop(page_ghost_report(&state)));
        let integrity = time(&|| drop(page_integrity_report(&state, None)));
        eprintln!(
            "10,000-call log: full clone {full_clone:?}; get_ipc_log {log:?}; detect_ghost_commands {ghosts:?}; check_ipc_integrity {integrity:?}"
        );

        assert_eq!(page_ipc_log(&state, None).len(), PAGE_IPC_LOG_DEFAULT);
        assert_eq!(
            page_ipc_log(&state, Some(usize::MAX)).len(),
            PAGE_IPC_LOG_MAX
        );
        let newest = page_ipc_log(&state, Some(3));
        let ids: Vec<&str> = newest.iter().map(|c| c.id.as_str()).collect();
        assert_eq!(ids, ["c9997", "c9998", "c9999"]);

        let integrity = page_integrity_report(&state, None);
        assert_eq!(integrity.total_calls, 10_000);
        assert_eq!(integrity.errored, 5_000);
        assert_eq!(integrity.error_calls.len(), PAGE_INTEGRITY_MAX_ENTRIES);
        assert_eq!(
            integrity.error_calls.last().map(|c| c.id.as_str()),
            Some("c9999"),
            "the newest entries are kept"
        );

        let ghosts = page_ghost_report(&state);
        assert_eq!(ghosts.frontend_only.len(), 20);
    }

    /// R5B-EVALWIN1: `victauri_eval_callback` delivered a result for any pending eval id to
    /// whichever window sent it. A result is now accepted only from the window the eval was sent
    /// to — by label, or for an eval sent to the default window, from the window the default
    /// resolves to.
    #[tokio::test]
    async fn eval_results_are_accepted_only_from_the_target_window() {
        let state = Arc::new(VictauriState::for_tests());
        let no_default = || None;

        // A page eval for window "main".
        let (tx, mut rx) = tokio::sync::oneshot::channel();
        let slot = reserve_page_eval(&state, "main", tx).await.unwrap();
        let id = slot.id().to_string();
        assert!(!accept_eval_result(
            Arc::clone(&state),
            "other",
            no_default,
            id.clone(),
            "forged".into()
        ));
        assert!(
            rx.try_recv().is_err(),
            "another window's result was delivered"
        );
        assert!(accept_eval_result(
            Arc::clone(&state),
            "main",
            no_default,
            id,
            "real".into()
        ));
        assert_eq!(rx.await.unwrap(), "real");
        drop(slot);

        // An agent eval sent to the default window.
        let (tx, mut rx) = tokio::sync::oneshot::channel();
        let slot = {
            let mut pending = state.pending_evals.lock().await;
            crate::PendingSlot::insert(&state.pending_evals, &mut pending, "agent-1".into(), tx)
        };
        slot.bind_window(None);
        let main_is_default = || Some("main".to_string());
        assert!(!accept_eval_result(
            Arc::clone(&state),
            "other",
            main_is_default,
            "agent-1".into(),
            "x".into()
        ));
        assert!(rx.try_recv().is_err());
        // Narrowed to the window it actually resolved to.
        slot.bind_window(Some("side"));
        assert!(!accept_eval_result(
            Arc::clone(&state),
            "main",
            main_is_default,
            "agent-1".into(),
            "x".into()
        ));
        assert!(accept_eval_result(
            Arc::clone(&state),
            "side",
            main_is_default,
            "agent-1".into(),
            "ok".into()
        ));
        assert_eq!(rx.await.unwrap(), "ok");

        // An eval that was never bound, and an id with no pending eval, are refused.
        let (tx, _rx) = tokio::sync::oneshot::channel();
        let unbound = {
            let mut pending = state.pending_evals.lock().await;
            crate::PendingSlot::insert(&state.pending_evals, &mut pending, "agent-2".into(), tx)
        };
        assert!(!accept_eval_result(
            Arc::clone(&state),
            "main",
            main_is_default,
            "agent-2".into(),
            "x".into()
        ));
        assert!(!accept_eval_result(
            Arc::clone(&state),
            "main",
            main_is_default,
            "nope".into(),
            "x".into()
        ));
        drop(unbound);
        // The binding goes with the slot.
        drop(slot);
        assert!(!crate::eval_result_accepted("agent-1", "side", || None));
    }

    /// Tauri runs an `async` command on a tokio worker and extracts/drops its arguments and its
    /// resolver (which holds a `Webview`) there; on Linux every Tauri handle carries a non-atomic
    /// `Rc`, and doing that for the eval callback on every eval corrupted the host heap (0.9.0).
    /// Only the two commands that genuinely wait for the page may be `async`. A new `async`
    /// command here must first be shown not to touch Tauri handles off the main thread.
    #[test]
    fn only_the_commands_that_wait_for_the_page_are_async() {
        let source = include_str!("tools.rs");
        let async_commands: Vec<&str> = source
            .lines()
            .filter_map(|l| l.trim_start().strip_prefix("pub async fn victauri_"))
            .map(|rest| rest.split(['<', '(']).next().unwrap_or(rest))
            .collect();
        assert_eq!(
            async_commands,
            vec!["eval_js", "dom_snapshot"],
            "only victauri_eval_js / victauri_dom_snapshot may be async commands"
        );
        assert!(
            source.contains("pub fn victauri_eval_callback<R: Runtime>("),
            "victauri_eval_callback must stay a synchronous command"
        );
    }

    /// Every `.rs` file under this crate's `src/`, as `(path, source)`.
    fn crate_sources() -> Vec<(std::path::PathBuf, String)> {
        fn walk(dir: &std::path::Path, out: &mut Vec<(std::path::PathBuf, String)>) {
            for entry in std::fs::read_dir(dir).unwrap() {
                let path = entry.unwrap().path();
                if path.is_dir() {
                    walk(&path, out);
                } else if path.extension().is_some_and(|e| e == "rs") {
                    let source = std::fs::read_to_string(&path).unwrap();
                    out.push((path, source));
                }
            }
        }
        let mut out = Vec::new();
        walk(
            &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
            &mut out,
        );
        out
    }

    /// The same rule from the other side: `#[tauri::command]` makes a `fn` an async
    /// command without the `async` keyword, and a command defined outside this file would escape
    /// the check above. So: no `#[tauri::command(…)]` attribute anywhere in the crate may mention
    /// `async`, and every command the plugin registers is sync except the two page waiters.
    #[test]
    fn every_registered_command_is_sync_except_the_page_waiters() {
        let sources = crate_sources();
        for (path, source) in &sources {
            for line in source.lines() {
                let attr: String = line.chars().filter(|c| !c.is_whitespace()).collect();
                if attr.starts_with("#[tauri::command(") {
                    assert!(
                        !attr.contains("async"),
                        "{}: async command attribute `{}` — see \
                         only_the_commands_that_wait_for_the_page_are_async",
                        path.display(),
                        line.trim()
                    );
                }
            }
        }

        let lib = include_str!("lib.rs");
        let start = lib
            .find("generate_handler![")
            .expect("lib.rs registers its commands with generate_handler!");
        let list = &lib[start + "generate_handler![".len()..];
        let list = &list[..list.find(']').expect("generate_handler! list is closed")];
        let registered: Vec<&str> = list
            .split(',')
            .map(str::trim)
            .filter(|s| !s.is_empty())
            .collect();
        assert!(registered.len() >= 10, "parsed {registered:?}");
        for path in &registered {
            let name = path.rsplit("::").next().unwrap_or(path);
            let sync_def = format!("pub fn {name}");
            let async_def = format!("pub async fn {name}");
            let defs: Vec<bool> = sources
                .iter()
                .flat_map(|(_, s)| s.lines())
                .map(str::trim_start)
                .filter_map(|l| {
                    let is_def = |d: &str| {
                        l.strip_prefix(d)
                            .is_some_and(|rest| rest.starts_with(['<', '(']))
                    };
                    if is_def(&async_def) {
                        Some(true)
                    } else if is_def(&sync_def) {
                        Some(false)
                    } else {
                        None
                    }
                })
                .collect();
            assert_eq!(defs.len(), 1, "command {path}: expected one definition");
            let may_be_async = matches!(name, "victauri_eval_js" | "victauri_dom_snapshot");
            assert!(
                !defs[0] || may_be_async,
                "registered command {path} is async — only the page waiters may be"
            );
        }
    }
}