1
//! Keyring manager — cross-platform state for the system-keyring surface
2
//! (`SUPER_PLAN_2` §4 P4.2).
3
//!
4
//! Request-driven, mirroring [`crate::managers::biometric`]:
5
//!
6
//! - A **callback** calls `CallbackInfo::keyring_store/get/delete(...)`,
7
//!   which parks a [`KeyringRequest`] in the request channel.
8
//! - The dll **layout pass** drains it and dispatches to the platform
9
//!   backend (`dll::desktop::extra::keyring`) — Keychain / `KeyStore` /
10
//!   libsecret / `CredentialLocker`. A biometry-bound `Get` shows the OS
11
//!   prompt; the outcome is parked in the result channel.
12
//! - The layout pass folds the latest result into the manager via
13
//!   [`KeyringManager::set_last_result`]; callbacks read it with
14
//!   `CallbackInfo::get_keyring_result()`.
15
//!
16
//! No platform deps (`SUPER_PLAN_2` §0.5); the channels are the same
17
//! poison-recovering `Mutex<Vec<_>>` pattern as the geolocation /
18
//! biometric managers.
19

            
20
use alloc::vec::Vec;
21

            
22
// `KeyringRequest` / `KeyringResult` live in `azul-core` so they cross the
23
// FFI without a cyclic dep on `azul-layout`. Re-exported for the existing
24
// `azul_layout::managers::keyring::*` import paths.
25
pub use azul_core::keyring::{KeyringRequest, KeyringResult};
26

            
27
use azul_core::dom::DomNodeId;
28
use azul_core::events::{
29
    EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
30
};
31
use azul_core::task::Instant;
32

            
33
/// Cross-platform keyring state. One per `App` — the OS keyring is a
34
/// per-process (per-app-identity) store, not per-window.
35
#[derive(Debug, Clone, PartialEq, Eq, Default)]
36
pub struct KeyringManager {
37
    /// Outcome of the most recent keyring op, or `None` until the first
38
    /// completes. Read by callbacks via `CallbackInfo::get_keyring_result()`.
39
    pub last_result: Option<KeyringResult>,
40
    /// Ops dispatched to the native backend whose outcome has not been
41
    /// folded back yet (MWA-A1b arming signal for the capability pump).
42
    pub in_flight: u32,
43
    /// `true` when an op outcome was folded since the last event pass (set
44
    /// on EVERY completion — a repeated identical outcome still answers a
45
    /// fresh op). Read by the `EventProvider` impl
46
    /// (`EventType::KeyringResult`), cleared by
47
    /// [`clear_pending_event`](Self::clear_pending_event).
48
    pub pending_event: bool,
49
}
50

            
51
impl KeyringManager {
52
5608
    #[must_use] pub fn new() -> Self {
53
5608
        Self::default()
54
5608
    }
55

            
56
    /// Most recent keyring outcome, or `None` until the first op resolves.
57
21
    #[must_use] pub const fn last_result(&self) -> Option<&KeyringResult> {
58
21
        self.last_result.as_ref()
59
21
    }
60

            
61
    /// Apply the outcome the backend delivered. Returns `true` if it
62
    /// differs from the previous one (so the window can be marked dirty to
63
    /// re-render the revealed / stored state).
64
1055
    pub fn set_last_result(&mut self, result: KeyringResult) -> bool {
65
1055
        let changed = self.last_result.as_ref() != Some(&result);
66
1055
        self.last_result = Some(result);
67
        // MWA-A1b: every completion fires an event and retires one
68
        // in-flight op.
69
1055
        self.pending_event = true;
70
1055
        self.in_flight = self.in_flight.saturating_sub(1);
71
1055
        changed
72
1055
    }
73

            
74
    /// The pump dispatched `n` ops to the native backend; keep the timer
75
    /// armed until their outcomes fold back (MWA-A1b).
76
10019
    pub const fn mark_requests_dispatched(&mut self, n: u32) {
77
10019
        self.in_flight = self.in_flight.saturating_add(n);
78
10019
    }
79

            
80
    /// Clear the pending-event flag. The dll calls this after the event
81
    /// pass has collected the `KeyringResult` event.
82
81
    pub const fn clear_pending_event(&mut self) {
83
81
        self.pending_event = false;
84
81
    }
85

            
86
    /// `true` while a dispatched op's outcome is still outstanding
87
    /// (MWA-A1b arming signal).
88
20
    #[must_use] pub const fn has_pending_async(&self) -> bool {
89
20
        self.in_flight > 0
90
20
    }
91
}
92

            
93
impl EventProvider for KeyringManager {
94
    /// Yield a window-level `KeyringResult` event when an op outcome was
95
    /// folded since the last pass (target = root; read the outcome via
96
    /// `CallbackInfo::get_keyring_result` inside the callback).
97
85
    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
98
85
        if self.pending_event {
99
6
            alloc::vec![SyntheticEvent::new(
100
6
                EventType::KeyringResult,
101
6
                CoreEventSource::User,
102
                DomNodeId::ROOT,
103
6
                timestamp,
104
6
                EventData::None,
105
            )]
106
        } else {
107
79
            Vec::new()
108
        }
109
85
    }
110
}
111

            
112
// ────────── Request channel (callback → platform backend) ─────────────
113

            
114
static PENDING_REQUESTS: std::sync::Mutex<Vec<KeyringRequest>> =
115
    std::sync::Mutex::new(Vec::new());
116

            
117
/// Queue a keyring op from a callback. Drained by the dll layout pass and
118
/// dispatched to the native keyring. Thread-safe; poison-recovering.
119
1408
pub fn push_keyring_request(request: KeyringRequest) {
120
1408
    let mut q = PENDING_REQUESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
121
1408
    q.push(request);
122
1408
}
123

            
124
/// Drain every queued keyring op, in arrival order. Called once per
125
/// layout pass; the dll dispatches each to the platform backend.
126
19
pub fn drain_keyring_requests() -> Vec<KeyringRequest> {
127
19
    let mut q = PENDING_REQUESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
128
19
    core::mem::take(&mut *q)
129
19
}
130

            
131
/// MWA-C-biometric/keyring: see `biometric::has_queued_requests` — pump
132
/// arming must count parked-but-undispatched requests.
133
6
pub fn has_queued_requests() -> bool {
134
6
    PENDING_REQUESTS
135
6
        .lock()
136
6
        .map_or_else(|e| !e.into_inner().is_empty(), |q| !q.is_empty())
137
6
}
138

            
139
// ────────── Result channel (platform backend → manager) ───────────────
140

            
141
static PENDING_RESULTS: std::sync::Mutex<Vec<KeyringResult>> =
142
    std::sync::Mutex::new(Vec::new());
143

            
144
/// Park a keyring result delivered by a platform backend (in the dll).
145
/// Thread-safe; poison-recovering (a biometry-bound `Get` resolves from
146
/// the OS prompt's reply on an arbitrary thread).
147
7
pub fn push_keyring_result(result: KeyringResult) {
148
7
    let mut q = PENDING_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
149
7
    q.push(result);
150
7
}
151

            
152
/// Drain every parked keyring result, in arrival order. Called once per
153
/// layout pass; the caller applies them via [`KeyringManager::set_last_result`].
154
14
pub fn drain_keyring_results() -> Vec<KeyringResult> {
155
14
    let mut q = PENDING_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
156
14
    core::mem::take(&mut *q)
157
14
}
158

            
159
#[cfg(test)]
160
mod tests {
161
    use super::*;
162
    use azul_css::AzString;
163

            
164
    #[test]
165
1
    fn manager_defaults_to_no_result() {
166
1
        let mgr = KeyringManager::new();
167
1
        assert_eq!(mgr.last_result(), None);
168
1
    }
169

            
170
    #[test]
171
1
    fn set_last_result_returns_change_flag() {
172
1
        let mut mgr = KeyringManager::new();
173
1
        assert!(mgr.set_last_result(KeyringResult::Stored));
174
1
        assert_eq!(mgr.last_result(), Some(&KeyringResult::Stored));
175
        // Re-applying the same outcome is not a change.
176
1
        assert!(!mgr.set_last_result(KeyringResult::Stored));
177
        // A new outcome is a change.
178
1
        assert!(mgr.set_last_result(KeyringResult::Deleted));
179
1
    }
180

            
181
    #[test]
182
1
    fn result_helpers() {
183
1
        let secret = KeyringResult::Retrieved(AzString::from_const_str("hunter2"));
184
1
        assert_eq!(secret.secret().map(AzString::as_str), Some("hunter2"));
185
1
        assert!(secret.is_ok());
186
1
        assert!(KeyringResult::Stored.is_ok());
187
1
        assert!(KeyringResult::Deleted.is_ok());
188
4
        for r in [
189
1
            KeyringResult::NotFound,
190
1
            KeyringResult::Denied,
191
1
            KeyringResult::Unavailable,
192
1
            KeyringResult::Error,
193
        ] {
194
4
            assert!(!r.is_ok(), "{r:?} must not be ok");
195
4
            assert_eq!(r.secret(), None);
196
        }
197
1
    }
198

            
199
    #[test]
200
1
    fn requests_round_trip_through_channel() {
201
        // Process-global; serialize against every other channel test, then
202
        // clear residue.
203
1
        let _guard = autotest_generated::lock_channels();
204
1
        drop(drain_keyring_requests());
205

            
206
1
        push_keyring_request(KeyringRequest::Store {
207
1
            key: AzString::from_const_str("token"),
208
1
            secret: AzString::from_const_str("abc"),
209
1
            require_biometry: true,
210
1
        });
211
1
        push_keyring_request(KeyringRequest::Get {
212
1
            key: AzString::from_const_str("token"),
213
1
        });
214
1
        let drained = drain_keyring_requests();
215
1
        assert_eq!(drained.len(), 2, "both queued requests drain in order");
216
1
        assert!(matches!(drained[0], KeyringRequest::Store { .. }));
217
1
        assert!(matches!(drained[1], KeyringRequest::Get { .. }));
218
1
        assert!(drain_keyring_requests().is_empty());
219
1
    }
220

            
221
    #[test]
222
1
    fn results_round_trip_through_manager() {
223
        // Process-global; serialize against every other channel test, then
224
        // clear residue.
225
1
        let _guard = autotest_generated::lock_channels();
226
1
        drop(drain_keyring_results());
227

            
228
1
        push_keyring_result(KeyringResult::NotFound);
229
1
        push_keyring_result(KeyringResult::Retrieved(AzString::from_const_str("s"))); // last wins
230
1
        let drained = drain_keyring_results();
231
1
        assert_eq!(drained.len(), 2);
232

            
233
1
        let mut mgr = KeyringManager::new();
234
3
        for r in drained {
235
2
            mgr.set_last_result(r);
236
2
        }
237
1
        assert_eq!(
238
1
            mgr.last_result().and_then(|r| r.secret()).map(AzString::as_str),
239
            Some("s"),
240
            "the last applied result wins"
241
        );
242
1
        assert!(drain_keyring_results().is_empty());
243
1
    }
244
}
245

            
246
#[cfg(test)]
247
mod pump_provider_tests {
248
    use super::*;
249
    use azul_core::task::{Instant, SystemTick};
250

            
251
3
    fn ts() -> Instant {
252
3
        Instant::Tick(SystemTick::new(0))
253
3
    }
254

            
255
    #[test]
256
1
    fn in_flight_and_events_track_op_lifecycle() {
257
1
        let mut mgr = KeyringManager::new();
258
1
        assert!(!mgr.has_pending_async());
259
1
        mgr.mark_requests_dispatched(1);
260
1
        assert!(mgr.has_pending_async());
261
1
        assert!(mgr.get_pending_events(ts()).is_empty(), "no outcome yet");
262

            
263
1
        mgr.set_last_result(KeyringResult::Stored);
264
1
        assert!(!mgr.has_pending_async());
265
1
        let events = mgr.get_pending_events(ts());
266
1
        assert_eq!(events.len(), 1);
267
1
        assert_eq!(events[0].event_type, EventType::KeyringResult);
268
1
        mgr.clear_pending_event();
269

            
270
        // repeated identical outcome still fires — it answers a fresh op
271
1
        mgr.mark_requests_dispatched(1);
272
1
        mgr.set_last_result(KeyringResult::Stored);
273
1
        assert_eq!(mgr.get_pending_events(ts()).len(), 1);
274
1
    }
275
}
276

            
277
#[cfg(test)]
278
mod autotest_generated {
279
    use std::sync::{Mutex, PoisonError};
280

            
281
    use azul_core::task::SystemTick;
282
    use azul_css::AzString;
283

            
284
    use super::*;
285

            
286
    /// The request/result channels are process-global statics and `cargo
287
    /// test` runs tests in parallel threads, so EVERY test that pushes to or
288
    /// drains a channel must hold this lock — otherwise one test's pushes
289
    /// land inside another's drain window. Shared with the hand-written
290
    /// `tests` module above.
291
    static CHANNEL_LOCK: Mutex<()> = Mutex::new(());
292

            
293
    pub(super) fn lock_channels() -> std::sync::MutexGuard<'static, ()> {
294
        CHANNEL_LOCK.lock().unwrap_or_else(PoisonError::into_inner)
295
    }
296

            
297
    fn ts() -> Instant {
298
        Instant::Tick(SystemTick::new(0))
299
    }
300

            
301
    /// Interior NUL, C0/C1 controls and CRLF — breaks naive C-string /
302
    /// byte-length handling in the FFI layer the channels feed.
303
    const NASTY_BYTES: &str = "pw\0with\u{1}nul\u{7f}\r\n\t";
304
    /// Combining marks, an RTL override, the replacement char and the
305
    /// maximum scalar value.
306
    const NASTY_UNICODE: &str = "🔑 ключ 鍵 مفتاح e\u{301}\u{202e}terces\u{202c}\u{fffd}\u{10ffff}";
307

            
308
    /// Every `KeyringResult` variant, in declaration order.
309
    fn all_results() -> [KeyringResult; 7] {
310
        [
311
            KeyringResult::Stored,
312
            KeyringResult::Retrieved(AzString::from("s3cr3t")),
313
            KeyringResult::Deleted,
314
            KeyringResult::NotFound,
315
            KeyringResult::Denied,
316
            KeyringResult::Unavailable,
317
            KeyringResult::Error,
318
        ]
319
    }
320

            
321
    // ── constructor: KeyringManager::new ───────────────────────────────
322

            
323
    #[test]
324
    fn new_equals_default_and_holds_the_zero_state_invariants() {
325
        let a = KeyringManager::new();
326
        let b = KeyringManager::default();
327
        assert_eq!(a, b, "new() must be exactly default()");
328

            
329
        // Every documented field is at its zero value, and the derived
330
        // accessors agree with the raw fields.
331
        assert_eq!(a.last_result, None);
332
        assert_eq!(a.last_result(), None);
333
        assert_eq!(a.in_flight, 0);
334
        assert!(!a.has_pending_async());
335
        assert!(!a.pending_event);
336
        assert!(
337
            a.get_pending_events(ts()).is_empty(),
338
            "a fresh manager has nothing to report"
339
        );
340
    }
341

            
342
    #[test]
343
    fn new_carries_no_hidden_global_state_between_instances() {
344
        // The manager is per-App, but the channels are process-global — a
345
        // fresh manager must not pick up anything parked in them.
346
        let _guard = lock_channels();
347
        drop(drain_keyring_results());
348
        push_keyring_result(KeyringResult::Retrieved(AzString::from("leaked")));
349

            
350
        let mgr = KeyringManager::new();
351
        assert_eq!(
352
            mgr.last_result(),
353
            None,
354
            "construction must not drain the result channel"
355
        );
356

            
357
        // The parked result is still there — new() did not consume it.
358
        assert_eq!(drain_keyring_results().len(), 1);
359
    }
360

            
361
    // ── getter: KeyringManager::last_result ────────────────────────────
362

            
363
    #[test]
364
    fn last_result_returns_the_exact_outcome_that_was_folded() {
365
        let mut mgr = KeyringManager::new();
366
        assert_eq!(mgr.last_result(), None, "None until the first op resolves");
367

            
368
        for r in all_results() {
369
            mgr.set_last_result(r.clone());
370
            assert_eq!(
371
                mgr.last_result(),
372
                Some(&r),
373
                "last_result() must hand back exactly what was folded"
374
            );
375
        }
376
    }
377

            
378
    #[test]
379
    fn last_result_preserves_nul_control_and_max_scalar_payloads() {
380
        // `AzString::as_str` is `from_utf8_unchecked`, so a byte-level
381
        // mangling on the way through the manager would be UB rather than a
382
        // clean error — assert byte equality, not just string equality.
383
        for payload in [NASTY_BYTES, NASTY_UNICODE, ""] {
384
            let mut mgr = KeyringManager::new();
385
            mgr.set_last_result(KeyringResult::Retrieved(AzString::from(payload)));
386

            
387
            let s = mgr
388
                .last_result()
389
                .and_then(KeyringResult::secret)
390
                .expect("Retrieved must expose its secret");
391
            assert_eq!(s.as_str(), payload);
392
            assert_eq!(s.as_str().as_bytes(), payload.as_bytes());
393
            assert_eq!(s.as_str().len(), payload.len(), "nothing truncated at the NUL");
394
        }
395
    }
396

            
397
    #[test]
398
    fn last_result_handles_a_megabyte_secret() {
399
        let big = "k".repeat(1 << 20);
400
        let mut mgr = KeyringManager::new();
401
        assert!(mgr.set_last_result(KeyringResult::Retrieved(AzString::from(big.clone()))));
402

            
403
        let s = mgr
404
            .last_result()
405
            .and_then(KeyringResult::secret)
406
            .expect("Retrieved must expose its secret");
407
        assert_eq!(s.as_str().len(), 1 << 20);
408
        assert_eq!(s.as_str(), big.as_str());
409

            
410
        // Re-folding the identical megabyte payload is not a change (the
411
        // comparison must be by value, not by pointer).
412
        assert!(!mgr.set_last_result(KeyringResult::Retrieved(AzString::from(big))));
413
    }
414

            
415
    #[test]
416
    fn last_result_survives_the_flag_and_counter_mutators() {
417
        let mut mgr = KeyringManager::new();
418
        mgr.set_last_result(KeyringResult::Retrieved(AzString::from("keep-me")));
419

            
420
        mgr.clear_pending_event();
421
        mgr.mark_requests_dispatched(7);
422
        mgr.mark_requests_dispatched(0);
423

            
424
        assert_eq!(
425
            mgr.last_result().and_then(KeyringResult::secret).map(AzString::as_str),
426
            Some("keep-me"),
427
            "neither the event flag nor the in-flight counter may clobber the outcome"
428
        );
429
    }
430

            
431
    // ── other: KeyringManager::set_last_result ─────────────────────────
432

            
433
    #[test]
434
    fn set_last_result_change_flag_is_true_only_on_a_distinct_outcome() {
435
        let mut mgr = KeyringManager::new();
436
        for r in all_results() {
437
            assert!(
438
                mgr.set_last_result(r.clone()),
439
                "{r:?} differs from the previous outcome → changed"
440
            );
441
            assert!(
442
                !mgr.set_last_result(r.clone()),
443
                "re-folding the identical {r:?} is not a change"
444
            );
445
        }
446
    }
447

            
448
    #[test]
449
    fn set_last_result_compares_retrieved_payloads_by_value() {
450
        let mut mgr = KeyringManager::new();
451
        assert!(mgr.set_last_result(KeyringResult::Retrieved(AzString::from("a"))));
452
        assert!(
453
            !mgr.set_last_result(KeyringResult::Retrieved(AzString::from("a"))),
454
            "same payload, freshly allocated → not a change"
455
        );
456
        assert!(
457
            mgr.set_last_result(KeyringResult::Retrieved(AzString::from("b"))),
458
            "a different secret under the same variant IS a change"
459
        );
460
        // An empty secret is a *present* secret, distinct from NotFound.
461
        assert!(mgr.set_last_result(KeyringResult::Retrieved(AzString::from(""))));
462
        assert!(!mgr.set_last_result(KeyringResult::Retrieved(AzString::from(""))));
463
        assert!(
464
            mgr.set_last_result(KeyringResult::NotFound),
465
            "Retrieved(\"\") and NotFound must not collapse into one another"
466
        );
467
    }
468

            
469
    #[test]
470
    fn set_last_result_fires_the_event_even_when_the_outcome_is_unchanged() {
471
        // MWA-A1b: a repeated identical outcome still answers a *fresh* op,
472
        // so the event must fire regardless of the change flag.
473
        let mut mgr = KeyringManager::new();
474
        mgr.set_last_result(KeyringResult::Denied);
475
        mgr.clear_pending_event();
476

            
477
        assert!(!mgr.set_last_result(KeyringResult::Denied), "not a change…");
478
        assert!(mgr.pending_event, "…but still a completion → event pending");
479
        assert_eq!(mgr.get_pending_events(ts()).len(), 1);
480
    }
481

            
482
    #[test]
483
    fn set_last_result_without_a_dispatch_does_not_underflow_in_flight() {
484
        // The adversarial case: a backend delivers an outcome nobody armed.
485
        // A wrapping `-1` here would leave in_flight = u32::MAX and pin the
486
        // capability pump on forever.
487
        let mut mgr = KeyringManager::new();
488
        assert_eq!(mgr.in_flight, 0);
489
        for _ in 0..1_000 {
490
            mgr.set_last_result(KeyringResult::Error);
491
        }
492
        assert_eq!(mgr.in_flight, 0, "saturating_sub must clamp at 0, not wrap");
493
        assert!(!mgr.has_pending_async());
494
    }
495

            
496
    #[test]
497
    fn set_last_result_retires_exactly_one_in_flight_op_per_fold() {
498
        let mut mgr = KeyringManager::new();
499
        mgr.mark_requests_dispatched(3);
500
        for expected in [2_u32, 1, 0] {
501
            mgr.set_last_result(KeyringResult::Stored);
502
            assert_eq!(mgr.in_flight, expected, "one fold retires exactly one op");
503
        }
504
        assert!(!mgr.has_pending_async(), "all dispatched ops resolved");
505

            
506
        // Extra outcomes past the dispatched count clamp at zero.
507
        mgr.set_last_result(KeyringResult::Deleted);
508
        assert_eq!(mgr.in_flight, 0);
509
    }
510

            
511
    #[test]
512
    fn set_last_result_retires_one_op_even_at_the_u32_max_ceiling() {
513
        let mut mgr = KeyringManager::new();
514
        mgr.in_flight = u32::MAX;
515
        mgr.set_last_result(KeyringResult::Stored);
516
        assert_eq!(
517
            mgr.in_flight,
518
            u32::MAX - 1,
519
            "the counter is only saturated at the bottom, not stuck at the top"
520
        );
521
        assert!(mgr.has_pending_async());
522
    }
523

            
524
    // ── numeric: KeyringManager::mark_requests_dispatched ──────────────
525

            
526
    #[test]
527
    fn mark_requests_dispatched_zero_is_a_no_op() {
528
        let mut mgr = KeyringManager::new();
529
        mgr.mark_requests_dispatched(0);
530
        assert_eq!(mgr.in_flight, 0);
531
        assert!(
532
            !mgr.has_pending_async(),
533
            "dispatching nothing must not arm the pump"
534
        );
535

            
536
        mgr.mark_requests_dispatched(5);
537
        mgr.mark_requests_dispatched(0);
538
        assert_eq!(mgr.in_flight, 5, "a zero dispatch must not disturb the count");
539
    }
540

            
541
    #[test]
542
    fn mark_requests_dispatched_accumulates_across_calls() {
543
        let mut mgr = KeyringManager::new();
544
        mgr.mark_requests_dispatched(1);
545
        mgr.mark_requests_dispatched(2);
546
        mgr.mark_requests_dispatched(3);
547
        assert_eq!(mgr.in_flight, 6);
548

            
549
        let mut many = KeyringManager::new();
550
        for _ in 0..10_000 {
551
            many.mark_requests_dispatched(1);
552
        }
553
        assert_eq!(many.in_flight, 10_000);
554
        assert!(many.has_pending_async());
555
    }
556

            
557
    #[test]
558
    fn mark_requests_dispatched_saturates_at_u32_max_instead_of_overflowing() {
559
        // A debug build would panic on a plain `+` here; the contract is
560
        // saturation.
561
        let mut mgr = KeyringManager::new();
562
        mgr.mark_requests_dispatched(u32::MAX);
563
        assert_eq!(mgr.in_flight, u32::MAX);
564

            
565
        mgr.mark_requests_dispatched(1);
566
        assert_eq!(mgr.in_flight, u32::MAX, "saturating_add clamps at the ceiling");
567
        mgr.mark_requests_dispatched(u32::MAX);
568
        assert_eq!(mgr.in_flight, u32::MAX);
569
        assert!(mgr.has_pending_async());
570

            
571
        // And from a non-zero base: MAX-1 plus 2 still clamps.
572
        let mut near = KeyringManager::new();
573
        near.in_flight = u32::MAX - 1;
574
        near.mark_requests_dispatched(2);
575
        assert_eq!(near.in_flight, u32::MAX);
576
    }
577

            
578
    #[test]
579
    fn mark_requests_dispatched_touches_neither_the_event_flag_nor_the_outcome() {
580
        let mut mgr = KeyringManager::new();
581
        mgr.mark_requests_dispatched(u32::MAX);
582
        assert!(
583
            !mgr.pending_event,
584
            "dispatching is not a completion — no event may fire"
585
        );
586
        assert_eq!(mgr.last_result(), None);
587
        assert!(mgr.get_pending_events(ts()).is_empty());
588
    }
589

            
590
    // ── other: KeyringManager::clear_pending_event ─────────────────────
591

            
592
    #[test]
593
    fn clear_pending_event_is_idempotent_and_only_touches_the_flag() {
594
        let mut mgr = KeyringManager::new();
595
        // Clearing an already-clear flag is a no-op, not an underflow.
596
        mgr.clear_pending_event();
597
        assert!(!mgr.pending_event);
598

            
599
        mgr.mark_requests_dispatched(2);
600
        mgr.set_last_result(KeyringResult::Stored);
601
        assert!(mgr.pending_event);
602

            
603
        mgr.clear_pending_event();
604
        mgr.clear_pending_event();
605
        assert!(!mgr.pending_event);
606
        assert!(
607
            mgr.get_pending_events(ts()).is_empty(),
608
            "a cleared flag yields no events"
609
        );
610

            
611
        // The outcome and the in-flight counter are untouched by the clear.
612
        assert_eq!(mgr.last_result(), Some(&KeyringResult::Stored));
613
        assert_eq!(mgr.in_flight, 1);
614
        assert!(
615
            mgr.has_pending_async(),
616
            "clearing the event flag must not disarm the still-outstanding op"
617
        );
618
    }
619

            
620
    // ── predicate: KeyringManager::has_pending_async ───────────────────
621

            
622
    #[test]
623
    fn has_pending_async_is_exactly_in_flight_greater_than_zero() {
624
        let mut mgr = KeyringManager::new();
625
        for (in_flight, expected) in [(0_u32, false), (1, true), (2, true), (u32::MAX, true)] {
626
            mgr.in_flight = in_flight;
627
            assert_eq!(
628
                mgr.has_pending_async(),
629
                expected,
630
                "in_flight = {in_flight} → has_pending_async() = {expected}"
631
            );
632
        }
633
    }
634

            
635
    #[test]
636
    fn has_pending_async_goes_false_only_when_the_last_op_folds_back() {
637
        let mut mgr = KeyringManager::new();
638
        assert!(!mgr.has_pending_async());
639
        mgr.mark_requests_dispatched(2);
640
        assert!(mgr.has_pending_async());
641
        mgr.set_last_result(KeyringResult::Stored);
642
        assert!(mgr.has_pending_async(), "one op is still outstanding");
643
        mgr.set_last_result(KeyringResult::Stored);
644
        assert!(!mgr.has_pending_async(), "both ops resolved → pump disarms");
645
    }
646

            
647
    // ── EventProvider ──────────────────────────────────────────────────
648

            
649
    #[test]
650
    fn pending_event_is_a_root_targeted_user_sourced_keyring_result() {
651
        let mut mgr = KeyringManager::new();
652
        mgr.set_last_result(KeyringResult::Retrieved(AzString::from("s")));
653

            
654
        let stamp = Instant::Tick(SystemTick::new(u64::MAX));
655
        let events = mgr.get_pending_events(stamp.clone());
656
        assert_eq!(events.len(), 1, "exactly one event per pass");
657

            
658
        let e = &events[0];
659
        assert_eq!(e.event_type, EventType::KeyringResult);
660
        assert_eq!(e.source, CoreEventSource::User);
661
        assert_eq!(e.target, DomNodeId::ROOT, "keyring events are window-level");
662
        assert_eq!(e.current_target, DomNodeId::ROOT);
663
        assert_eq!(e.timestamp, stamp, "the caller's timestamp is echoed verbatim");
664
        assert!(
665
            matches!(e.data, EventData::None),
666
            "the outcome is read via CallbackInfo, not carried in the event"
667
        );
668
    }
669

            
670
    #[test]
671
    fn get_pending_events_is_non_destructive_until_cleared() {
672
        // The dll clears the flag explicitly; polling must not consume it,
673
        // or an event pass that reads twice would drop the event.
674
        let mut mgr = KeyringManager::new();
675
        mgr.set_last_result(KeyringResult::Deleted);
676

            
677
        assert_eq!(mgr.get_pending_events(ts()).len(), 1);
678
        assert_eq!(mgr.get_pending_events(ts()).len(), 1, "still pending");
679
        assert!(mgr.pending_event);
680

            
681
        mgr.clear_pending_event();
682
        assert!(mgr.get_pending_events(ts()).is_empty());
683
    }
684

            
685
    // ── channels: push / drain / has_queued_requests ───────────────────
686

            
687
    #[test]
688
    fn request_channel_is_fifo_across_every_variant() {
689
        let _guard = lock_channels();
690
        drop(drain_keyring_requests());
691

            
692
        push_keyring_request(KeyringRequest::Store {
693
            key: AzString::from("k1"),
694
            secret: AzString::from("s1"),
695
            require_biometry: true,
696
        });
697
        push_keyring_request(KeyringRequest::Get {
698
            key: AzString::from("k2"),
699
        });
700
        push_keyring_request(KeyringRequest::Delete {
701
            key: AzString::from("k3"),
702
        });
703

            
704
        let drained = drain_keyring_requests();
705
        assert_eq!(drained.len(), 3);
706
        assert_eq!(
707
            drained[0],
708
            KeyringRequest::Store {
709
                key: AzString::from("k1"),
710
                secret: AzString::from("s1"),
711
                require_biometry: true,
712
            },
713
            "arrival order and payload preserved exactly"
714
        );
715
        assert_eq!(
716
            drained[1],
717
            KeyringRequest::Get {
718
                key: AzString::from("k2")
719
            }
720
        );
721
        assert_eq!(
722
            drained[2],
723
            KeyringRequest::Delete {
724
                key: AzString::from("k3")
725
            }
726
        );
727

            
728
        // The queue was taken, not copied.
729
        assert!(drain_keyring_requests().is_empty());
730
    }
731

            
732
    #[test]
733
    fn draining_an_empty_channel_is_an_empty_vec_not_a_panic() {
734
        let _guard = lock_channels();
735
        drop(drain_keyring_requests());
736
        drop(drain_keyring_results());
737

            
738
        for _ in 0..3 {
739
            assert!(drain_keyring_requests().is_empty());
740
            assert!(drain_keyring_results().is_empty());
741
        }
742
        assert!(!has_queued_requests());
743
    }
744

            
745
    #[test]
746
    fn has_queued_requests_tracks_only_the_request_channel() {
747
        let _guard = lock_channels();
748
        drop(drain_keyring_requests());
749
        drop(drain_keyring_results());
750
        assert!(!has_queued_requests(), "empty queue → nothing to pump");
751

            
752
        // A parked *result* is not a queued *request* — the two statics must
753
        // not be conflated, or the pump would arm on its own output.
754
        push_keyring_result(KeyringResult::Stored);
755
        assert!(!has_queued_requests());
756

            
757
        push_keyring_request(KeyringRequest::Get {
758
            key: AzString::from("k"),
759
        });
760
        assert!(has_queued_requests(), "a parked-but-undispatched request arms the pump");
761
        // Reading the predicate does not consume the request.
762
        assert!(has_queued_requests());
763

            
764
        assert_eq!(drain_keyring_requests().len(), 1);
765
        assert!(!has_queued_requests(), "draining disarms it");
766

            
767
        drop(drain_keyring_results());
768
    }
769

            
770
    #[test]
771
    fn request_channel_preserves_nul_unicode_and_megabyte_payloads() {
772
        let _guard = lock_channels();
773
        drop(drain_keyring_requests());
774

            
775
        let big = "s".repeat(1 << 20);
776
        push_keyring_request(KeyringRequest::Store {
777
            key: AzString::from(NASTY_BYTES),
778
            secret: AzString::from(NASTY_UNICODE),
779
            require_biometry: false,
780
        });
781
        push_keyring_request(KeyringRequest::Get {
782
            key: AzString::from(big.clone()),
783
        });
784

            
785
        let drained = drain_keyring_requests();
786
        assert_eq!(drained.len(), 2);
787

            
788
        let KeyringRequest::Store {
789
            key,
790
            secret,
791
            require_biometry,
792
        } = &drained[0]
793
        else {
794
            panic!("first request must still be the Store: {:?}", drained[0]);
795
        };
796
        // Byte-exact both ways: no truncation at the interior NUL, no
797
        // re-encoding of the astral / combining-mark scalars.
798
        assert_eq!(key.as_str().as_bytes(), NASTY_BYTES.as_bytes());
799
        assert_eq!(secret.as_str().as_bytes(), NASTY_UNICODE.as_bytes());
800
        assert!(secret.as_str().ends_with('\u{10ffff}'));
801
        assert!(!*require_biometry);
802

            
803
        let KeyringRequest::Get { key } = &drained[1] else {
804
            panic!("second request must still be the Get: {:?}", drained[1]);
805
        };
806
        assert_eq!(key.as_str().len(), 1 << 20);
807
        assert_eq!(key.as_str(), big.as_str());
808
    }
809

            
810
    #[test]
811
    fn request_channel_keeps_arrival_order_under_many_pushes() {
812
        let _guard = lock_channels();
813
        drop(drain_keyring_requests());
814

            
815
        const N: usize = 1_000;
816
        for i in 0..N {
817
            push_keyring_request(KeyringRequest::Get {
818
                key: AzString::from(format!("key-{i}")),
819
            });
820
        }
821

            
822
        let drained = drain_keyring_requests();
823
        assert_eq!(drained.len(), N);
824
        for (i, req) in drained.iter().enumerate() {
825
            let KeyringRequest::Get { key } = req else {
826
                panic!("expected a Get at {i}, got {req:?}");
827
            };
828
            assert_eq!(key.as_str(), format!("key-{i}"), "FIFO order at {i}");
829
        }
830
        assert!(drain_keyring_requests().is_empty());
831
    }
832

            
833
    #[test]
834
    fn concurrent_pushes_from_many_threads_lose_no_requests() {
835
        // The channel is documented thread-safe: a biometry-bound Get can
836
        // resolve on an arbitrary thread. Lost or duplicated entries here
837
        // would mean a dropped keyring op.
838
        let _guard = lock_channels();
839
        drop(drain_keyring_requests());
840

            
841
        const THREADS: usize = 8;
842
        const PER_THREAD: usize = 50;
843

            
844
        let handles: Vec<_> = (0..THREADS)
845
            .map(|t| {
846
                std::thread::spawn(move || {
847
                    for i in 0..PER_THREAD {
848
                        push_keyring_request(KeyringRequest::Delete {
849
                            key: AzString::from(format!("t{t}-{i}")),
850
                        });
851
                    }
852
                })
853
            })
854
            .collect();
855
        for h in handles {
856
            h.join().expect("pushing thread must not panic");
857
        }
858

            
859
        let drained = drain_keyring_requests();
860
        assert_eq!(drained.len(), THREADS * PER_THREAD, "no request lost or duplicated");
861

            
862
        // Cross-thread interleaving is unspecified, but the multiset of keys
863
        // must be exactly what was pushed.
864
        let mut keys: Vec<String> = drained
865
            .iter()
866
            .map(|r| match r {
867
                KeyringRequest::Delete { key } => key.as_str().to_string(),
868
                other => panic!("unexpected request in the channel: {other:?}"),
869
            })
870
            .collect();
871
        keys.sort();
872
        let mut expected: Vec<String> = (0..THREADS)
873
            .flat_map(|t| (0..PER_THREAD).map(move |i| format!("t{t}-{i}")))
874
            .collect();
875
        expected.sort();
876
        assert_eq!(keys, expected);
877
    }
878

            
879
    #[test]
880
    fn result_channel_folds_into_the_manager_with_last_write_winning() {
881
        let _guard = lock_channels();
882
        drop(drain_keyring_results());
883

            
884
        let mut mgr = KeyringManager::new();
885
        mgr.mark_requests_dispatched(3);
886

            
887
        push_keyring_result(KeyringResult::Denied);
888
        push_keyring_result(KeyringResult::NotFound);
889
        push_keyring_result(KeyringResult::Retrieved(AzString::from(NASTY_UNICODE)));
890

            
891
        let drained = drain_keyring_results();
892
        assert_eq!(drained.len(), 3, "results drain in arrival order");
893
        assert_eq!(drained[0], KeyringResult::Denied);
894
        assert_eq!(drained[1], KeyringResult::NotFound);
895

            
896
        for r in drained {
897
            mgr.set_last_result(r);
898
        }
899
        assert_eq!(
900
            mgr.last_result().and_then(KeyringResult::secret).map(AzString::as_str),
901
            Some(NASTY_UNICODE),
902
            "the last applied result wins"
903
        );
904
        assert_eq!(mgr.in_flight, 0, "all three dispatched ops retired");
905
        assert!(!mgr.has_pending_async());
906
        assert!(mgr.pending_event);
907
        assert!(drain_keyring_results().is_empty());
908
    }
909
}