Skip to main content

fsqlite_types/
cx.rs

1//! Capability context (`Cx`) for FrankenSQLite.
2//!
3//! This is a **capability-passing style** context object that:
4//! - threads cancellation checks (`checkpoint`) through long-running operations
5//! - carries a [`Budget`] for deadline/priority propagation
6//! - encodes available effects (spawn/time/random/io/remote) in the type system
7//!   via [`cap::CapSet`], so widening is a **compile-time error**.
8//!
9//! # Compile-time capability narrowing
10//!
11//! Narrowing always succeeds:
12//! ```
13//! use fsqlite_types::cx::{cap, Cx};
14//!
15//! let cx = Cx::<cap::All>::new();
16//! let _compute = cx.restrict::<cap::None>();
17//! ```
18//!
19//! Widening is rejected at compile time:
20//! ```compile_fail
21//! use fsqlite_types::cx::{cap, Cx};
22//!
23//! let cx = Cx::<cap::All>::new();
24//! let compute = cx.restrict::<cap::None>();
25//! let _nope = compute.restrict::<cap::All>();
26//! ```
27//
28
29use std::future::Future;
30use std::marker::PhantomData;
31use std::pin::Pin;
32#[cfg(feature = "native")]
33use std::sync::atomic::AtomicU8;
34use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
35use std::sync::{Arc, Mutex, Weak};
36use std::task::{Context as TaskContext, Poll, Waker};
37use std::time::Duration;
38
39use crate::sync_primitives::SystemTime;
40
41#[cfg(feature = "native")]
42use asupersync::types::Time as NativeTime;
43#[cfg(feature = "native")]
44use asupersync::types::{CancelKind as NativeCancelKind, CancelReason as NativeCancelReason};
45#[cfg(feature = "native")]
46use asupersync::{Budget as NativeBudget, Cx as NativeCx};
47
48#[cfg(not(feature = "native"))]
49mod native_cx_shim {
50    use std::sync::atomic::{AtomicBool, Ordering};
51    use std::sync::{Arc, Mutex};
52
53    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
54    pub enum NativeCancelKind {
55        User,
56        Timeout,
57        Deadline,
58        PollQuota,
59        CostBudget,
60        FailFast,
61        RaceLost,
62        ParentCancelled,
63        Shutdown,
64        LinkedExit,
65        ResourceUnavailable,
66    }
67
68    #[derive(Debug, Clone, PartialEq, Eq)]
69    pub struct NativeCancelReason {
70        pub kind: NativeCancelKind,
71    }
72
73    impl NativeCancelReason {
74        #[must_use]
75        pub const fn timeout() -> Self {
76            Self {
77                kind: NativeCancelKind::Timeout,
78            }
79        }
80
81        #[must_use]
82        pub fn user(_message: impl Into<String>) -> Self {
83            Self {
84                kind: NativeCancelKind::User,
85            }
86        }
87
88        #[must_use]
89        pub const fn parent_cancelled() -> Self {
90            Self {
91                kind: NativeCancelKind::ParentCancelled,
92            }
93        }
94
95        #[must_use]
96        pub const fn resource_unavailable() -> Self {
97            Self {
98                kind: NativeCancelKind::ResourceUnavailable,
99            }
100        }
101    }
102
103    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
104    pub struct NativeCheckpointError;
105
106    #[derive(Debug, Default)]
107    struct NativeCxInner {
108        cancel_requested: AtomicBool,
109        cancel_reason: Mutex<Option<NativeCancelReason>>,
110    }
111
112    #[derive(Debug, Clone, Default)]
113    pub struct NativeCx {
114        inner: Arc<NativeCxInner>,
115    }
116
117    impl NativeCx {
118        #[must_use]
119        pub fn for_testing() -> Self {
120            Self::default()
121        }
122
123        pub fn set_cancel_requested(&self, requested: bool) {
124            self.inner
125                .cancel_requested
126                .store(requested, Ordering::Release);
127            if !requested {
128                *self
129                    .inner
130                    .cancel_reason
131                    .lock()
132                    .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
133            }
134        }
135
136        pub fn set_cancel_reason(&self, reason: NativeCancelReason) {
137            *self
138                .inner
139                .cancel_reason
140                .lock()
141                .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(reason);
142            self.inner.cancel_requested.store(true, Ordering::Release);
143        }
144
145        #[must_use]
146        pub fn is_cancel_requested(&self) -> bool {
147            self.inner.cancel_requested.load(Ordering::Acquire)
148        }
149
150        #[must_use]
151        pub fn cancel_reason(&self) -> Option<NativeCancelReason> {
152            self.inner
153                .cancel_reason
154                .lock()
155                .unwrap_or_else(std::sync::PoisonError::into_inner)
156                .clone()
157        }
158
159        pub fn checkpoint(&self) -> std::result::Result<(), NativeCheckpointError> {
160            if self.is_cancel_requested() {
161                Err(NativeCheckpointError)
162            } else {
163                Ok(())
164            }
165        }
166    }
167}
168
169#[cfg(not(feature = "native"))]
170use native_cx_shim::NativeCx;
171
172use crate::eprocess::{EProcessDecision, EProcessOracle, EProcessSnapshot};
173
174/// SQLite error code for `SQLITE_INTERRUPT`.
175pub const SQLITE_INTERRUPT: i32 = 9;
176
177/// Maximum nesting depth for masked cancellation sections (INV-MASK-BOUNDED).
178///
179/// Exceeding this limit panics in lab mode and emits a fatal diagnostic in production.
180pub const MAX_MASK_DEPTH: u32 = 64;
181
182// ---------------------------------------------------------------------------
183// §4.12 Cancellation State Machine
184// ---------------------------------------------------------------------------
185
186/// Observable state of a task's cancellation lifecycle (asupersync oracle model).
187///
188/// ```text
189/// Created → Running → CancelRequested → Cancelling → Finalizing → Completed
190/// ```
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192pub enum CancelState {
193    Created,
194    Running,
195    CancelRequested,
196    Cancelling,
197    Finalizing,
198    Completed,
199}
200
201/// Reason for cancellation, ordered from weakest to strongest.
202///
203/// INV-CANCEL-IDEMPOTENT: multiple cancel requests are monotone — the strongest
204/// reason wins and the reason can never get weaker.
205#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
206pub enum CancelReason {
207    Timeout = 0,
208    UserInterrupt = 1,
209    RegionClose = 2,
210    Abort = 3,
211}
212
213/// Capability set definitions and subset reasoning.
214pub mod cap {
215    mod sealed {
216        pub trait Sealed {}
217
218        pub struct Bit<const V: bool>;
219
220        pub trait Le {}
221        impl Le for (Bit<false>, Bit<false>) {}
222        impl Le for (Bit<false>, Bit<true>) {}
223        impl Le for (Bit<true>, Bit<true>) {}
224    }
225
226    /// Type-level capability set: `[SPAWN, TIME, RANDOM, IO, REMOTE]`.
227    #[derive(Debug, Clone, Copy, Default)]
228    pub struct CapSet<
229        const SPAWN: bool,
230        const TIME: bool,
231        const RANDOM: bool,
232        const IO: bool,
233        const REMOTE: bool,
234    >;
235
236    impl<
237        const SPAWN: bool,
238        const TIME: bool,
239        const RANDOM: bool,
240        const IO: bool,
241        const REMOTE: bool,
242    > sealed::Sealed for CapSet<SPAWN, TIME, RANDOM, IO, REMOTE>
243    {
244    }
245
246    /// Full capability set.
247    pub type All = CapSet<true, true, true, true, true>;
248    /// No capabilities.
249    pub type None = CapSet<false, false, false, false, false>;
250
251    /// Type-level subset relation.
252    ///
253    /// Encodes pointwise ordering on capability bits: `false <= false`, `false <= true`,
254    /// `true <= true`. The missing impl `(true <= false)` forbids widening.
255    pub trait SubsetOf<Super>: sealed::Sealed {}
256
257    impl<
258        const S_SPAWN: bool,
259        const S_TIME: bool,
260        const S_RANDOM: bool,
261        const S_IO: bool,
262        const S_REMOTE: bool,
263        const P_SPAWN: bool,
264        const P_TIME: bool,
265        const P_RANDOM: bool,
266        const P_IO: bool,
267        const P_REMOTE: bool,
268    > SubsetOf<CapSet<P_SPAWN, P_TIME, P_RANDOM, P_IO, P_REMOTE>>
269        for CapSet<S_SPAWN, S_TIME, S_RANDOM, S_IO, S_REMOTE>
270    where
271        (sealed::Bit<S_SPAWN>, sealed::Bit<P_SPAWN>): sealed::Le,
272        (sealed::Bit<S_TIME>, sealed::Bit<P_TIME>): sealed::Le,
273        (sealed::Bit<S_RANDOM>, sealed::Bit<P_RANDOM>): sealed::Le,
274        (sealed::Bit<S_IO>, sealed::Bit<P_IO>): sealed::Le,
275        (sealed::Bit<S_REMOTE>, sealed::Bit<P_REMOTE>): sealed::Le,
276    {
277    }
278
279    pub trait HasSpawn: sealed::Sealed {}
280    impl<const TIME: bool, const RANDOM: bool, const IO: bool, const REMOTE: bool> HasSpawn
281        for CapSet<true, TIME, RANDOM, IO, REMOTE>
282    {
283    }
284
285    pub trait HasTime: sealed::Sealed {}
286    impl<const SPAWN: bool, const RANDOM: bool, const IO: bool, const REMOTE: bool> HasTime
287        for CapSet<SPAWN, true, RANDOM, IO, REMOTE>
288    {
289    }
290
291    pub trait HasRandom: sealed::Sealed {}
292    impl<const SPAWN: bool, const TIME: bool, const IO: bool, const REMOTE: bool> HasRandom
293        for CapSet<SPAWN, TIME, true, IO, REMOTE>
294    {
295    }
296
297    pub trait HasIo: sealed::Sealed {}
298    impl<const SPAWN: bool, const TIME: bool, const RANDOM: bool, const REMOTE: bool> HasIo
299        for CapSet<SPAWN, TIME, RANDOM, true, REMOTE>
300    {
301    }
302
303    pub trait HasRemote: sealed::Sealed {}
304    impl<const SPAWN: bool, const TIME: bool, const RANDOM: bool, const IO: bool> HasRemote
305        for CapSet<SPAWN, TIME, RANDOM, IO, true>
306    {
307    }
308}
309
310/// Connection-level capabilities: everything enabled.
311pub type FullCaps = cap::All;
312/// Storage-layer capabilities: time + I/O only.
313pub type StorageCaps = cap::CapSet<false, true, false, true, false>;
314/// Pure computation capabilities: no I/O, no time, no randomness.
315pub type ComputeCaps = cap::None;
316
317/// A budget for cancellation/deadline/priority propagation.
318///
319/// This is a product lattice with mixed meet/join semantics:
320/// - resource constraints tighten by `min` (deadline/poll/cost)
321/// - priority propagates by `max`
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub struct Budget {
324    /// Relative timeout translated into the receiving runtime's clock domain
325    /// when work crosses a native async boundary.
326    pub deadline: Option<Duration>,
327    pub poll_quota: u32,
328    pub cost_quota: Option<u64>,
329    pub priority: u8,
330}
331
332impl Budget {
333    /// No constraints (identity for [`Self::meet`]).
334    pub const INFINITE: Self = Self {
335        deadline: None,
336        poll_quota: u32::MAX,
337        cost_quota: None,
338        priority: 0,
339    };
340
341    /// Minimal budget for cleanup/finalizers.
342    pub const MINIMAL: Self = Self {
343        deadline: None,
344        poll_quota: 100,
345        cost_quota: None,
346        priority: 0,
347    };
348
349    #[must_use]
350    pub const fn with_deadline(self, deadline: Duration) -> Self {
351        Self {
352            deadline: Some(deadline),
353            ..self
354        }
355    }
356
357    #[must_use]
358    pub const fn with_priority(self, priority: u8) -> Self {
359        Self { priority, ..self }
360    }
361
362    #[must_use]
363    pub const fn with_poll_quota(self, poll_quota: u32) -> Self {
364        Self { poll_quota, ..self }
365    }
366
367    #[must_use]
368    pub const fn with_cost_quota(self, cost_quota: u64) -> Self {
369        Self {
370            cost_quota: Some(cost_quota),
371            ..self
372        }
373    }
374
375    /// Meet (tighten) two budgets.
376    #[must_use]
377    pub fn meet(self, other: Self) -> Self {
378        Self {
379            deadline: match (self.deadline, other.deadline) {
380                (Some(a), Some(b)) => Some(a.min(b)),
381                (Some(a), None) => Some(a),
382                (None, Some(b)) => Some(b),
383                (None, None) => None,
384            },
385            poll_quota: self.poll_quota.min(other.poll_quota),
386            cost_quota: match (self.cost_quota, other.cost_quota) {
387                (Some(a), Some(b)) => Some(a.min(b)),
388                (Some(a), None) => Some(a),
389                (None, Some(b)) => Some(b),
390                (None, None) => None,
391            },
392            priority: self.priority.max(other.priority),
393        }
394    }
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398pub enum ErrorKind {
399    Cancelled,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct Error {
404    kind: ErrorKind,
405}
406
407impl std::fmt::Display for Error {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        match self.kind {
410            ErrorKind::Cancelled => write!(f, "operation cancelled"),
411        }
412    }
413}
414
415impl std::error::Error for Error {}
416
417impl Error {
418    #[must_use]
419    pub const fn cancelled() -> Self {
420        Self {
421            kind: ErrorKind::Cancelled,
422        }
423    }
424
425    #[must_use]
426    pub const fn kind(&self) -> ErrorKind {
427        self.kind
428    }
429
430    #[must_use]
431    pub const fn sqlite_error_code(&self) -> i32 {
432        match self.kind {
433            ErrorKind::Cancelled => SQLITE_INTERRUPT,
434        }
435    }
436}
437
438pub type Result<T, E = Error> = std::result::Result<T, E>;
439
440#[derive(Debug)]
441struct LocalCancelWaiter {
442    id: u64,
443    waker: Waker,
444}
445
446#[derive(Debug, Default)]
447struct LocalCancelWaiters {
448    next_id: u64,
449    entries: Vec<LocalCancelWaiter>,
450}
451
452impl LocalCancelWaiters {
453    fn allocate_id(&mut self) -> u64 {
454        loop {
455            let id = self.next_id;
456            self.next_id = self.next_id.wrapping_add(1);
457            if self.entries.iter().all(|waiter| waiter.id != id) {
458                return id;
459            }
460        }
461    }
462
463    fn remove(&mut self, id: u64) -> Option<LocalCancelWaiter> {
464        self.entries
465            .iter()
466            .position(|waiter| waiter.id == id)
467            .map(|position| self.entries.swap_remove(position))
468    }
469}
470
471#[derive(Debug)]
472struct CxInner {
473    cancel_requested: AtomicBool,
474    // Strongest ordinary cancellation for this local node. Kept separate
475    // from `cancel_reason` so operation-local cancellation is never mirrored
476    // into native I/O. NativeCx clones remain upstream-owned overwrite handles;
477    // this rank intentionally does not claim cross-sibling arbitration.
478    #[cfg(feature = "native")]
479    native_cancel_reason: AtomicU8,
480    cancel_state: Mutex<CancelState>,
481    cancel_reason: Mutex<Option<CancelReason>>,
482    mask_depth: AtomicU32,
483    cancel_dispatch_gate: Arc<Mutex<()>>,
484    local_cancel_waiters: Mutex<LocalCancelWaiters>,
485    children: Mutex<Vec<Weak<Self>>>,
486    last_checkpoint_msg: Mutex<Option<String>>,
487    last_eprocess_decision: Mutex<Option<EProcessDecision>>,
488    eprocess_oracle: std::sync::OnceLock<Arc<EProcessOracle>>,
489    #[cfg(feature = "native")]
490    attached_native_cx: Mutex<Option<NativeCx>>,
491    #[cfg(feature = "native")]
492    fallback_native_cx: std::sync::OnceLock<NativeCx>,
493    // bd-bjm5d: set only on the root Cx of a dedicated engine OS thread
494    // that owns its Connection exclusively and is not a shared scheduler
495    // worker. Grants VFS backends permission to issue bounded EINTR-safe
496    // positional I/O inline instead of hopping to the blocking pool.
497    // Deliberately NOT feature-gated: this is an OS-thread-ownership
498    // property, not an asupersync property.
499    blocking_io_inline_safe: AtomicBool,
500    // Deterministic clock override: milliseconds since epoch for tests.
501    //
502    // The mode bit is separate from the value so every `u64`, including zero
503    // and `u64::MAX`, remains a valid fixed timestamp. Writers publish the
504    // value before the mode bit with release ordering; readers acquire the
505    // mode before loading the value.
506    unix_millis: AtomicU64,
507    unix_millis_is_fixed: AtomicBool,
508}
509
510impl CxInner {
511    fn new(cancel_dispatch_gate: Arc<Mutex<()>>) -> Self {
512        Self {
513            cancel_requested: AtomicBool::new(false),
514            #[cfg(feature = "native")]
515            native_cancel_reason: AtomicU8::new(0),
516            cancel_state: Mutex::new(CancelState::Created),
517            cancel_reason: Mutex::new(None),
518            mask_depth: AtomicU32::new(0),
519            cancel_dispatch_gate,
520            local_cancel_waiters: Mutex::new(LocalCancelWaiters::default()),
521            children: Mutex::new(Vec::new()),
522            last_checkpoint_msg: Mutex::new(None),
523            last_eprocess_decision: Mutex::new(None),
524            eprocess_oracle: std::sync::OnceLock::new(),
525            #[cfg(feature = "native")]
526            attached_native_cx: Mutex::new(None),
527            #[cfg(feature = "native")]
528            fallback_native_cx: std::sync::OnceLock::new(),
529            blocking_io_inline_safe: AtomicBool::new(false),
530            unix_millis: AtomicU64::new(0),
531            unix_millis_is_fixed: AtomicBool::new(false),
532        }
533    }
534}
535
536#[cfg(feature = "native")]
537#[must_use]
538fn local_reason_to_native(reason: CancelReason) -> NativeCancelReason {
539    match reason {
540        CancelReason::Timeout => NativeCancelReason::timeout(),
541        CancelReason::UserInterrupt => NativeCancelReason::user("sqlite interrupt"),
542        CancelReason::RegionClose => NativeCancelReason::parent_cancelled(),
543        CancelReason::Abort => NativeCancelReason::resource_unavailable(),
544    }
545}
546
547#[cfg(feature = "native")]
548#[must_use]
549fn native_reason_to_local(reason: &NativeCancelReason) -> CancelReason {
550    match reason.kind {
551        NativeCancelKind::User => CancelReason::UserInterrupt,
552        NativeCancelKind::Timeout
553        | NativeCancelKind::Deadline
554        | NativeCancelKind::PollQuota
555        | NativeCancelKind::CostBudget => CancelReason::Timeout,
556        NativeCancelKind::FailFast
557        | NativeCancelKind::RaceLost
558        | NativeCancelKind::ParentCancelled
559        | NativeCancelKind::Shutdown
560        | NativeCancelKind::LinkedExit => CancelReason::RegionClose,
561        NativeCancelKind::ResourceUnavailable => CancelReason::Abort,
562    }
563}
564
565#[cfg(feature = "native")]
566const fn encode_native_cancel_reason(reason: CancelReason) -> u8 {
567    match reason {
568        CancelReason::Timeout => 1,
569        CancelReason::UserInterrupt => 2,
570        CancelReason::RegionClose => 3,
571        CancelReason::Abort => 4,
572    }
573}
574
575#[cfg(feature = "native")]
576fn decode_native_cancel_reason(encoded: u8) -> Option<CancelReason> {
577    match encoded {
578        0 => None,
579        1 => Some(CancelReason::Timeout),
580        2 => Some(CancelReason::UserInterrupt),
581        3 => Some(CancelReason::RegionClose),
582        4 => Some(CancelReason::Abort),
583        _ => unreachable!("invalid native cancellation reason rank"),
584    }
585}
586
587#[cfg(feature = "native")]
588fn record_native_cancel_reason(inner: &CxInner, reason: CancelReason) {
589    inner
590        .native_cancel_reason
591        .fetch_max(encode_native_cancel_reason(reason), Ordering::AcqRel);
592}
593
594#[cfg(feature = "native")]
595fn mirrored_native_cancel_reason(inner: &CxInner) -> Option<CancelReason> {
596    decode_native_cancel_reason(inner.native_cancel_reason.load(Ordering::Acquire))
597}
598
599#[cfg(feature = "native")]
600fn sync_one_native_cx_cancel(inner: &CxInner, native: &NativeCx) {
601    let mut encoded = inner.native_cancel_reason.load(Ordering::Acquire);
602    while let Some(reason) = decode_native_cancel_reason(encoded) {
603        // NativeCx drops its own lock before invoking arbitrary wakers. Hold
604        // no FrankenSQLite lock across this callback boundary.
605        native.set_cancel_reason(local_reason_to_native(reason));
606        let latest = inner.native_cancel_reason.load(Ordering::Acquire);
607        if latest == encoded {
608            break;
609        }
610        encoded = latest;
611    }
612}
613
614#[cfg(feature = "native")]
615#[must_use]
616#[allow(dead_code)]
617fn native_budget_from_local_at(budget: Budget, now: NativeTime) -> NativeBudget {
618    let mut native_budget = NativeBudget::new()
619        .with_poll_quota(budget.poll_quota)
620        .with_priority(budget.priority);
621    if let Some(cost_quota) = budget.cost_quota {
622        native_budget = native_budget.with_cost_quota(cost_quota);
623    }
624    if let Some(timeout) = budget.deadline {
625        native_budget = native_budget.with_timeout(now, timeout);
626    }
627    native_budget
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
631enum NativeCancelPropagation {
632    LocalAndNative,
633    LocalOnly,
634}
635
636#[must_use]
637fn local_cancellation_matches(inner: &CxInner, respect_mask: bool) -> bool {
638    inner.cancel_requested.load(Ordering::Acquire)
639        && (!respect_mask || inner.mask_depth.load(Ordering::Acquire) == 0)
640}
641
642fn take_local_cancel_waiters(inner: &CxInner) -> Vec<LocalCancelWaiter> {
643    let mut waiters = inner
644        .local_cancel_waiters
645        .lock()
646        .unwrap_or_else(std::sync::PoisonError::into_inner);
647    std::mem::take(&mut waiters.entries)
648}
649
650fn capture_cancel_callback_panic(
651    first_panic: &mut Option<Box<dyn std::any::Any + Send>>,
652    callback: impl FnOnce(),
653) {
654    if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback)) {
655        if first_panic.is_none() {
656            *first_panic = Some(payload);
657        } else {
658            // A panic payload may itself panic when dropped. The first panic
659            // is the one we report; leaking a secondary payload is preferable
660            // to aborting before the remaining cancellation observers run.
661            std::mem::forget(payload);
662        }
663    }
664}
665
666fn dispatch_local_cancel_waiters(
667    waiters: Vec<LocalCancelWaiter>,
668    first_panic: &mut Option<Box<dyn std::any::Any + Send>>,
669) {
670    // A Waker callback or final destructor may run arbitrary executor code.
671    // Both therefore happen only after the FrankenSQLite mutex is released.
672    // One panicking callback must not prevent the remaining observers from
673    // being notified.
674    for waiter in waiters {
675        capture_cancel_callback_panic(first_panic, || waiter.waker.wake_by_ref());
676        capture_cancel_callback_panic(first_panic, || drop(waiter));
677    }
678}
679
680fn publish_cancel_state(
681    inner: &CxInner,
682    local_reason: CancelReason,
683    native_reason: Option<CancelReason>,
684) -> CancelReason {
685    #[cfg(not(feature = "native"))]
686    let _ = native_reason;
687
688    // Monotone reason update.
689    let effective_local_reason = {
690        let mut r = inner
691            .cancel_reason
692            .lock()
693            .unwrap_or_else(std::sync::PoisonError::into_inner);
694        match *r {
695            Some(existing) if existing >= local_reason => existing,
696            _ => {
697                *r = Some(local_reason);
698                local_reason
699            }
700        }
701    };
702
703    // State transition: Created/Running → CancelRequested.
704    {
705        let mut state = inner
706            .cancel_state
707            .lock()
708            .unwrap_or_else(std::sync::PoisonError::into_inner);
709        if matches!(*state, CancelState::Created | CancelState::Running) {
710            *state = CancelState::CancelRequested;
711        }
712    }
713
714    // Record only cancellation requests that are allowed to cross the native
715    // boundary. This reason is deliberately separate from the aggregate local
716    // reason: a stronger operation-local cancellation must never be laundered
717    // into a shared native context by a later, weaker ordinary cancellation.
718    #[cfg(feature = "native")]
719    if let Some(reason) = native_reason {
720        record_native_cancel_reason(inner, reason);
721    }
722
723    // Publish the fast-path flag only after the reason and state are visible.
724    // This gives attachment and child-registration rechecks a stable reason to
725    // consume once they observe cancellation.
726    inner.cancel_requested.store(true, Ordering::Release);
727
728    effective_local_reason
729}
730
731fn try_append_live_children(
732    inner: &CxInner,
733    descendants: &mut Vec<Arc<CxInner>>,
734) -> std::result::Result<(), std::collections::TryReserveError> {
735    let mut children = inner
736        .children
737        .lock()
738        .unwrap_or_else(std::sync::PoisonError::into_inner);
739    children.retain(|child| child.strong_count() > 0);
740    descendants.try_reserve(children.len())?;
741    for child in children.iter().filter_map(Weak::upgrade) {
742        descendants.push(child);
743    }
744    Ok(())
745}
746
747#[must_use]
748fn local_cancel_waiter_count(inner: &CxInner) -> usize {
749    inner
750        .local_cancel_waiters
751        .lock()
752        .unwrap_or_else(std::sync::PoisonError::into_inner)
753        .entries
754        .len()
755}
756
757fn drain_local_cancel_waiters_into(inner: &CxInner, waiters: &mut Vec<LocalCancelWaiter>) {
758    let mut registered_waiters = inner
759        .local_cancel_waiters
760        .lock()
761        .unwrap_or_else(std::sync::PoisonError::into_inner);
762    debug_assert!(
763        waiters.capacity() - waiters.len() >= registered_waiters.entries.len(),
764        "cancellation waiter capacity must be reserved before publication"
765    );
766    waiters.append(&mut registered_waiters.entries);
767}
768
769#[cfg(feature = "native")]
770struct NativeCancelTarget {
771    descendant_index: Option<usize>,
772    native_cx: NativeCx,
773}
774
775#[cfg(feature = "native")]
776fn append_native_cancel_targets(
777    inner: &CxInner,
778    descendant_index: Option<usize>,
779    targets: &mut Vec<NativeCancelTarget>,
780) {
781    let attached_native = inner
782        .attached_native_cx
783        .lock()
784        .unwrap_or_else(std::sync::PoisonError::into_inner)
785        .as_ref()
786        .cloned();
787    if let Some(native_cx) = attached_native {
788        targets.push(NativeCancelTarget {
789            descendant_index,
790            native_cx,
791        });
792    }
793
794    if let Some(native_cx) = inner.fallback_native_cx.get().cloned() {
795        targets.push(NativeCancelTarget {
796            descendant_index,
797            native_cx,
798        });
799    }
800}
801
802/// Propagate cancellation to a `CxInner` node and all its descendants.
803///
804/// The explicit worklist avoids consuming one physical stack frame per
805/// descendant. Each node lock is released before its children are visited.
806fn propagate_cancel_tree(
807    inner: &CxInner,
808    reason: CancelReason,
809    native_propagation: NativeCancelPropagation,
810) {
811    let mut descendants = Vec::new();
812    let mut waiters = Vec::new();
813    #[cfg(feature = "native")]
814    let mut native_targets = Vec::new();
815    let mut reserve_error = None;
816    {
817        // Every context in one parent/child family shares this phase gate.
818        // Only state publication and waiter extraction happen while it is
819        // held; arbitrary native and executor callbacks run after unlock.
820        let _dispatch = inner
821            .cancel_dispatch_gate
822            .lock()
823            .unwrap_or_else(std::sync::PoisonError::into_inner);
824        let native_reason = match native_propagation {
825            NativeCancelPropagation::LocalAndNative => Some(reason),
826            NativeCancelPropagation::LocalOnly => None,
827        };
828
829        if let Err(error) = try_append_live_children(inner, &mut descendants) {
830            reserve_error = Some(error);
831        }
832        let mut cursor = 0;
833        while reserve_error.is_none() && cursor < descendants.len() {
834            let node = Arc::clone(&descendants[cursor]);
835            if let Err(error) = try_append_live_children(&node, &mut descendants) {
836                reserve_error = Some(error);
837                break;
838            }
839            cursor += 1;
840        }
841
842        if reserve_error.is_none() {
843            // Pending registration takes the same family gate. Concurrent
844            // unregistration can only make this count smaller, so one
845            // aggregate reservation covers every later append.
846            let waiter_count = descendants
847                .iter()
848                .fold(local_cancel_waiter_count(inner), |count, node| {
849                    count.saturating_add(local_cancel_waiter_count(node))
850                });
851            if let Err(error) = waiters.try_reserve(waiter_count) {
852                reserve_error = Some(error);
853            }
854        }
855
856        #[cfg(feature = "native")]
857        if reserve_error.is_none() && native_propagation == NativeCancelPropagation::LocalAndNative
858        {
859            let target_capacity = descendants.len().saturating_mul(2).saturating_add(2);
860            if let Err(error) = native_targets.try_reserve(target_capacity) {
861                reserve_error = Some(error);
862            } else {
863                append_native_cancel_targets(inner, None, &mut native_targets);
864                for (index, node) in descendants.iter().enumerate() {
865                    append_native_cancel_targets(node, Some(index), &mut native_targets);
866                }
867            }
868        }
869
870        if reserve_error.is_none() {
871            let propagated_local_reason = publish_cancel_state(inner, reason, native_reason);
872            drain_local_cancel_waiters_into(inner, &mut waiters);
873
874            #[cfg(feature = "native")]
875            let propagated_native_reason = match native_propagation {
876                NativeCancelPropagation::LocalAndNative => mirrored_native_cancel_reason(inner),
877                NativeCancelPropagation::LocalOnly => None,
878            };
879            #[cfg(not(feature = "native"))]
880            let propagated_native_reason = None;
881
882            for node in &descendants {
883                publish_cancel_state(node, propagated_local_reason, propagated_native_reason);
884                drain_local_cancel_waiters_into(node, &mut waiters);
885            }
886        }
887    }
888
889    // Every node visible when this phase began has its cancellation flag
890    // published before this batch invokes native or executor callbacks.
891    // Reasons are monotone per node; concurrent stronger batches may advance
892    // them further after this phase releases the family gate.
893    if let Some(error) = reserve_error {
894        panic!("failed to reserve cancellation propagation storage: {error}");
895    }
896
897    let mut first_panic = None;
898    #[cfg(feature = "native")]
899    for target in &native_targets {
900        let target_inner = target
901            .descendant_index
902            .map_or(inner, |index| &descendants[index]);
903        capture_cancel_callback_panic(&mut first_panic, || {
904            sync_one_native_cx_cancel(target_inner, &target.native_cx);
905        });
906    }
907    dispatch_local_cancel_waiters(waiters, &mut first_panic);
908    if let Some(payload) = first_panic {
909        std::panic::resume_unwind(payload);
910    }
911}
912
913fn propagate_cancel(inner: &CxInner, reason: CancelReason) {
914    propagate_cancel_tree(inner, reason, NativeCancelPropagation::LocalAndNative);
915}
916
917fn propagate_local_cancel(inner: &CxInner, reason: CancelReason) {
918    propagate_cancel_tree(inner, reason, NativeCancelPropagation::LocalOnly);
919}
920
921/// Cancellation-only authority for one derived [`Cx`] subtree.
922///
923/// This relay deliberately carries only a weak reference to the local
924/// FrankenSQLite cancellation state. It cannot expose the target context,
925/// runtime effects, I/O authority, budgets, or native asupersync context.
926/// Local cancellation reaches the target and its descendants without
927/// cancelling an attached native context that may be shared with the worker
928/// root. Calling the relay performs synchronous local bookkeeping and subtree
929/// traversal; actor integrations must not invoke it from a nonblocking `Drop`.
930#[derive(Debug)]
931#[must_use = "dropping the relay discards the authority to cancel the derived operation"]
932pub struct LocalCancelRelay {
933    inner: Weak<CxInner>,
934}
935
936impl LocalCancelRelay {
937    /// Request cancellation of the derived local context subtree.
938    ///
939    /// The strongest [`CancelReason`] remains monotone. Returns `false` when
940    /// the target context has already been dropped; otherwise returns `true`,
941    /// including for an idempotent repeated request.
942    #[must_use]
943    pub fn cancel_local(&self, reason: CancelReason) -> bool {
944        let Some(inner) = self.inner.upgrade() else {
945            return false;
946        };
947        propagate_local_cancel(&inner, reason);
948        true
949    }
950}
951
952/// Scoped notification of local cancellation for one [`Cx`].
953///
954/// This future watches only FrankenSQLite's local cancellation state. It does
955/// not inspect or modify a native runtime context, carry effect capabilities,
956/// or create a child context. The constructor determines whether masking
957/// defers readiness or whether the raw request itself is sufficient.
958///
959/// A pending registration is removed on `Drop`, so repeatedly creating and
960/// abandoning these futures does not accumulate stale child or waker entries.
961#[derive(Debug)]
962#[must_use = "futures do nothing unless polled or awaited"]
963pub struct LocalCancellation<'a> {
964    inner: &'a CxInner,
965    waiter_id: Option<u64>,
966    respect_mask: bool,
967}
968
969impl LocalCancellation<'_> {
970    fn unregister(&mut self) {
971        let Some(id) = self.waiter_id.take() else {
972            return;
973        };
974        let retired = {
975            let mut waiters = self
976                .inner
977                .local_cancel_waiters
978                .lock()
979                .unwrap_or_else(std::sync::PoisonError::into_inner);
980            waiters.remove(id)
981        };
982        // Releasing the final Waker reference may invoke executor code.
983        drop(retired);
984    }
985}
986
987impl Future for LocalCancellation<'_> {
988    type Output = ();
989
990    fn poll(mut self: Pin<&mut Self>, task_cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
991        let mut prepared_waker = None;
992        let this = self.as_mut().get_mut();
993        let inner = this.inner;
994
995        loop {
996            if local_cancellation_matches(inner, this.respect_mask) {
997                this.unregister();
998                drop(prepared_waker);
999                return Poll::Ready(());
1000            }
1001
1002            // Registration participates in the same family phase gate as
1003            // tree publication. This lets cancellation pre-reserve one flat
1004            // waiter buffer for the complete tree before it changes any
1005            // node. Waker cloning and destruction still happen after both
1006            // internal locks are released.
1007            let dispatch = inner
1008                .cancel_dispatch_gate
1009                .lock()
1010                .unwrap_or_else(std::sync::PoisonError::into_inner);
1011            let mut waiters = inner
1012                .local_cancel_waiters
1013                .lock()
1014                .unwrap_or_else(std::sync::PoisonError::into_inner);
1015            if local_cancellation_matches(inner, this.respect_mask) {
1016                let retired = this.waiter_id.take().and_then(|id| waiters.remove(id));
1017                drop(waiters);
1018                drop(dispatch);
1019                drop(retired);
1020                drop(prepared_waker);
1021                return Poll::Ready(());
1022            }
1023
1024            let existing_position = this.waiter_id.and_then(|id| {
1025                waiters
1026                    .entries
1027                    .iter()
1028                    .position(|registered| registered.id == id)
1029            });
1030            if existing_position
1031                .is_some_and(|position| waiters.entries[position].waker.will_wake(task_cx.waker()))
1032            {
1033                drop(waiters);
1034                drop(dispatch);
1035                drop(prepared_waker);
1036                return Poll::Pending;
1037            }
1038
1039            if existing_position.is_none()
1040                && let Err(error) = waiters.entries.try_reserve(1)
1041            {
1042                drop(waiters);
1043                drop(dispatch);
1044                drop(prepared_waker);
1045                panic!("failed to reserve local-cancellation waiter storage: {error}");
1046            }
1047
1048            let Some(new_waker) = prepared_waker.take() else {
1049                // RawWaker::clone may execute arbitrary user code, so prepare
1050                // the replacement only after releasing the internal mutex.
1051                drop(waiters);
1052                drop(dispatch);
1053                prepared_waker = Some(task_cx.waker().clone());
1054                continue;
1055            };
1056
1057            let retired = if let Some(position) = existing_position {
1058                Some(std::mem::replace(
1059                    &mut waiters.entries[position].waker,
1060                    new_waker,
1061                ))
1062            } else {
1063                let id = waiters.allocate_id();
1064                waiters.entries.push(LocalCancelWaiter {
1065                    id,
1066                    waker: new_waker,
1067                });
1068                this.waiter_id = Some(id);
1069                None
1070            };
1071            drop(waiters);
1072            drop(dispatch);
1073            // Waker destruction is deliberately deferred until after every
1074            // internal lock is released.
1075            drop(retired);
1076            return Poll::Pending;
1077        }
1078    }
1079}
1080
1081impl Drop for LocalCancellation<'_> {
1082    fn drop(&mut self) {
1083        self.unregister();
1084    }
1085}
1086
1087/// Capability context passed through all effectful operations.
1088///
1089/// Carries tracing identifiers (`trace_id`, `decision_id`, `policy_id`) that
1090/// propagate through all context derivations (clone, restrict, scope, child).
1091/// A value of `0` means "unset / not assigned".
1092#[derive(Debug)]
1093pub struct Cx<Caps: cap::SubsetOf<cap::All> = FullCaps> {
1094    inner: Arc<CxInner>,
1095    budget: Budget,
1096    trace_id: u64,
1097    decision_id: u64,
1098    policy_id: u64,
1099    // fn() -> Caps ensures Send+Sync regardless of Caps marker type.
1100    _caps: PhantomData<fn() -> Caps>,
1101}
1102
1103impl<Caps: cap::SubsetOf<cap::All>> Clone for Cx<Caps> {
1104    fn clone(&self) -> Self {
1105        Self {
1106            inner: Arc::clone(&self.inner),
1107            budget: self.budget,
1108            trace_id: self.trace_id,
1109            decision_id: self.decision_id,
1110            policy_id: self.policy_id,
1111            _caps: PhantomData,
1112        }
1113    }
1114}
1115
1116impl Default for Cx<FullCaps> {
1117    fn default() -> Self {
1118        Self::new()
1119    }
1120}
1121
1122impl Cx<FullCaps> {
1123    #[must_use]
1124    pub fn new() -> Self {
1125        Self::with_budget(Budget::INFINITE)
1126    }
1127}
1128
1129impl<Caps: cap::SubsetOf<cap::All>> Cx<Caps> {
1130    #[cfg(all(feature = "native", test))]
1131    #[must_use]
1132    #[allow(dead_code)]
1133    fn effective_native_cx(&self) -> NativeCx {
1134        let native = {
1135            let _dispatch = self
1136                .inner
1137                .cancel_dispatch_gate
1138                .lock()
1139                .unwrap_or_else(std::sync::PoisonError::into_inner);
1140            let attached_native = self
1141                .inner
1142                .attached_native_cx
1143                .lock()
1144                .unwrap_or_else(std::sync::PoisonError::into_inner)
1145                .as_ref()
1146                .cloned();
1147            attached_native.unwrap_or_else(|| {
1148                self.inner
1149                    .fallback_native_cx
1150                    .get_or_init(|| {
1151                        NativeCx::for_request_with_budget(native_budget_from_local_at(
1152                            self.budget,
1153                            asupersync::time::wall_now(),
1154                        ))
1155                    })
1156                    .clone()
1157            })
1158        };
1159        // Recheck after releasing the family publication gate: NativeCx
1160        // dispatches arbitrary wakers synchronously, so no FrankenSQLite lock
1161        // may be held while mirroring the reason.
1162        sync_one_native_cx_cancel(&self.inner, &native);
1163        native
1164    }
1165
1166    #[cfg(feature = "native")]
1167    #[must_use]
1168    fn native_cx_for_checkpoint(&self) -> Option<NativeCx> {
1169        let attached_native = self
1170            .inner
1171            .attached_native_cx
1172            .lock()
1173            .unwrap_or_else(std::sync::PoisonError::into_inner)
1174            .as_ref()
1175            .cloned();
1176        attached_native.or_else(|| self.inner.fallback_native_cx.get().cloned())
1177    }
1178
1179    #[must_use]
1180    pub fn with_budget(budget: Budget) -> Self {
1181        Self::with_budget_and_cancel_dispatch(budget, Arc::new(Mutex::new(())))
1182    }
1183
1184    fn with_budget_and_cancel_dispatch(
1185        budget: Budget,
1186        cancel_dispatch_gate: Arc<Mutex<()>>,
1187    ) -> Self {
1188        Self {
1189            inner: Arc::new(CxInner::new(cancel_dispatch_gate)),
1190            budget,
1191            trace_id: 0,
1192            decision_id: 0,
1193            policy_id: 0,
1194            _caps: PhantomData,
1195        }
1196    }
1197
1198    #[must_use]
1199    pub fn budget(&self) -> Budget {
1200        self.budget
1201    }
1202
1203    // -----------------------------------------------------------------------
1204    // Tracing IDs (§4 Cx capability context threading)
1205    // -----------------------------------------------------------------------
1206
1207    /// The trace ID for this context (0 = unset).
1208    #[must_use]
1209    pub fn trace_id(&self) -> u64 {
1210        self.trace_id
1211    }
1212
1213    /// The decision ID for this context (0 = unset).
1214    #[must_use]
1215    pub fn decision_id(&self) -> u64 {
1216        self.decision_id
1217    }
1218
1219    /// The policy ID for this context (0 = unset).
1220    #[must_use]
1221    pub fn policy_id(&self) -> u64 {
1222        self.policy_id
1223    }
1224
1225    /// Set all three tracing identifiers at once.
1226    ///
1227    /// Typically called once when a connection or request is initialized.
1228    #[must_use]
1229    pub fn with_trace_context(mut self, trace_id: u64, decision_id: u64, policy_id: u64) -> Self {
1230        self.trace_id = trace_id;
1231        self.decision_id = decision_id;
1232        self.policy_id = policy_id;
1233        self
1234    }
1235
1236    /// Return a new context with only the `decision_id` changed.
1237    ///
1238    /// Used when starting a new operation within the same trace.
1239    #[must_use]
1240    pub fn with_decision_id(mut self, decision_id: u64) -> Self {
1241        self.decision_id = decision_id;
1242        self
1243    }
1244
1245    /// Return a new context with only the `policy_id` changed.
1246    #[must_use]
1247    pub fn with_policy_id(mut self, policy_id: u64) -> Self {
1248        self.policy_id = policy_id;
1249        self
1250    }
1251
1252    /// Returns a view of this context with a tighter effective budget.
1253    ///
1254    /// The effective budget is computed as `self.budget.meet(child)`, so the
1255    /// child cannot loosen its parent's constraints.
1256    /// Tracing IDs propagate unchanged.
1257    #[must_use]
1258    pub fn scope_with_budget(&self, child: Budget) -> Self {
1259        Self {
1260            inner: Arc::clone(&self.inner),
1261            budget: self.budget.meet(child),
1262            trace_id: self.trace_id,
1263            decision_id: self.decision_id,
1264            policy_id: self.policy_id,
1265            _caps: PhantomData,
1266        }
1267    }
1268
1269    /// Returns a cleanup scope that uses [`Budget::MINIMAL`].
1270    #[must_use]
1271    pub fn cleanup_scope(&self) -> Self {
1272        self.scope_with_budget(Budget::MINIMAL)
1273    }
1274
1275    /// Re-type this context to a narrower capability set.
1276    ///
1277    /// This is zero-cost at runtime and shares cancellation state.
1278    #[must_use]
1279    pub fn restrict<NewCaps>(&self) -> Cx<NewCaps>
1280    where
1281        NewCaps: cap::SubsetOf<cap::All> + cap::SubsetOf<Caps>,
1282    {
1283        self.retype()
1284    }
1285
1286    /// Internal re-typing helper without subset enforcement.
1287    #[must_use]
1288    fn retype<NewCaps>(&self) -> Cx<NewCaps>
1289    where
1290        NewCaps: cap::SubsetOf<cap::All>,
1291    {
1292        Cx {
1293            inner: Arc::clone(&self.inner),
1294            budget: self.budget,
1295            trace_id: self.trace_id,
1296            decision_id: self.decision_id,
1297            policy_id: self.policy_id,
1298            _caps: PhantomData,
1299        }
1300    }
1301
1302    // -----------------------------------------------------------------------
1303    // Cancellation state machine (§4.12)
1304    // -----------------------------------------------------------------------
1305
1306    #[must_use]
1307    pub fn is_cancel_requested(&self) -> bool {
1308        self.inner.cancel_requested.load(Ordering::Acquire)
1309    }
1310
1311    /// Wait until local cancellation is observable at an unmasked boundary.
1312    ///
1313    /// The returned future borrows this context's existing cancellation node;
1314    /// it does not allocate a child context or attach a native runtime context.
1315    /// Dropping it removes any pending waker registration.
1316    pub fn wait_for_local_cancellation(&self) -> LocalCancellation<'_> {
1317        LocalCancellation {
1318            inner: &self.inner,
1319            waiter_id: None,
1320            respect_mask: true,
1321        }
1322    }
1323
1324    /// Wait until local cancellation has been requested, even while masked.
1325    ///
1326    /// This is a raw wakeup primitive for cancellation-sensitive admission
1327    /// machinery that separately defines whether masked operation is legal.
1328    /// It does not transition cancellation state or inspect a native context.
1329    pub fn wait_for_local_cancel_request(&self) -> LocalCancellation<'_> {
1330        LocalCancellation {
1331            inner: &self.inner,
1332            waiter_id: None,
1333            respect_mask: false,
1334        }
1335    }
1336
1337    /// Request cancellation with the default reason (`UserInterrupt`).
1338    ///
1339    /// Propagates to all child contexts per INV-CANCEL-PROPAGATES.
1340    pub fn cancel(&self) {
1341        self.cancel_with_reason(CancelReason::UserInterrupt);
1342    }
1343
1344    /// Request cancellation with an explicit reason.
1345    ///
1346    /// INV-CANCEL-IDEMPOTENT: the strongest reason wins; weaker reasons are
1347    /// ignored once a stronger one has been set.
1348    ///
1349    /// INV-CANCEL-PROPAGATES: cancellation propagates to all descendants.
1350    pub fn cancel_with_reason(&self, reason: CancelReason) {
1351        propagate_cancel(&self.inner, reason);
1352    }
1353
1354    /// Current state in the cancellation lifecycle.
1355    #[must_use]
1356    pub fn cancel_state(&self) -> CancelState {
1357        *self
1358            .inner
1359            .cancel_state
1360            .lock()
1361            .unwrap_or_else(std::sync::PoisonError::into_inner)
1362    }
1363
1364    /// The strongest cancellation reason set so far, if any.
1365    #[must_use]
1366    pub fn cancel_reason(&self) -> Option<CancelReason> {
1367        *self
1368            .inner
1369            .cancel_reason
1370            .lock()
1371            .unwrap_or_else(std::sync::PoisonError::into_inner)
1372    }
1373
1374    /// Transition from `Created` to `Running`.
1375    pub fn transition_to_running(&self) {
1376        let mut state = self
1377            .inner
1378            .cancel_state
1379            .lock()
1380            .unwrap_or_else(std::sync::PoisonError::into_inner);
1381        if *state == CancelState::Created {
1382            *state = CancelState::Running;
1383        }
1384    }
1385
1386    /// Transition from `Cancelling` to `Finalizing`.
1387    pub fn transition_to_finalizing(&self) {
1388        let mut state = self
1389            .inner
1390            .cancel_state
1391            .lock()
1392            .unwrap_or_else(std::sync::PoisonError::into_inner);
1393        if *state == CancelState::Cancelling {
1394            *state = CancelState::Finalizing;
1395        }
1396    }
1397
1398    /// Transition to `Completed` (from `Finalizing` or `Running`).
1399    pub fn transition_to_completed(&self) {
1400        let mut state = self
1401            .inner
1402            .cancel_state
1403            .lock()
1404            .unwrap_or_else(std::sync::PoisonError::into_inner);
1405        if matches!(*state, CancelState::Finalizing | CancelState::Running) {
1406            *state = CancelState::Completed;
1407        }
1408    }
1409
1410    /// Attach an e-process oracle used by [`Self::checkpoint`].
1411    pub fn set_eprocess_oracle(&self, oracle: Arc<EProcessOracle>) {
1412        let _ = self.inner.eprocess_oracle.set(oracle);
1413    }
1414
1415    /// Remove the currently attached e-process oracle.
1416    pub fn clear_eprocess_oracle(&self) {
1417        // OnceLock cannot be easily cleared. We just leave it as is.
1418        // It's only called in unused methods anyway.
1419    }
1420
1421    /// Attach a native asupersync context used by [`Self::checkpoint`].
1422    #[cfg(feature = "native")]
1423    pub fn set_native_cx(&self, native_cx: NativeCx) {
1424        let retired = {
1425            let _dispatch = self
1426                .inner
1427                .cancel_dispatch_gate
1428                .lock()
1429                .unwrap_or_else(std::sync::PoisonError::into_inner);
1430            let mut attached = self
1431                .inner
1432                .attached_native_cx
1433                .lock()
1434                .unwrap_or_else(std::sync::PoisonError::into_inner);
1435            attached.replace(native_cx.clone())
1436        };
1437        // Synchronize the exact handle supplied by this call. Re-reading the
1438        // attachment here would let a racing replacement return with this
1439        // handle still uncancelled.
1440        sync_one_native_cx_cancel(&self.inner, &native_cx);
1441        // Releasing the final old NativeCx reference may retire cancellation
1442        // Wakers. Do that only after both FrankenSQLite locks are gone and the
1443        // replacement's synchronization postcondition has been established.
1444        drop(retired);
1445    }
1446
1447    /// Attach a native context shim in non-native builds.
1448    #[cfg(not(feature = "native"))]
1449    pub fn set_native_cx<T>(&self, _native_cx: T) {}
1450
1451    /// Return the attached native asupersync context, if one exists.
1452    #[cfg(feature = "native")]
1453    #[must_use]
1454    pub fn attached_native_cx(&self) -> Option<NativeCx> {
1455        self.inner
1456            .attached_native_cx
1457            .lock()
1458            .unwrap_or_else(std::sync::PoisonError::into_inner)
1459            .clone()
1460    }
1461
1462    /// Compute the native runtime budget for a task spawned on `native_cx`.
1463    ///
1464    /// FrankenSQLite deadlines are relative timeouts. They must be translated
1465    /// using the selected native context's clock so production monotonic time
1466    /// and deterministic lab time stay in the same domain. Meeting the result
1467    /// with the native parent's budget preserves every tighter parent bound,
1468    /// including the maximum scheduling priority.
1469    #[cfg(feature = "native")]
1470    #[must_use]
1471    pub fn native_spawn_budget(&self, native_cx: &NativeCx) -> NativeBudget {
1472        let local = native_budget_from_local_at(self.budget, native_cx.now_for_observability());
1473        native_cx.budget().meet(local)
1474    }
1475
1476    /// Return the attached native context shim, if one exists.
1477    #[cfg(not(feature = "native"))]
1478    #[must_use]
1479    pub fn attached_native_cx(&self) -> Option<NativeCx> {
1480        None
1481    }
1482
1483    /// Remove the currently attached native asupersync context.
1484    #[cfg(feature = "native")]
1485    pub fn clear_native_cx(&self) {
1486        let retired = {
1487            let _dispatch = self
1488                .inner
1489                .cancel_dispatch_gate
1490                .lock()
1491                .unwrap_or_else(std::sync::PoisonError::into_inner);
1492            self.inner
1493                .attached_native_cx
1494                .lock()
1495                .unwrap_or_else(std::sync::PoisonError::into_inner)
1496                .take()
1497        };
1498        // NativeCx destruction may invoke executor-owned cleanup.
1499        drop(retired);
1500    }
1501
1502    /// Mark this context as running on a dedicated engine OS thread where
1503    /// bounded blocking I/O may be issued inline (bd-bjm5d). Irreversible
1504    /// for the lifetime of this context; shared through [`Cx::clone`] and
1505    /// propagated to children by [`Cx::create_child`].
1506    pub fn mark_blocking_io_inline_safe(&self) {
1507        self.inner
1508            .blocking_io_inline_safe
1509            .store(true, Ordering::Release);
1510    }
1511
1512    /// Whether bounded, page-sized positional I/O may be issued inline on
1513    /// the calling thread instead of via the blocking pool (bd-bjm5d).
1514    #[must_use]
1515    pub fn blocking_io_inline_safe(&self) -> bool {
1516        self.inner.blocking_io_inline_safe.load(Ordering::Acquire)
1517    }
1518
1519    /// Remove the currently attached native context shim.
1520    #[cfg(not(feature = "native"))]
1521    pub fn clear_native_cx(&self) {}
1522
1523    #[must_use]
1524    fn maybe_cancel_via_eprocess(&self) -> bool {
1525        let Some(oracle) = self.inner.eprocess_oracle.get() else {
1526            return false;
1527        };
1528        let decision = oracle.decision(self.budget.priority);
1529        self.record_eprocess_decision(decision.clone());
1530        tracing::debug!(
1531            target: "fsqlite::cx",
1532            event = "eprocess_checkpoint",
1533            trace_id = self.trace_id,
1534            decision_id = self.decision_id,
1535            policy_id = self.policy_id,
1536            priority = decision.priority,
1537            evalue = decision.snapshot.evalue,
1538            threshold = decision.snapshot.rejection_threshold,
1539            observations = decision.snapshot.observations,
1540            priority_threshold = decision.snapshot.priority_threshold,
1541            should_shed = decision.should_shed,
1542            signal = ?decision.snapshot.last_signal
1543        );
1544        if decision.should_shed {
1545            tracing::info!(
1546                target: "fsqlite::cx",
1547                event = "eprocess_shedding_triggered",
1548                trace_id = self.trace_id,
1549                decision_id = self.decision_id,
1550                policy_id = self.policy_id,
1551                priority = decision.priority,
1552                evalue = decision.snapshot.evalue,
1553                threshold = decision.snapshot.rejection_threshold,
1554                signal = ?decision.snapshot.last_signal
1555            );
1556            self.cancel_with_reason(CancelReason::Abort);
1557            return true;
1558        }
1559        false
1560    }
1561
1562    #[cfg(feature = "native")]
1563    #[must_use]
1564    fn maybe_cancel_via_native_cx(&self, masked: bool) -> bool {
1565        let Some(native) = self.native_cx_for_checkpoint() else {
1566            return false;
1567        };
1568
1569        if masked {
1570            if native.is_cancel_requested() {
1571                let reason = native
1572                    .cancel_reason()
1573                    .as_ref()
1574                    .map_or(CancelReason::Timeout, native_reason_to_local);
1575                self.cancel_with_reason(reason);
1576                return true;
1577            }
1578            return false;
1579        }
1580
1581        if native.checkpoint().is_err() {
1582            let reason = native
1583                .cancel_reason()
1584                .as_ref()
1585                .map_or(CancelReason::Timeout, native_reason_to_local);
1586            self.cancel_with_reason(reason);
1587            return true;
1588        }
1589        false
1590    }
1591
1592    // -----------------------------------------------------------------------
1593    // Checkpoints (§4.12.1)
1594    // -----------------------------------------------------------------------
1595
1596    /// Check for cancellation at a yield point.
1597    ///
1598    /// Returns `Ok(())` when not cancelled **or when inside a masked section**.
1599    /// When cancellation is observed, transitions state from `CancelRequested`
1600    /// to `Cancelling`.
1601    ///
1602    /// Hot-path note: the cheap `cancel_requested` atomic load is consulted
1603    /// first, then `mask_depth`. Only if neither cheap signal proves we're
1604    /// clear do we consult the e-process oracle and the native asupersync
1605    /// `Cx::checkpoint()`. Previously `maybe_cancel_via_native_cx` was
1606    /// evaluated **unconditionally** before the fast-path test — every
1607    /// checkpoint paid for the nested asupersync cancel machinery even when
1608    /// the cheap atomic said "not cancelled". That showed up as 5.87%
1609    /// self-time on the 2026-04-23 post-bench-fix MT 8t capture
1610    /// (`fsqlite-bench-fix-validation-194151`).
1611    pub fn checkpoint(&self) -> Result<()> {
1612        let cancel_requested = self.inner.cancel_requested.load(Ordering::Acquire);
1613        if !cancel_requested {
1614            // Cheap path already proved we're not locally cancelled. Only
1615            // the oracle + native cx can still observe a cancel signal.
1616            if !self.maybe_cancel_via_eprocess() {
1617                #[cfg(feature = "native")]
1618                {
1619                    let masked = self.inner.mask_depth.load(Ordering::Acquire) > 0;
1620                    if !self.maybe_cancel_via_native_cx(masked) {
1621                        return Ok(());
1622                    }
1623                }
1624                #[cfg(not(feature = "native"))]
1625                {
1626                    return Ok(());
1627                }
1628            }
1629        }
1630
1631        // Either cancel_requested is set locally, or one of the async plane
1632        // checks fired. Masked sections defer observation unconditionally.
1633        let masked = self.inner.mask_depth.load(Ordering::Acquire) > 0;
1634        if masked {
1635            return Ok(());
1636        }
1637
1638        // Slow path: transition CancelRequested → Cancelling.
1639        {
1640            let mut state = self
1641                .inner
1642                .cancel_state
1643                .lock()
1644                .unwrap_or_else(std::sync::PoisonError::into_inner);
1645            if *state == CancelState::CancelRequested {
1646                *state = CancelState::Cancelling;
1647            }
1648        }
1649        Err(Error::cancelled())
1650    }
1651
1652    /// Check for cancellation and record a progress message.
1653    pub fn checkpoint_with(&self, msg: impl Into<String>) -> Result<()> {
1654        {
1655            let mut guard = self
1656                .inner
1657                .last_checkpoint_msg
1658                .lock()
1659                .unwrap_or_else(std::sync::PoisonError::into_inner);
1660            *guard = Some(msg.into());
1661        }
1662        self.checkpoint()
1663    }
1664
1665    #[must_use]
1666    pub fn last_checkpoint_message(&self) -> Option<String> {
1667        self.inner
1668            .last_checkpoint_msg
1669            .lock()
1670            .unwrap_or_else(std::sync::PoisonError::into_inner)
1671            .clone()
1672    }
1673
1674    /// Most recent e-process decision recorded during [`Self::checkpoint`].
1675    #[must_use]
1676    pub fn last_eprocess_decision(&self) -> Option<EProcessDecision> {
1677        self.inner
1678            .last_eprocess_decision
1679            .lock()
1680            .unwrap_or_else(std::sync::PoisonError::into_inner)
1681            .clone()
1682    }
1683
1684    /// Snapshot portion of the most recent e-process decision.
1685    #[must_use]
1686    pub fn last_eprocess_snapshot(&self) -> Option<EProcessSnapshot> {
1687        self.last_eprocess_decision()
1688            .map(|decision| decision.snapshot)
1689    }
1690
1691    fn record_eprocess_decision(&self, decision: EProcessDecision) {
1692        *self
1693            .inner
1694            .last_eprocess_decision
1695            .lock()
1696            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(decision);
1697    }
1698
1699    // -----------------------------------------------------------------------
1700    // Masked critical sections (§4.12.2)
1701    // -----------------------------------------------------------------------
1702
1703    /// Enter a masked section where `checkpoint()` returns `Ok(())` even if
1704    /// cancellation is requested.
1705    ///
1706    /// Returns a [`MaskGuard`] whose `Drop` restores the mask depth.
1707    ///
1708    /// # Panics
1709    ///
1710    /// Panics if nesting exceeds [`MAX_MASK_DEPTH`] (INV-MASK-BOUNDED).
1711    #[must_use]
1712    pub fn masked(&self) -> MaskGuard<'_> {
1713        let prev = self.inner.mask_depth.fetch_add(1, Ordering::AcqRel);
1714        if prev >= MAX_MASK_DEPTH {
1715            self.inner.mask_depth.fetch_sub(1, Ordering::Release);
1716            assert!(
1717                prev < MAX_MASK_DEPTH,
1718                "MAX_MASK_DEPTH ({MAX_MASK_DEPTH}) exceeded: mask nesting depth would be {}",
1719                prev + 1
1720            );
1721        }
1722        MaskGuard { inner: &self.inner }
1723    }
1724
1725    /// Current mask nesting depth.
1726    #[must_use]
1727    pub fn mask_depth(&self) -> u32 {
1728        self.inner.mask_depth.load(Ordering::Acquire)
1729    }
1730
1731    // -----------------------------------------------------------------------
1732    // Commit sections (§4.12.3)
1733    // -----------------------------------------------------------------------
1734
1735    /// Execute a logically atomic commit section.
1736    ///
1737    /// The section masks cancellation, enforces a poll quota bound, and
1738    /// guarantees the `finalizer` runs even on cancellation or panic.
1739    pub fn commit_section<R>(
1740        &self,
1741        poll_quota: u32,
1742        body: impl FnOnce(&CommitCtx) -> R,
1743        finalizer: impl FnOnce(),
1744    ) -> R {
1745        struct FinGuard<G: FnOnce()>(Option<G>);
1746        impl<G: FnOnce()> Drop for FinGuard<G> {
1747            fn drop(&mut self) {
1748                if let Some(f) = self.0.take() {
1749                    f();
1750                }
1751            }
1752        }
1753
1754        let _mask = self.masked();
1755        let _fin = FinGuard(Some(finalizer));
1756        let ctx = CommitCtx::new(poll_quota);
1757        body(&ctx)
1758    }
1759
1760    // -----------------------------------------------------------------------
1761    // Child context management (INV-CANCEL-PROPAGATES)
1762    // -----------------------------------------------------------------------
1763
1764    /// Create a child `Cx` that shares the parent's budget but has
1765    /// independent cancellation state. Cancelling the parent propagates
1766    /// to this child. Tracing IDs propagate to the child.
1767    #[must_use]
1768    pub fn create_child(&self) -> Self {
1769        self.create_child_with_runtime_affinity(true)
1770    }
1771
1772    /// Create a child context that may be moved into a newly spawned task.
1773    ///
1774    /// Cancellation lineage, budget, tracing metadata, e-process policy, and
1775    /// typed capabilities are inherited exactly as for [`Self::create_child`].
1776    /// Task- and OS-thread-affine state is deliberately excluded: the child
1777    /// starts without an attached/fallback native context and without inline
1778    /// blocking-I/O permission. The spawned task must attach its own native
1779    /// context after it starts.
1780    #[must_use]
1781    pub fn create_child_for_spawn(&self) -> Self {
1782        self.create_child_with_runtime_affinity(false)
1783    }
1784
1785    fn create_child_with_runtime_affinity(&self, inherit_runtime_affinity: bool) -> Self {
1786        let mut child = Self::with_budget_and_cancel_dispatch(
1787            self.budget,
1788            Arc::clone(&self.inner.cancel_dispatch_gate),
1789        );
1790        child.trace_id = self.trace_id;
1791        child.decision_id = self.decision_id;
1792        child.policy_id = self.policy_id;
1793        if self.inner.unix_millis_is_fixed.load(Ordering::Acquire) {
1794            let unix_millis = self.inner.unix_millis.load(Ordering::Acquire);
1795            child
1796                .inner
1797                .unix_millis
1798                .store(unix_millis, Ordering::Release);
1799            child
1800                .inner
1801                .unix_millis_is_fixed
1802                .store(true, Ordering::Release);
1803        }
1804        if let Some(oracle) = self.inner.eprocess_oracle.get().cloned() {
1805            child.set_eprocess_oracle(oracle);
1806        }
1807        // bd-bjm5d: inline-safety is a property of the owning OS thread,
1808        // not of the native runtime handle. Ordinary same-thread children
1809        // inherit it; spawn-safe children deliberately do not.
1810        if inherit_runtime_affinity && self.blocking_io_inline_safe() {
1811            child.mark_blocking_io_inline_safe();
1812        }
1813
1814        #[cfg(feature = "native")]
1815        let native_to_sync = {
1816            let _dispatch = self
1817                .inner
1818                .cancel_dispatch_gate
1819                .lock()
1820                .unwrap_or_else(std::sync::PoisonError::into_inner);
1821            let attached_native = inherit_runtime_affinity
1822                .then(|| {
1823                    self.inner
1824                        .attached_native_cx
1825                        .lock()
1826                        .unwrap_or_else(std::sync::PoisonError::into_inner)
1827                        .clone()
1828                })
1829                .flatten();
1830            if let Some(native) = attached_native.as_ref() {
1831                *child
1832                    .inner
1833                    .attached_native_cx
1834                    .lock()
1835                    .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(native.clone());
1836            }
1837
1838            let local_reason = self.cancel_reason();
1839            let native_reason = mirrored_native_cancel_reason(&self.inner);
1840            if let Some(local_reason) = local_reason.or(native_reason) {
1841                publish_cancel_state(&child.inner, local_reason, native_reason);
1842            }
1843
1844            // The child becomes visible only after its inherited cancellation
1845            // state is fully initialized.
1846            let mut children = self
1847                .inner
1848                .children
1849                .lock()
1850                .unwrap_or_else(std::sync::PoisonError::into_inner);
1851            if children.len() == children.capacity() {
1852                children.retain(|registered| registered.strong_count() > 0);
1853            }
1854            children.push(Arc::downgrade(&child.inner));
1855
1856            attached_native.filter(|_| native_reason.is_some())
1857        };
1858
1859        #[cfg(not(feature = "native"))]
1860        {
1861            let _dispatch = self
1862                .inner
1863                .cancel_dispatch_gate
1864                .lock()
1865                .unwrap_or_else(std::sync::PoisonError::into_inner);
1866            if let Some(reason) = self.cancel_reason() {
1867                publish_cancel_state(&child.inner, reason, None);
1868            }
1869            let mut children = self
1870                .inner
1871                .children
1872                .lock()
1873                .unwrap_or_else(std::sync::PoisonError::into_inner);
1874            if children.len() == children.capacity() {
1875                children.retain(|registered| registered.strong_count() > 0);
1876            }
1877            children.push(Arc::downgrade(&child.inner));
1878        }
1879
1880        #[cfg(feature = "native")]
1881        if let Some(native) = native_to_sync {
1882            sync_one_native_cx_cancel(&child.inner, &native);
1883        }
1884
1885        child
1886    }
1887
1888    /// Create a child context plus narrowly scoped local cancellation authority.
1889    ///
1890    /// The child inherits the same budget, tracing metadata, effect
1891    /// capabilities, and native I/O context as [`Self::create_child`]. The
1892    /// returned relay can only request local cancellation for that child
1893    /// subtree; it cannot access the context or cancel the inherited native
1894    /// context.
1895    pub fn create_child_with_local_cancel_relay(&self) -> (Self, LocalCancelRelay) {
1896        let child = self.create_child();
1897        let relay = LocalCancelRelay {
1898            inner: Arc::downgrade(&child.inner),
1899        };
1900        (child, relay)
1901    }
1902
1903    /// Set a deterministic unix time for tests.
1904    pub fn set_unix_millis_for_testing(&self, millis: u64)
1905    where
1906        Caps: cap::HasTime,
1907    {
1908        self.inner.unix_millis.store(millis, Ordering::Release);
1909        self.inner
1910            .unix_millis_is_fixed
1911            .store(true, Ordering::Release);
1912    }
1913
1914    /// Return current Unix time in milliseconds.
1915    ///
1916    /// Production contexts use the live system wall clock. Tests can install
1917    /// an exact fixed value with [`Self::set_unix_millis_for_testing`].
1918    #[must_use]
1919    pub fn current_time_unix_millis(&self) -> u64
1920    where
1921        Caps: cap::HasTime,
1922    {
1923        if self.inner.unix_millis_is_fixed.load(Ordering::Acquire) {
1924            return self.inner.unix_millis.load(Ordering::Acquire);
1925        }
1926
1927        u64::try_from(
1928            SystemTime::now()
1929                .duration_since(SystemTime::UNIX_EPOCH)
1930                .unwrap_or_default()
1931                .as_millis(),
1932        )
1933        .unwrap_or(u64::MAX)
1934    }
1935
1936    /// Return current time as a Julian day.
1937    #[must_use]
1938    pub fn current_time_julian_day(&self) -> f64
1939    where
1940        Caps: cap::HasTime,
1941    {
1942        let millis = self.current_time_unix_millis();
1943        #[allow(clippy::cast_precision_loss)]
1944        let secs = (millis as f64) / 1000.0;
1945        // Unix epoch in Julian days: 2440587.5
1946        2_440_587.5 + (secs / 86_400.0)
1947    }
1948}
1949
1950// ---------------------------------------------------------------------------
1951// MaskGuard — RAII guard for masked cancellation sections (§4.12.2)
1952// ---------------------------------------------------------------------------
1953
1954/// RAII guard that keeps the `Cx` masked while alive.
1955///
1956/// Created by [`Cx::masked()`]. On drop, the mask depth is decremented.
1957#[derive(Debug)]
1958pub struct MaskGuard<'a> {
1959    inner: &'a CxInner,
1960}
1961
1962impl Drop for MaskGuard<'_> {
1963    fn drop(&mut self) {
1964        let previous = self.inner.mask_depth.fetch_sub(1, Ordering::AcqRel);
1965        debug_assert!(previous > 0, "mask depth underflow");
1966        if previous == 1 {
1967            let waiters = {
1968                let _dispatch = self
1969                    .inner
1970                    .cancel_dispatch_gate
1971                    .lock()
1972                    .unwrap_or_else(std::sync::PoisonError::into_inner);
1973                if self.inner.mask_depth.load(Ordering::Acquire) == 0
1974                    && self.inner.cancel_requested.load(Ordering::Acquire)
1975                {
1976                    take_local_cancel_waiters(self.inner)
1977                } else {
1978                    Vec::new()
1979                }
1980            };
1981            let mut first_panic = None;
1982            dispatch_local_cancel_waiters(waiters, &mut first_panic);
1983            if let Some(payload) = first_panic {
1984                // Never resume arbitrary callback panic from a destructor:
1985                // this guard may itself be running during another unwind.
1986                std::mem::forget(payload);
1987            }
1988        }
1989    }
1990}
1991
1992// ---------------------------------------------------------------------------
1993// CommitCtx — bounded context for commit sections (§4.12.3)
1994// ---------------------------------------------------------------------------
1995
1996/// Context passed to commit-section bodies.
1997///
1998/// Tracks a poll-quota budget that operations can decrement via [`Self::tick`].
1999#[derive(Debug)]
2000pub struct CommitCtx {
2001    poll_remaining: AtomicU32,
2002}
2003
2004impl CommitCtx {
2005    fn new(poll_quota: u32) -> Self {
2006        Self {
2007            poll_remaining: AtomicU32::new(poll_quota),
2008        }
2009    }
2010
2011    /// Remaining poll budget.
2012    #[must_use]
2013    pub fn poll_remaining(&self) -> u32 {
2014        self.poll_remaining.load(Ordering::Acquire)
2015    }
2016
2017    /// Consume one unit of poll budget. Returns `true` if budget remains.
2018    pub fn tick(&self) -> bool {
2019        let prev = self.poll_remaining.load(Ordering::Acquire);
2020        if prev == 0 {
2021            return false;
2022        }
2023        self.poll_remaining.fetch_sub(1, Ordering::AcqRel);
2024        true
2025    }
2026}
2027
2028#[cfg(test)]
2029mod tests {
2030    use super::*;
2031    use crate::eprocess::{EProcessConfig, EProcessSignal};
2032    use std::path::{Path, PathBuf};
2033    use std::sync::atomic::{AtomicBool, AtomicUsize};
2034    use std::sync::{Arc, Barrier, Weak};
2035
2036    #[derive(Debug, Default)]
2037    struct CountingWake(AtomicUsize);
2038
2039    impl std::task::Wake for CountingWake {
2040        fn wake(self: Arc<Self>) {
2041            self.0.fetch_add(1, Ordering::AcqRel);
2042        }
2043
2044        fn wake_by_ref(self: &Arc<Self>) {
2045            self.0.fetch_add(1, Ordering::AcqRel);
2046        }
2047    }
2048
2049    #[derive(Debug)]
2050    struct DescendantStateProbeWake {
2051        descendant: Weak<CxInner>,
2052        wake_count: AtomicUsize,
2053        saw_descendant_cancelled: AtomicBool,
2054        dispatch_gate_was_unlocked: AtomicBool,
2055    }
2056
2057    impl DescendantStateProbeWake {
2058        fn observe(&self) {
2059            let descendant = self
2060                .descendant
2061                .upgrade()
2062                .expect("observed descendant should remain alive");
2063            self.saw_descendant_cancelled.store(
2064                descendant.cancel_requested.load(Ordering::Acquire),
2065                Ordering::Release,
2066            );
2067            let dispatch_guard = descendant
2068                .cancel_dispatch_gate
2069                .try_lock()
2070                .expect("cancellation callbacks must run outside the family phase gate");
2071            self.dispatch_gate_was_unlocked
2072                .store(true, Ordering::Release);
2073            drop(dispatch_guard);
2074            self.wake_count.fetch_add(1, Ordering::AcqRel);
2075        }
2076    }
2077
2078    impl std::task::Wake for DescendantStateProbeWake {
2079        fn wake(self: Arc<Self>) {
2080            self.observe();
2081        }
2082
2083        fn wake_by_ref(self: &Arc<Self>) {
2084            self.observe();
2085        }
2086    }
2087
2088    #[derive(Debug)]
2089    struct ReentrantFamilyWake {
2090        cx: Cx<FullCaps>,
2091        wake_count: AtomicUsize,
2092        child_inherited_cancellation: AtomicBool,
2093    }
2094
2095    impl ReentrantFamilyWake {
2096        fn exercise(&self) {
2097            self.cx.cancel_with_reason(CancelReason::Abort);
2098            let child = self.cx.create_child();
2099            self.child_inherited_cancellation.store(
2100                child.cancel_reason() == Some(CancelReason::Abort),
2101                Ordering::Release,
2102            );
2103            let mask = self.cx.masked();
2104            drop(mask);
2105            self.wake_count.fetch_add(1, Ordering::AcqRel);
2106        }
2107    }
2108
2109    impl std::task::Wake for ReentrantFamilyWake {
2110        fn wake(self: Arc<Self>) {
2111            self.exercise();
2112        }
2113
2114        fn wake_by_ref(self: &Arc<Self>) {
2115            self.exercise();
2116        }
2117    }
2118
2119    #[derive(Debug, Default)]
2120    struct PanicWake;
2121
2122    impl std::task::Wake for PanicWake {
2123        fn wake(self: Arc<Self>) {
2124            panic!("intentional cancellation-waker panic");
2125        }
2126
2127        fn wake_by_ref(self: &Arc<Self>) {
2128            panic!("intentional cancellation-waker panic");
2129        }
2130    }
2131
2132    #[derive(Debug)]
2133    struct RegistryProbeWake {
2134        inner: Weak<CxInner>,
2135        wake_count: AtomicUsize,
2136        registry_was_unlocked: AtomicBool,
2137    }
2138
2139    impl RegistryProbeWake {
2140        fn probe_registry(&self) {
2141            let inner = self
2142                .inner
2143                .upgrade()
2144                .expect("observed context should remain alive");
2145            let registry_guard = inner
2146                .local_cancel_waiters
2147                .try_lock()
2148                .expect("waker callbacks must run after releasing the waiter registry");
2149            self.registry_was_unlocked.store(true, Ordering::Release);
2150            drop(registry_guard);
2151            self.wake_count.fetch_add(1, Ordering::AcqRel);
2152        }
2153    }
2154
2155    impl std::task::Wake for RegistryProbeWake {
2156        fn wake(self: Arc<Self>) {
2157            self.probe_registry();
2158        }
2159
2160        fn wake_by_ref(self: &Arc<Self>) {
2161            self.probe_registry();
2162        }
2163    }
2164
2165    fn local_cancel_waiter_count<Caps: cap::SubsetOf<cap::All>>(cx: &Cx<Caps>) -> usize {
2166        cx.inner
2167            .local_cancel_waiters
2168            .lock()
2169            .unwrap_or_else(std::sync::PoisonError::into_inner)
2170            .entries
2171            .len()
2172    }
2173
2174    #[test]
2175    fn test_cx_checkpoint_observes_cancellation() {
2176        let cx = Cx::new();
2177        assert_eq!(local_cancel_waiter_count(&cx), 0);
2178        assert!(cx.checkpoint().is_ok());
2179        cx.cancel();
2180        assert_eq!(local_cancel_waiter_count(&cx), 0);
2181        let err = cx.checkpoint().unwrap_err();
2182        assert_eq!(err.kind(), ErrorKind::Cancelled);
2183        assert_eq!(err.sqlite_error_code(), SQLITE_INTERRUPT);
2184    }
2185
2186    #[test]
2187    fn test_cx_capability_narrowing_compiles() {
2188        let cx = Cx::<FullCaps>::new();
2189        let _compute = cx.restrict::<ComputeCaps>();
2190        let _storage = cx.restrict::<StorageCaps>();
2191    }
2192
2193    #[test]
2194    fn test_cx_budget_meet_tightens() {
2195        let parent = Budget::INFINITE.with_deadline(Duration::from_millis(100));
2196        let child = Budget::INFINITE.with_deadline(Duration::from_millis(200));
2197        let effective = parent.meet(child);
2198        assert_eq!(effective.deadline, Some(Duration::from_millis(100)));
2199    }
2200
2201    #[test]
2202    fn test_cx_budget_priority_join() {
2203        let parent = Budget::INFINITE.with_priority(2);
2204        let child = Budget::INFINITE.with_priority(5);
2205        let effective = parent.meet(child);
2206        assert_eq!(effective.priority, 5);
2207    }
2208
2209    #[cfg(feature = "native")]
2210    #[test]
2211    fn bd_2jpu6_2_native_budget_translation_uses_supplied_clock_domain() {
2212        let now = NativeTime::from_nanos(1_000);
2213        let local = Budget::INFINITE.with_deadline(Duration::from_nanos(250));
2214
2215        let native = native_budget_from_local_at(local, now);
2216
2217        assert_eq!(native.deadline, Some(NativeTime::from_nanos(1_250)));
2218    }
2219
2220    #[cfg(feature = "native")]
2221    #[test]
2222    fn bd_2jpu6_2_native_spawn_budget_meets_parent_bounds_and_priority() {
2223        let parent = NativeBudget::INFINITE
2224            .with_poll_quota(80)
2225            .with_cost_quota(900)
2226            .with_priority(9);
2227        let native_cx = NativeCx::for_testing_with_budget(parent);
2228        let local = Cx::<FullCaps>::with_budget(
2229            Budget::INFINITE
2230                .with_poll_quota(60)
2231                .with_cost_quota(700)
2232                .with_priority(3),
2233        );
2234
2235        let effective = local.native_spawn_budget(&native_cx);
2236
2237        assert_eq!(effective.poll_quota, 60);
2238        assert_eq!(effective.cost_quota, Some(700));
2239        assert_eq!(effective.priority, 9);
2240    }
2241
2242    #[test]
2243    fn test_cx_scope_with_budget_cannot_loosen() {
2244        let cx =
2245            Cx::<FullCaps>::with_budget(Budget::INFINITE.with_deadline(Duration::from_millis(50)));
2246        let child = Budget::INFINITE.with_deadline(Duration::from_millis(100));
2247        let scoped = cx.scope_with_budget(child);
2248        assert_eq!(scoped.budget().deadline, Some(Duration::from_millis(50)));
2249    }
2250
2251    #[test]
2252    fn test_cx_checkpoint_with_message_records_message() {
2253        let cx = Cx::new();
2254        assert!(cx.checkpoint_with("vdbe pc=5").is_ok());
2255        assert_eq!(cx.last_checkpoint_message().as_deref(), Some("vdbe pc=5"));
2256    }
2257
2258    #[test]
2259    fn test_cx_cleanup_uses_minimal_budget() {
2260        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_poll_quota(10_000));
2261        let cleanup = cx.cleanup_scope();
2262        assert_eq!(cleanup.budget(), Budget::MINIMAL);
2263    }
2264
2265    #[test]
2266    fn test_cx_restrict_storage_to_compute() {
2267        let cx = Cx::<FullCaps>::new();
2268        let storage = cx.restrict::<StorageCaps>();
2269        let _compute = storage.restrict::<ComputeCaps>();
2270    }
2271
2272    #[test]
2273    fn test_cx_restrict_is_zero_cost() {
2274        // CapSet is a ZST; Cx carries only Arc + Budget + PhantomData.
2275        // Restrict changes only the phantom marker — same size, same pointer.
2276        assert_eq!(
2277            std::mem::size_of::<Cx<FullCaps>>(),
2278            std::mem::size_of::<Cx<ComputeCaps>>()
2279        );
2280    }
2281
2282    #[test]
2283    fn test_budget_mixed_lattice() {
2284        let a = Budget {
2285            deadline: Some(Duration::from_millis(100)),
2286            poll_quota: 500,
2287            cost_quota: Some(1000),
2288            priority: 2,
2289        };
2290        let b = Budget {
2291            deadline: Some(Duration::from_millis(200)),
2292            poll_quota: 300,
2293            cost_quota: Some(2000),
2294            priority: 5,
2295        };
2296        let m = a.meet(b);
2297        // Resources tighten by min.
2298        assert_eq!(m.deadline, Some(Duration::from_millis(100)));
2299        assert_eq!(m.poll_quota, 300);
2300        assert_eq!(m.cost_quota, Some(1000));
2301        // Priority propagates by max (join).
2302        assert_eq!(m.priority, 5);
2303    }
2304
2305    #[test]
2306    fn test_budget_meet_commutative() {
2307        let a = Budget {
2308            deadline: Some(Duration::from_millis(50)),
2309            poll_quota: 400,
2310            cost_quota: Some(800),
2311            priority: 3,
2312        };
2313        let b = Budget {
2314            deadline: Some(Duration::from_millis(150)),
2315            poll_quota: 200,
2316            cost_quota: None,
2317            priority: 7,
2318        };
2319        assert_eq!(a.meet(b), b.meet(a));
2320    }
2321
2322    #[test]
2323    fn test_budget_meet_associative() {
2324        let a = Budget::INFINITE
2325            .with_deadline(Duration::from_millis(50))
2326            .with_poll_quota(100)
2327            .with_priority(1);
2328        let b = Budget::INFINITE
2329            .with_deadline(Duration::from_millis(150))
2330            .with_poll_quota(200)
2331            .with_priority(5);
2332        let c = Budget::INFINITE
2333            .with_deadline(Duration::from_millis(75))
2334            .with_poll_quota(50)
2335            .with_priority(3);
2336        assert_eq!(a.meet(b).meet(c), a.meet(b.meet(c)));
2337    }
2338
2339    #[test]
2340    fn test_budget_minimal_is_stricter_than_normal() {
2341        let normal = Budget::INFINITE.with_poll_quota(10_000);
2342        let effective = normal.meet(Budget::MINIMAL);
2343        assert_eq!(effective.poll_quota, Budget::MINIMAL.poll_quota);
2344    }
2345
2346    #[test]
2347    fn test_cx_cancel_shared_across_clones() {
2348        let cx1 = Cx::<FullCaps>::new();
2349        let cx2 = cx1.clone();
2350        assert!(!cx2.is_cancel_requested());
2351        cx1.cancel();
2352        assert!(cx2.is_cancel_requested());
2353        assert!(cx2.checkpoint().is_err());
2354    }
2355
2356    #[test]
2357    fn test_cx_cancel_shared_across_restrict() {
2358        let cx = Cx::<FullCaps>::new();
2359        let compute = cx.restrict::<ComputeCaps>();
2360        cx.cancel();
2361        assert!(compute.checkpoint().is_err());
2362    }
2363
2364    fn system_time_unix_millis() -> u64 {
2365        u64::try_from(
2366            SystemTime::now()
2367                .duration_since(SystemTime::UNIX_EPOCH)
2368                .unwrap_or_default()
2369                .as_millis(),
2370        )
2371        .unwrap_or(u64::MAX)
2372    }
2373
2374    #[test]
2375    fn test_cx_current_time_uses_live_clock_by_default() {
2376        let cx = Cx::<FullCaps>::new();
2377        let observed = cx.current_time_unix_millis();
2378        let expected = system_time_unix_millis();
2379
2380        assert!(
2381            observed.abs_diff(expected) <= 60_000,
2382            "default Cx clock must be live: observed={observed}, expected approximately {expected}"
2383        );
2384    }
2385
2386    #[test]
2387    fn test_cx_fixed_unix_millis_supports_full_u64_domain() {
2388        let cx = Cx::<FullCaps>::new();
2389
2390        cx.set_unix_millis_for_testing(0);
2391        assert_eq!(cx.current_time_unix_millis(), 0);
2392
2393        cx.set_unix_millis_for_testing(u64::MAX);
2394        assert_eq!(cx.current_time_unix_millis(), u64::MAX);
2395    }
2396
2397    #[test]
2398    fn test_cx_current_time_julian_day_uses_fixed_unix_millis() {
2399        let cx = Cx::<FullCaps>::new();
2400
2401        // Unix epoch = Julian day 2440587.5.
2402        cx.set_unix_millis_for_testing(0);
2403        let jd = cx.current_time_julian_day();
2404        assert!((jd - 2_440_587.5).abs() < 1e-10);
2405
2406        // One day = 86_400_000 ms.
2407        cx.set_unix_millis_for_testing(86_400_000);
2408        let jd = cx.current_time_julian_day();
2409        assert!((jd - 2_440_588.5).abs() < 1e-10);
2410    }
2411
2412    #[test]
2413    fn test_cx_children_inherit_fixed_or_live_clock_state() {
2414        let fixed_parent = Cx::<FullCaps>::new();
2415        fixed_parent.set_unix_millis_for_testing(0);
2416        let fixed_child = fixed_parent.create_child();
2417        let fixed_spawn_child = fixed_parent.create_child_for_spawn();
2418
2419        assert_eq!(fixed_child.current_time_unix_millis(), 0);
2420        assert_eq!(fixed_spawn_child.current_time_unix_millis(), 0);
2421
2422        // Child clocks are snapshots rather than aliases of the parent's
2423        // override, so a later parent update cannot rewrite an existing child.
2424        fixed_parent.set_unix_millis_for_testing(86_400_000);
2425        assert_eq!(fixed_child.current_time_unix_millis(), 0);
2426
2427        let live_parent = Cx::<FullCaps>::new();
2428        let live_child = live_parent.create_child();
2429        let observed = live_child.current_time_unix_millis();
2430        let expected = system_time_unix_millis();
2431        assert!(
2432            observed.abs_diff(expected) <= 60_000,
2433            "child of live Cx must remain live: observed={observed}, expected approximately {expected}"
2434        );
2435    }
2436
2437    #[test]
2438    fn test_cx_fixed_clock_updates_publish_complete_values() {
2439        const FIRST: u64 = 0xAAAA_AAAA_AAAA_AAAA;
2440        const SECOND: u64 = 0x5555_5555_5555_5555;
2441
2442        let cx = Cx::<FullCaps>::new();
2443        cx.set_unix_millis_for_testing(FIRST);
2444        let writer_cx = cx.clone();
2445        let writer = std::thread::spawn(move || {
2446            for _ in 0..1_000 {
2447                writer_cx.set_unix_millis_for_testing(FIRST);
2448                writer_cx.set_unix_millis_for_testing(SECOND);
2449            }
2450        });
2451
2452        for _ in 0..1_000 {
2453            let observed = cx.current_time_unix_millis();
2454            assert!(matches!(observed, FIRST | SECOND));
2455        }
2456        writer.join().expect("clock writer must not panic");
2457        assert_eq!(cx.current_time_unix_millis(), SECOND);
2458    }
2459
2460    #[test]
2461    fn test_capset_is_zero_sized() {
2462        assert_eq!(std::mem::size_of::<cap::All>(), 0);
2463        assert_eq!(std::mem::size_of::<cap::None>(), 0);
2464        assert_eq!(
2465            std::mem::size_of::<cap::CapSet<true, false, true, false, true>>(),
2466            0
2467        );
2468    }
2469
2470    #[test]
2471    fn test_cx_checkpoint_not_cancelled() {
2472        let cx = Cx::new();
2473        assert!(cx.checkpoint().is_ok());
2474        assert!(cx.checkpoint_with("still going").is_ok());
2475    }
2476
2477    #[test]
2478    fn test_cx_checkpoint_maps_to_sqlite_interrupt() {
2479        let cx = Cx::new();
2480        cx.cancel();
2481        let err = cx.checkpoint().unwrap_err();
2482        assert_eq!(err.sqlite_error_code(), SQLITE_INTERRUPT);
2483    }
2484
2485    #[test]
2486    fn test_cx_checkpoint_eprocess_sheds_low_priority_context() {
2487        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
2488        let oracle = Arc::new(EProcessOracle::new(
2489            EProcessConfig {
2490                p0: 0.1,
2491                lambda: 5.0,
2492                alpha: 0.05,
2493                max_evalue: 1e12,
2494            },
2495            1,
2496        ));
2497        let signal = EProcessSignal::new(1.0, 1.0, 1.0);
2498        oracle.observe_signal(signal);
2499        oracle.observe_signal(signal);
2500        cx.set_eprocess_oracle(oracle);
2501        let err = cx.checkpoint().unwrap_err();
2502        assert_eq!(err.kind(), ErrorKind::Cancelled);
2503        assert_eq!(cx.cancel_reason(), Some(CancelReason::Abort));
2504        let decision = cx
2505            .last_eprocess_decision()
2506            .expect("checkpoint should record an e-process decision");
2507        assert!(decision.should_shed);
2508        assert_eq!(decision.snapshot.last_signal, Some(signal));
2509    }
2510
2511    #[test]
2512    fn test_cx_checkpoint_eprocess_respects_priority_threshold() {
2513        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(1));
2514        let oracle = Arc::new(EProcessOracle::new(
2515            EProcessConfig {
2516                p0: 0.1,
2517                lambda: 5.0,
2518                alpha: 0.05,
2519                max_evalue: 1e12,
2520            },
2521            1,
2522        ));
2523        let signal = EProcessSignal::new(1.0, 1.0, 1.0);
2524        oracle.observe_signal(signal);
2525        oracle.observe_signal(signal);
2526        cx.set_eprocess_oracle(oracle);
2527        assert!(cx.checkpoint().is_ok());
2528        assert!(!cx.is_cancel_requested());
2529        let decision = cx
2530            .last_eprocess_decision()
2531            .expect("checkpoint should still record non-shedding decisions");
2532        assert!(!decision.should_shed);
2533        assert_eq!(decision.priority, 1);
2534        assert_eq!(decision.snapshot.last_signal, Some(signal));
2535    }
2536
2537    #[test]
2538    fn test_cx_checkpoint_eprocess_preserves_masking_semantics() {
2539        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
2540        let oracle = Arc::new(EProcessOracle::new(
2541            EProcessConfig {
2542                p0: 0.1,
2543                lambda: 5.0,
2544                alpha: 0.05,
2545                max_evalue: 1e12,
2546            },
2547            1,
2548        ));
2549        let signal = EProcessSignal::new(1.0, 1.0, 1.0);
2550        oracle.observe_signal(signal);
2551        oracle.observe_signal(signal);
2552        cx.set_eprocess_oracle(oracle);
2553        {
2554            let _mask = cx.masked();
2555            assert!(cx.checkpoint().is_ok());
2556            assert!(cx.is_cancel_requested());
2557            assert_eq!(cx.cancel_state(), CancelState::CancelRequested);
2558            assert_eq!(
2559                cx.last_eprocess_snapshot()
2560                    .expect("checkpoint should record the masked decision")
2561                    .last_signal,
2562                Some(signal)
2563            );
2564        }
2565        let err = cx.checkpoint().unwrap_err();
2566        assert_eq!(err.kind(), ErrorKind::Cancelled);
2567    }
2568
2569    #[test]
2570    fn test_create_child_inherits_eprocess_oracle() {
2571        let parent = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
2572        let oracle = Arc::new(EProcessOracle::new(
2573            EProcessConfig {
2574                p0: 0.1,
2575                lambda: 5.0,
2576                alpha: 0.05,
2577                max_evalue: 1e12,
2578            },
2579            1,
2580        ));
2581        let signal = EProcessSignal::new(1.0, 1.0, 1.0);
2582        oracle.observe_signal(signal);
2583        oracle.observe_signal(signal);
2584        parent.set_eprocess_oracle(oracle);
2585
2586        let child = parent.create_child();
2587        let err = child.checkpoint().unwrap_err();
2588        assert_eq!(err.kind(), ErrorKind::Cancelled);
2589        assert_eq!(child.cancel_reason(), Some(CancelReason::Abort));
2590        assert_eq!(
2591            child
2592                .last_eprocess_snapshot()
2593                .expect("child checkpoint should record inherited oracle decision")
2594                .last_signal,
2595            Some(signal)
2596        );
2597    }
2598
2599    #[test]
2600    fn spawn_child_preserves_logical_context_without_thread_affinity() {
2601        let budget = Budget {
2602            deadline: Some(Duration::from_secs(7)),
2603            poll_quota: 123,
2604            cost_quota: Some(456),
2605            priority: 3,
2606        };
2607        let parent = Cx::<FullCaps>::with_budget(budget).with_trace_context(50, 60, 70);
2608        let oracle = Arc::new(EProcessOracle::new(
2609            EProcessConfig {
2610                p0: 0.1,
2611                lambda: 5.0,
2612                alpha: 0.05,
2613                max_evalue: 1e12,
2614            },
2615            1,
2616        ));
2617        parent.set_eprocess_oracle(Arc::clone(&oracle));
2618        parent.mark_blocking_io_inline_safe();
2619
2620        let child = parent.create_child_for_spawn();
2621        assert_eq!(child.budget(), budget);
2622        assert_eq!(child.trace_id(), 50);
2623        assert_eq!(child.decision_id(), 60);
2624        assert_eq!(child.policy_id(), 70);
2625        assert!(
2626            Arc::ptr_eq(
2627                child
2628                    .inner
2629                    .eprocess_oracle
2630                    .get()
2631                    .expect("spawn child should inherit the e-process oracle"),
2632                &oracle
2633            ),
2634            "spawn child must retain the exact logical policy oracle"
2635        );
2636        assert!(
2637            !child.blocking_io_inline_safe(),
2638            "spawn child must not inherit an OS-thread-only I/O permission"
2639        );
2640
2641        parent.cancel_with_reason(CancelReason::RegionClose);
2642        assert_eq!(child.cancel_reason(), Some(CancelReason::RegionClose));
2643    }
2644
2645    #[test]
2646    fn test_create_child_inherits_preexisting_parent_cancellation() {
2647        let parent = Cx::<FullCaps>::new();
2648        parent.cancel_with_reason(CancelReason::RegionClose);
2649
2650        let child = parent.create_child();
2651        assert_eq!(child.cancel_reason(), Some(CancelReason::RegionClose));
2652        assert_eq!(child.cancel_state(), CancelState::CancelRequested);
2653
2654        let err = child.checkpoint().unwrap_err();
2655        assert_eq!(err.kind(), ErrorKind::Cancelled);
2656    }
2657
2658    #[test]
2659    fn local_cancel_relay_is_subtree_scoped_and_reason_monotone() {
2660        let root = Cx::<FullCaps>::new();
2661        let sibling = root.create_child();
2662        let (operation, relay) = root.create_child_with_local_cancel_relay();
2663        let existing_descendant = operation.create_child();
2664
2665        assert!(relay.cancel_local(CancelReason::Timeout));
2666        assert!(relay.cancel_local(CancelReason::Abort));
2667        assert!(relay.cancel_local(CancelReason::UserInterrupt));
2668
2669        assert_eq!(operation.cancel_reason(), Some(CancelReason::Abort));
2670        assert_eq!(
2671            existing_descendant.cancel_reason(),
2672            Some(CancelReason::Abort)
2673        );
2674        assert!(operation.checkpoint().is_err());
2675        assert!(existing_descendant.checkpoint().is_err());
2676
2677        assert!(root.checkpoint().is_ok());
2678        assert!(sibling.checkpoint().is_ok());
2679        assert!(!root.is_cancel_requested());
2680        assert!(!sibling.is_cancel_requested());
2681
2682        let late_descendant = operation.create_child();
2683        assert_eq!(late_descendant.cancel_reason(), Some(CancelReason::Abort));
2684        assert!(late_descendant.checkpoint().is_err());
2685        assert!(root.checkpoint().is_ok());
2686    }
2687
2688    #[test]
2689    fn local_cancel_relay_is_weak_and_cross_thread_safe() {
2690        fn assert_send_sync<T: Send + Sync>() {}
2691        assert_send_sync::<LocalCancelRelay>();
2692
2693        let root = Cx::<FullCaps>::new();
2694        let (operation, relay) = root.create_child_with_local_cancel_relay();
2695        let cancel_thread =
2696            std::thread::spawn(move || relay.cancel_local(CancelReason::RegionClose));
2697        assert!(
2698            cancel_thread
2699                .join()
2700                .expect("cancel thread should not panic"),
2701            "live operation should accept a relayed cancellation"
2702        );
2703        assert_eq!(operation.cancel_reason(), Some(CancelReason::RegionClose));
2704
2705        let (dropped_operation, dropped_relay) = root.create_child_with_local_cancel_relay();
2706        drop(dropped_operation);
2707        assert!(
2708            !dropped_relay.cancel_local(CancelReason::Abort),
2709            "a weak relay must become inert after its target is dropped"
2710        );
2711        assert!(root.checkpoint().is_ok());
2712    }
2713
2714    #[test]
2715    fn local_cancellation_future_wakes_for_local_relay() {
2716        let root = Cx::<FullCaps>::new();
2717        let (operation, relay) = root.create_child_with_local_cancel_relay();
2718        let wake_count = Arc::new(CountingWake::default());
2719        let waker = Waker::from(Arc::clone(&wake_count));
2720        let mut task_cx = TaskContext::from_waker(&waker);
2721        let mut cancellation = std::pin::pin!(operation.wait_for_local_cancellation());
2722
2723        assert_eq!(
2724            cancellation.as_mut().poll(&mut task_cx),
2725            Poll::Pending,
2726            "uncancelled operation should register one waiter"
2727        );
2728        assert_eq!(local_cancel_waiter_count(&operation), 1);
2729
2730        assert!(relay.cancel_local(CancelReason::RegionClose));
2731        assert_eq!(
2732            wake_count.0.load(Ordering::Acquire),
2733            1,
2734            "local relay cancellation should wake the registered future"
2735        );
2736        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2737        assert_eq!(
2738            local_cancel_waiter_count(&operation),
2739            0,
2740            "ready future must leave no stale registration"
2741        );
2742        assert!(root.checkpoint().is_ok());
2743    }
2744
2745    #[test]
2746    fn dropping_local_cancellation_future_unregisters_waiter() {
2747        let cx = Cx::<FullCaps>::new();
2748        let wake_count = Arc::new(CountingWake::default());
2749        let waker = Waker::from(wake_count);
2750        let mut task_cx = TaskContext::from_waker(&waker);
2751
2752        {
2753            let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2754            assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2755            assert_eq!(local_cancel_waiter_count(&cx), 1);
2756        }
2757
2758        assert_eq!(
2759            local_cancel_waiter_count(&cx),
2760            0,
2761            "dropping a pending future must remove its waker"
2762        );
2763    }
2764
2765    #[test]
2766    fn already_cancelled_local_future_never_registers_a_waiter() {
2767        let cx = Cx::<FullCaps>::new();
2768        cx.cancel();
2769        assert_eq!(local_cancel_waiter_count(&cx), 0);
2770
2771        let wake_count = Arc::new(CountingWake::default());
2772        let waker = Waker::from(wake_count);
2773        let mut task_cx = TaskContext::from_waker(&waker);
2774        let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2775        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2776        assert_eq!(
2777            local_cancel_waiter_count(&cx),
2778            0,
2779            "an already-ready first poll must not register a waiter"
2780        );
2781    }
2782
2783    #[test]
2784    fn repoll_replaces_the_registered_waker() {
2785        let cx = Cx::<FullCaps>::new();
2786        let first_wake_count = Arc::new(CountingWake::default());
2787        let first_waker = Waker::from(Arc::clone(&first_wake_count));
2788        let mut first_task_cx = TaskContext::from_waker(&first_waker);
2789        let second_wake_count = Arc::new(CountingWake::default());
2790        let second_waker = Waker::from(Arc::clone(&second_wake_count));
2791        let mut second_task_cx = TaskContext::from_waker(&second_waker);
2792        let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2793
2794        assert_eq!(
2795            cancellation.as_mut().poll(&mut first_task_cx),
2796            Poll::Pending
2797        );
2798        assert_eq!(
2799            cancellation.as_mut().poll(&mut second_task_cx),
2800            Poll::Pending
2801        );
2802        assert_eq!(local_cancel_waiter_count(&cx), 1);
2803
2804        cx.cancel();
2805        assert_eq!(
2806            first_wake_count.0.load(Ordering::Acquire),
2807            0,
2808            "a replaced waker must not be invoked"
2809        );
2810        assert_eq!(
2811            second_wake_count.0.load(Ordering::Acquire),
2812            1,
2813            "only the most recently registered waker should be invoked"
2814        );
2815        assert_eq!(
2816            cancellation.as_mut().poll(&mut second_task_cx),
2817            Poll::Ready(())
2818        );
2819    }
2820
2821    #[test]
2822    fn local_cancellation_waker_runs_outside_the_registry_lock() {
2823        let cx = Cx::<FullCaps>::new();
2824        let probe = Arc::new(RegistryProbeWake {
2825            inner: Arc::downgrade(&cx.inner),
2826            wake_count: AtomicUsize::new(0),
2827            registry_was_unlocked: AtomicBool::new(false),
2828        });
2829        let waker = Waker::from(Arc::clone(&probe));
2830        let mut task_cx = TaskContext::from_waker(&waker);
2831        let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2832
2833        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2834        cx.cancel();
2835        assert!(
2836            probe.registry_was_unlocked.load(Ordering::Acquire),
2837            "wake callback should acquire the registry without reentrant deadlock"
2838        );
2839        assert_eq!(probe.wake_count.load(Ordering::Acquire), 1);
2840        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2841    }
2842
2843    #[test]
2844    fn cancellation_publishes_the_complete_subtree_before_waking_observers() {
2845        let root = Cx::<FullCaps>::new();
2846        let descendant = root.create_child();
2847        let probe = Arc::new(DescendantStateProbeWake {
2848            descendant: Arc::downgrade(&descendant.inner),
2849            wake_count: AtomicUsize::new(0),
2850            saw_descendant_cancelled: AtomicBool::new(false),
2851            dispatch_gate_was_unlocked: AtomicBool::new(false),
2852        });
2853        let waker = Waker::from(Arc::clone(&probe));
2854        let mut task_cx = TaskContext::from_waker(&waker);
2855        let mut cancellation = std::pin::pin!(root.wait_for_local_cancellation());
2856
2857        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2858        root.cancel();
2859
2860        assert_eq!(probe.wake_count.load(Ordering::Acquire), 1);
2861        assert!(
2862            probe.saw_descendant_cancelled.load(Ordering::Acquire),
2863            "reentrant observers must never see a half-published cancellation tree"
2864        );
2865        assert!(
2866            probe.dispatch_gate_was_unlocked.load(Ordering::Acquire),
2867            "callbacks must be able to re-enter family cancellation machinery"
2868        );
2869        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2870        assert!(descendant.checkpoint().is_err());
2871    }
2872
2873    #[test]
2874    fn panicking_cancellation_waker_does_not_suppress_other_observers() {
2875        let root = Cx::<FullCaps>::new();
2876        let descendant = root.create_child();
2877        let panic_waker = Waker::from(Arc::new(PanicWake));
2878        let mut panic_task_cx = TaskContext::from_waker(&panic_waker);
2879        let wake_count = Arc::new(CountingWake::default());
2880        let counting_waker = Waker::from(Arc::clone(&wake_count));
2881        let mut counting_task_cx = TaskContext::from_waker(&counting_waker);
2882        let mut panicking = std::pin::pin!(root.wait_for_local_cancellation());
2883        let mut counting = std::pin::pin!(root.wait_for_local_cancellation());
2884
2885        assert_eq!(panicking.as_mut().poll(&mut panic_task_cx), Poll::Pending);
2886        assert_eq!(counting.as_mut().poll(&mut counting_task_cx), Poll::Pending);
2887        let cancel_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2888            root.cancel();
2889        }));
2890
2891        assert!(
2892            cancel_result.is_err(),
2893            "the first callback panic must be resumed after notification completes"
2894        );
2895        assert_eq!(
2896            wake_count.0.load(Ordering::Acquire),
2897            1,
2898            "one panicking observer must not suppress later observers"
2899        );
2900        assert!(descendant.checkpoint().is_err());
2901        assert_eq!(panicking.as_mut().poll(&mut panic_task_cx), Poll::Ready(()));
2902        assert_eq!(
2903            counting.as_mut().poll(&mut counting_task_cx),
2904            Poll::Ready(())
2905        );
2906        assert_eq!(local_cancel_waiter_count(&root), 0);
2907    }
2908
2909    #[test]
2910    fn cancellation_waker_can_reenter_family_state_without_deadlock() {
2911        let root = Cx::<FullCaps>::new();
2912        let probe = Arc::new(ReentrantFamilyWake {
2913            cx: root.clone(),
2914            wake_count: AtomicUsize::new(0),
2915            child_inherited_cancellation: AtomicBool::new(false),
2916        });
2917        let waker = Waker::from(Arc::clone(&probe));
2918        let mut task_cx = TaskContext::from_waker(&waker);
2919        let mut cancellation = std::pin::pin!(root.wait_for_local_cancellation());
2920
2921        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2922        root.cancel();
2923
2924        assert_eq!(probe.wake_count.load(Ordering::Acquire), 1);
2925        assert!(
2926            probe.child_inherited_cancellation.load(Ordering::Acquire),
2927            "a child created reentrantly must be initialized before it is linked"
2928        );
2929        assert_eq!(root.cancel_reason(), Some(CancelReason::Abort));
2930        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2931    }
2932
2933    #[test]
2934    fn repeated_local_waiter_poll_and_drop_accumulates_nothing() {
2935        let cx = Cx::<FullCaps>::new();
2936        let initial_children = cx
2937            .inner
2938            .children
2939            .lock()
2940            .unwrap_or_else(std::sync::PoisonError::into_inner)
2941            .len();
2942        let wake_count = Arc::new(CountingWake::default());
2943        let waker = Waker::from(wake_count);
2944        let mut task_cx = TaskContext::from_waker(&waker);
2945
2946        for _ in 0..256 {
2947            let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2948            assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2949        }
2950
2951        assert_eq!(
2952            local_cancel_waiter_count(&cx),
2953            0,
2954            "dropped local wait futures must not accumulate registry entries"
2955        );
2956        assert_eq!(
2957            cx.inner
2958                .children
2959                .lock()
2960                .unwrap_or_else(std::sync::PoisonError::into_inner)
2961                .len(),
2962            initial_children,
2963            "local wait futures must not allocate child contexts"
2964        );
2965    }
2966
2967    #[test]
2968    fn local_cancellation_future_defers_while_masked_and_wakes_on_unmask() {
2969        let root = Cx::<FullCaps>::new();
2970        let (operation, relay) = root.create_child_with_local_cancel_relay();
2971        let mask = operation.masked();
2972        let wake_count = Arc::new(CountingWake::default());
2973        let waker = Waker::from(Arc::clone(&wake_count));
2974        let mut task_cx = TaskContext::from_waker(&waker);
2975        let mut cancellation = std::pin::pin!(operation.wait_for_local_cancellation());
2976
2977        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2978        assert!(relay.cancel_local(CancelReason::Abort));
2979        assert_eq!(wake_count.0.load(Ordering::Acquire), 1);
2980
2981        assert_eq!(
2982            cancellation.as_mut().poll(&mut task_cx),
2983            Poll::Pending,
2984            "masking must defer cancellation observation"
2985        );
2986        assert_eq!(
2987            local_cancel_waiter_count(&operation),
2988            1,
2989            "a masked future must remain registered for the unmask boundary"
2990        );
2991
2992        drop(mask);
2993        assert_eq!(
2994            wake_count.0.load(Ordering::Acquire),
2995            2,
2996            "outermost unmask must wake a deferred cancellation observer"
2997        );
2998        assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2999    }
3000
3001    #[test]
3002    fn local_cancel_request_future_is_ready_while_masked() {
3003        let root = Cx::<FullCaps>::new();
3004        let (operation, relay) = root.create_child_with_local_cancel_relay();
3005        let mask = operation.masked();
3006        let wake_count = Arc::new(CountingWake::default());
3007        let waker = Waker::from(Arc::clone(&wake_count));
3008        let mut task_cx = TaskContext::from_waker(&waker);
3009        let mut request = std::pin::pin!(operation.wait_for_local_cancel_request());
3010
3011        assert_eq!(request.as_mut().poll(&mut task_cx), Poll::Pending);
3012        assert!(relay.cancel_local(CancelReason::UserInterrupt));
3013        assert_eq!(wake_count.0.load(Ordering::Acquire), 1);
3014        assert_eq!(
3015            request.as_mut().poll(&mut task_cx),
3016            Poll::Ready(()),
3017            "raw request notification must not reinterpret masking policy"
3018        );
3019        assert!(
3020            operation.checkpoint().is_ok(),
3021            "the context checkpoint itself must continue to defer while masked"
3022        );
3023        drop(mask);
3024        assert!(operation.checkpoint().is_err());
3025    }
3026
3027    #[test]
3028    fn local_cancellation_registration_race_leaves_no_stale_waiter() {
3029        for _ in 0..64 {
3030            let root = Cx::<FullCaps>::new();
3031            let (operation, relay) = root.create_child_with_local_cancel_relay();
3032            let barrier = Arc::new(Barrier::new(2));
3033            let cancel_barrier = Arc::clone(&barrier);
3034            let cancel_thread = std::thread::spawn(move || {
3035                cancel_barrier.wait();
3036                relay.cancel_local(CancelReason::UserInterrupt)
3037            });
3038
3039            let wake_count = Arc::new(CountingWake::default());
3040            let waker = Waker::from(Arc::clone(&wake_count));
3041            let mut task_cx = TaskContext::from_waker(&waker);
3042            let mut cancellation = std::pin::pin!(operation.wait_for_local_cancellation());
3043            barrier.wait();
3044            let first_poll = cancellation.as_mut().poll(&mut task_cx);
3045
3046            assert!(
3047                cancel_thread
3048                    .join()
3049                    .expect("cancel thread should not panic")
3050            );
3051            if first_poll == Poll::Pending {
3052                assert!(
3053                    wake_count.0.load(Ordering::Acquire) > 0,
3054                    "a waiter registered during cancellation must be notified"
3055                );
3056            }
3057            assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
3058            assert_eq!(
3059                local_cancel_waiter_count(&operation),
3060                0,
3061                "registration/cancellation race must not strand a waker"
3062            );
3063        }
3064    }
3065
3066    #[test]
3067    fn local_cancel_relay_handles_deep_subtrees_on_a_small_stack() {
3068        let root = Cx::<FullCaps>::new();
3069        let (operation, relay) = root.create_child_with_local_cancel_relay();
3070        let mut chain = Vec::with_capacity(8_193);
3071        chain.push(operation);
3072        for _ in 0..8_192 {
3073            let child = chain
3074                .last()
3075                .expect("chain must contain its root")
3076                .create_child();
3077            chain.push(child);
3078        }
3079
3080        let cancel_thread = std::thread::Builder::new()
3081            .name("local-cancel-deep-tree".to_owned())
3082            .stack_size(256 * 1024)
3083            .spawn(move || relay.cancel_local(CancelReason::RegionClose))
3084            .expect("small-stack cancellation thread should spawn");
3085        assert!(
3086            cancel_thread
3087                .join()
3088                .expect("iterative cancellation traversal must not overflow")
3089        );
3090        assert_eq!(
3091            chain.last().and_then(Cx::cancel_reason),
3092            Some(CancelReason::RegionClose)
3093        );
3094        assert!(root.checkpoint().is_ok());
3095    }
3096
3097    #[cfg(feature = "native")]
3098    #[test]
3099    fn test_cx_checkpoint_native_cx_cancellation_maps_reason() {
3100        let cx = Cx::<FullCaps>::new();
3101        let native = NativeCx::for_testing();
3102        cx.set_native_cx(native.clone());
3103        native.set_cancel_reason(NativeCancelReason::timeout());
3104
3105        let err = cx.checkpoint().unwrap_err();
3106        assert_eq!(err.kind(), ErrorKind::Cancelled);
3107        assert_eq!(cx.cancel_reason(), Some(CancelReason::Timeout));
3108    }
3109
3110    #[cfg(feature = "native")]
3111    #[test]
3112    fn test_cx_cancel_reason_propagates_to_native_cx() {
3113        let cx = Cx::<FullCaps>::new();
3114        let native = NativeCx::for_testing();
3115        cx.set_native_cx(native.clone());
3116
3117        cx.cancel_with_reason(CancelReason::RegionClose);
3118        let reason = native
3119            .cancel_reason()
3120            .expect("native cancel reason must be set");
3121        assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
3122    }
3123
3124    #[cfg(feature = "native")]
3125    #[test]
3126    fn local_cancel_relay_preserves_shared_native_context_for_late_children() {
3127        let root = Cx::<FullCaps>::new();
3128        let native = NativeCx::for_testing();
3129        root.set_native_cx(native.clone());
3130        let sibling = root.create_child();
3131        let (operation, relay) = root.create_child_with_local_cancel_relay();
3132        let existing_descendant = operation.create_child();
3133
3134        assert!(relay.cancel_local(CancelReason::Abort));
3135        assert!(operation.checkpoint().is_err());
3136        assert!(existing_descendant.checkpoint().is_err());
3137        assert!(root.checkpoint().is_ok());
3138        assert!(sibling.checkpoint().is_ok());
3139        assert!(
3140            native.checkpoint().is_ok(),
3141            "local operation cancellation must not poison shared native I/O state"
3142        );
3143
3144        let late_descendant = operation.create_child();
3145        assert!(late_descendant.checkpoint().is_err());
3146        assert!(
3147            native.checkpoint().is_ok(),
3148            "a descendant created after local cancellation must inherit locally"
3149        );
3150
3151        root.cancel_with_reason(CancelReason::RegionClose);
3152        assert!(
3153            native.is_cancel_requested(),
3154            "later ordinary root cancellation must still cross the native boundary"
3155        );
3156    }
3157
3158    #[cfg(feature = "native")]
3159    #[test]
3160    fn local_cancel_relay_preserves_native_contexts_attached_after_cancellation() {
3161        let root = Cx::<FullCaps>::new();
3162        let (operation, relay) = root.create_child_with_local_cancel_relay();
3163        assert!(relay.cancel_local(CancelReason::Abort));
3164
3165        let fallback = operation.effective_native_cx();
3166        assert!(
3167            fallback.checkpoint().is_ok(),
3168            "local cancellation must not taint a later fallback native context"
3169        );
3170
3171        let replacement = NativeCx::for_testing();
3172        operation.set_native_cx(replacement.clone());
3173        assert!(
3174            replacement.checkpoint().is_ok(),
3175            "local cancellation must not taint a later explicit native context"
3176        );
3177        assert!(operation.checkpoint().is_err());
3178    }
3179
3180    #[cfg(feature = "native")]
3181    #[test]
3182    fn local_reason_never_leaks_through_later_ordinary_cancellation() {
3183        let root = Cx::<FullCaps>::new();
3184        let shared_native = NativeCx::for_testing();
3185        root.set_native_cx(shared_native.clone());
3186        let (operation, relay) = root.create_child_with_local_cancel_relay();
3187
3188        assert!(relay.cancel_local(CancelReason::Abort));
3189        root.cancel_with_reason(CancelReason::Timeout);
3190
3191        assert_eq!(operation.cancel_reason(), Some(CancelReason::Abort));
3192        assert_eq!(
3193            shared_native
3194                .cancel_reason()
3195                .expect("ordinary cancellation must reach shared native")
3196                .kind,
3197            NativeCancelKind::Timeout,
3198            "the stronger local-only Abort must not cross the native boundary"
3199        );
3200
3201        let late_descendant = operation.create_child();
3202        assert_eq!(
3203            late_descendant.cancel_reason(),
3204            Some(CancelReason::Abort),
3205            "late descendants inherit the aggregate local reason"
3206        );
3207        assert_eq!(
3208            shared_native
3209                .cancel_reason()
3210                .expect("late-child registration must retain ordinary reason")
3211                .kind,
3212            NativeCancelKind::Timeout
3213        );
3214
3215        operation.clear_native_cx();
3216        let fallback = operation.effective_native_cx();
3217        assert_eq!(
3218            fallback
3219                .cancel_reason()
3220                .expect("late fallback must receive ordinary reason")
3221                .kind,
3222            NativeCancelKind::Timeout
3223        );
3224
3225        let replacement = NativeCx::for_testing();
3226        operation.set_native_cx(replacement.clone());
3227        assert_eq!(
3228            replacement
3229                .cancel_reason()
3230                .expect("late explicit attachment must receive ordinary reason")
3231                .kind,
3232            NativeCancelKind::Timeout
3233        );
3234    }
3235
3236    #[cfg(feature = "native")]
3237    #[test]
3238    fn weaker_ordinary_reason_cannot_downgrade_native_cancellation() {
3239        let cx = Cx::<FullCaps>::new();
3240        let native = NativeCx::for_testing();
3241        cx.set_native_cx(native.clone());
3242
3243        cx.cancel_with_reason(CancelReason::Abort);
3244        cx.cancel_with_reason(CancelReason::Timeout);
3245
3246        assert_eq!(cx.cancel_reason(), Some(CancelReason::Abort));
3247        assert_eq!(
3248            native
3249                .cancel_reason()
3250                .expect("native reason must remain present")
3251                .kind,
3252            NativeCancelKind::ResourceUnavailable
3253        );
3254    }
3255
3256    #[cfg(feature = "native")]
3257    #[test]
3258    fn native_attachment_racing_ordinary_cancellation_never_misses_reason() {
3259        for _ in 0..128 {
3260            let cx = Cx::<FullCaps>::new();
3261            let cancel_cx = cx.clone();
3262            let attach_cx = cx.clone();
3263            let native = NativeCx::for_testing();
3264            let attached_native = native.clone();
3265            let gate = Arc::new(std::sync::Barrier::new(3));
3266            let cancel_gate = Arc::clone(&gate);
3267            let attach_gate = Arc::clone(&gate);
3268
3269            let cancel_thread = std::thread::spawn(move || {
3270                cancel_gate.wait();
3271                cancel_cx.cancel_with_reason(CancelReason::RegionClose);
3272            });
3273            let attach_thread = std::thread::spawn(move || {
3274                attach_gate.wait();
3275                attach_cx.set_native_cx(attached_native);
3276            });
3277            gate.wait();
3278            cancel_thread.join().expect("cancellation must not panic");
3279            attach_thread.join().expect("attachment must not panic");
3280
3281            assert_eq!(
3282                native
3283                    .cancel_reason()
3284                    .expect("racing attachment must observe cancellation")
3285                    .kind,
3286                NativeCancelKind::ParentCancelled
3287            );
3288        }
3289    }
3290
3291    #[cfg(feature = "native")]
3292    #[test]
3293    fn racing_native_replacements_each_synchronize_the_exact_supplied_handle() {
3294        for _ in 0..128 {
3295            let cx = Cx::<FullCaps>::new();
3296            cx.cancel_with_reason(CancelReason::RegionClose);
3297
3298            let native_a = NativeCx::for_testing();
3299            let native_b = NativeCx::for_testing();
3300            let setter_a = cx.clone();
3301            let setter_b = cx.clone();
3302            let supplied_a = native_a.clone();
3303            let supplied_b = native_b.clone();
3304            let gate = Arc::new(std::sync::Barrier::new(3));
3305            let gate_a = Arc::clone(&gate);
3306            let gate_b = Arc::clone(&gate);
3307
3308            let thread_a = std::thread::spawn(move || {
3309                gate_a.wait();
3310                setter_a.set_native_cx(supplied_a);
3311            });
3312            let thread_b = std::thread::spawn(move || {
3313                gate_b.wait();
3314                setter_b.set_native_cx(supplied_b);
3315            });
3316            gate.wait();
3317            thread_a.join().expect("first replacement must not panic");
3318            thread_b.join().expect("second replacement must not panic");
3319
3320            for native in [&native_a, &native_b] {
3321                assert_eq!(
3322                    native
3323                        .cancel_reason()
3324                        .expect("each exact supplied handle must be synchronized")
3325                        .kind,
3326                    NativeCancelKind::ParentCancelled
3327                );
3328            }
3329        }
3330    }
3331
3332    #[cfg(feature = "native")]
3333    #[test]
3334    fn fallback_creation_racing_ordinary_cancellation_never_misses_reason() {
3335        for _ in 0..128 {
3336            let cx = Cx::<FullCaps>::new();
3337            let cancel_cx = cx.clone();
3338            let fallback_cx = cx.clone();
3339            let gate = Arc::new(std::sync::Barrier::new(3));
3340            let cancel_gate = Arc::clone(&gate);
3341            let fallback_gate = Arc::clone(&gate);
3342
3343            let cancel_thread = std::thread::spawn(move || {
3344                cancel_gate.wait();
3345                cancel_cx.cancel_with_reason(CancelReason::RegionClose);
3346            });
3347            let fallback_thread = std::thread::spawn(move || {
3348                fallback_gate.wait();
3349                fallback_cx.effective_native_cx()
3350            });
3351            gate.wait();
3352            cancel_thread.join().expect("cancellation must not panic");
3353            let native = fallback_thread
3354                .join()
3355                .expect("fallback creation must not panic");
3356
3357            assert_eq!(
3358                native
3359                    .cancel_reason()
3360                    .expect("racing fallback must observe cancellation")
3361                    .kind,
3362                NativeCancelKind::ParentCancelled
3363            );
3364        }
3365    }
3366
3367    #[cfg(feature = "native")]
3368    #[test]
3369    fn ordinary_cancel_before_native_attachment_is_mirrored_after_registration() {
3370        let cx = Cx::<FullCaps>::new();
3371        cx.cancel_with_reason(CancelReason::RegionClose);
3372
3373        let native = NativeCx::for_testing();
3374        cx.set_native_cx(native.clone());
3375        let reason = native
3376            .cancel_reason()
3377            .expect("ordinary local cancellation must mirror to a later attachment");
3378        assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
3379    }
3380
3381    #[cfg(feature = "native")]
3382    #[test]
3383    fn test_cx_checkpoint_native_cx_respects_local_masking() {
3384        let cx = Cx::<FullCaps>::new();
3385        let native = NativeCx::for_testing();
3386        cx.set_native_cx(native.clone());
3387        native.set_cancel_reason(NativeCancelReason::user("cancel"));
3388
3389        {
3390            let _mask = cx.masked();
3391            assert!(cx.checkpoint().is_ok());
3392            assert!(cx.is_cancel_requested());
3393            assert_eq!(cx.cancel_state(), CancelState::CancelRequested);
3394        }
3395
3396        let err = cx.checkpoint().unwrap_err();
3397        assert_eq!(err.kind(), ErrorKind::Cancelled);
3398    }
3399
3400    #[cfg(feature = "native")]
3401    #[test]
3402    fn test_cx_effective_native_cx_uses_fallback_without_marking_explicit_attachment() {
3403        let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(7));
3404
3405        assert!(cx.attached_native_cx().is_none());
3406        let native = cx.effective_native_cx();
3407        assert!(cx.attached_native_cx().is_none());
3408        assert!(native.checkpoint().is_ok());
3409    }
3410
3411    #[cfg(feature = "native")]
3412    #[test]
3413    fn test_cx_checkpoint_without_native_context_does_not_create_fallback() {
3414        let cx = Cx::<FullCaps>::new();
3415
3416        assert!(cx.inner.fallback_native_cx.get().is_none());
3417        assert!(cx.checkpoint().is_ok());
3418        assert!(cx.inner.fallback_native_cx.get().is_none());
3419    }
3420
3421    #[cfg(feature = "native")]
3422    #[test]
3423    fn test_cx_set_native_cx_replaces_fallback_context() {
3424        let cx = Cx::<FullCaps>::new();
3425        let _ = cx.effective_native_cx();
3426
3427        let replacement = NativeCx::for_testing();
3428        cx.set_native_cx(replacement.clone());
3429        replacement.set_cancel_reason(NativeCancelReason::timeout());
3430
3431        let err = cx.checkpoint().unwrap_err();
3432        assert_eq!(err.kind(), ErrorKind::Cancelled);
3433        assert_eq!(cx.cancel_reason(), Some(CancelReason::Timeout));
3434    }
3435
3436    #[cfg(feature = "native")]
3437    #[test]
3438    fn test_create_child_copies_preexisting_cancellation_into_fallback_native_cx() {
3439        let parent = Cx::<FullCaps>::new();
3440        parent.cancel_with_reason(CancelReason::RegionClose);
3441
3442        let child = parent.create_child();
3443        let reason = child
3444            .effective_native_cx()
3445            .cancel_reason()
3446            .expect("fallback native cx should mirror inherited cancellation");
3447        assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
3448    }
3449
3450    #[cfg(feature = "native")]
3451    #[test]
3452    fn test_create_child_inherits_explicit_native_cx_attachment() {
3453        let parent = Cx::<FullCaps>::new();
3454        let native = NativeCx::for_testing();
3455        parent.set_native_cx(native.clone());
3456
3457        let child = parent.create_child();
3458        assert!(child.attached_native_cx().is_some());
3459
3460        native.set_cancel_reason(NativeCancelReason::timeout());
3461        let err = child
3462            .checkpoint()
3463            .expect_err("child should observe inherited native cancel");
3464        assert_eq!(err.kind(), ErrorKind::Cancelled);
3465        assert_eq!(child.cancel_reason(), Some(CancelReason::Timeout));
3466    }
3467
3468    #[cfg(feature = "native")]
3469    #[test]
3470    fn spawn_child_does_not_carry_a_task_affine_native_context() {
3471        let parent = Cx::<FullCaps>::new();
3472        parent.set_native_cx(NativeCx::for_testing());
3473        let child = parent.create_child_for_spawn();
3474
3475        assert!(
3476            child.attached_native_cx().is_none(),
3477            "spawn child must start without the caller task's native context"
3478        );
3479        assert!(
3480            child.inner.fallback_native_cx.get().is_none(),
3481            "spawn child must not invent a fallback context before task entry"
3482        );
3483
3484        let task_native = NativeCx::for_testing();
3485        child.set_native_cx(task_native);
3486        assert!(
3487            child.attached_native_cx().is_some(),
3488            "the spawned task must be able to attach its own native context"
3489        );
3490    }
3491
3492    #[test]
3493    fn test_budget_infinite_is_identity_for_meet() {
3494        let budget = Budget {
3495            deadline: Some(Duration::from_millis(42)),
3496            poll_quota: 500,
3497            cost_quota: Some(1000),
3498            priority: 7,
3499        };
3500        assert_eq!(budget.meet(Budget::INFINITE), budget);
3501        assert_eq!(Budget::INFINITE.meet(budget), budget);
3502    }
3503
3504    #[test]
3505    fn test_budget_none_constraints_propagate() {
3506        let a = Budget {
3507            deadline: None,
3508            poll_quota: u32::MAX,
3509            cost_quota: None,
3510            priority: 0,
3511        };
3512        let b = Budget {
3513            deadline: Some(Duration::from_millis(50)),
3514            poll_quota: 100,
3515            cost_quota: Some(500),
3516            priority: 3,
3517        };
3518        let m = a.meet(b);
3519        assert_eq!(m.deadline, Some(Duration::from_millis(50)));
3520        assert_eq!(m.poll_quota, 100);
3521        assert_eq!(m.cost_quota, Some(500));
3522        assert_eq!(m.priority, 3);
3523    }
3524
3525    #[test]
3526    fn test_cx_scope_budget_chains() {
3527        let cx = Cx::<FullCaps>::with_budget(
3528            Budget::INFINITE
3529                .with_deadline(Duration::from_millis(100))
3530                .with_poll_quota(1000),
3531        );
3532        // First scope tightens deadline.
3533        let s1 = cx.scope_with_budget(Budget::INFINITE.with_deadline(Duration::from_millis(50)));
3534        assert_eq!(s1.budget().deadline, Some(Duration::from_millis(50)));
3535        assert_eq!(s1.budget().poll_quota, 1000);
3536
3537        // Second scope tightens poll_quota further.
3538        let s2 = s1.scope_with_budget(Budget::INFINITE.with_poll_quota(200));
3539        assert_eq!(s2.budget().deadline, Some(Duration::from_millis(50)));
3540        assert_eq!(s2.budget().poll_quota, 200);
3541    }
3542
3543    fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
3544        for entry in std::fs::read_dir(dir)? {
3545            let entry = entry?;
3546            let path = entry.path();
3547            if path.is_dir() {
3548                collect_rs_files(&path, out)?;
3549            } else if path.extension().is_some_and(|ext| ext == "rs") {
3550                out.push(path);
3551            }
3552        }
3553        Ok(())
3554    }
3555
3556    fn scan_file_outside_cfg_test_items(src: &str, patterns: &[&str]) -> Vec<(usize, String)> {
3557        let mut hits = Vec::new();
3558
3559        let mut brace_depth: i32 = 0;
3560        let mut pending_cfg_test = false;
3561        let mut pending_attr_paren_depth: i32 = 0;
3562        let mut skip_until_depth: Option<i32> = None;
3563
3564        for (idx, line) in src.lines().enumerate() {
3565            let trimmed = line.trim_start();
3566            let paren_delta = i32::try_from(line.matches('(').count()).unwrap_or(i32::MAX)
3567                - i32::try_from(line.matches(')').count()).unwrap_or(i32::MAX);
3568
3569            if skip_until_depth.is_none() {
3570                // Handle single-line `#[cfg(test)]` items that open a block immediately.
3571                if trimmed.starts_with("#[cfg(test)]") && trimmed.contains('{') {
3572                    pending_cfg_test = false;
3573                    pending_attr_paren_depth = 0;
3574                    skip_until_depth = Some(brace_depth);
3575                } else if trimmed.contains("fn test_") && trimmed.contains('{') {
3576                    skip_until_depth = Some(brace_depth);
3577                } else if trimmed.starts_with("#[cfg(test)]") {
3578                    pending_cfg_test = true;
3579                    pending_attr_paren_depth = 0;
3580                } else if pending_cfg_test {
3581                    // Allow additional attributes/blank lines before the gated item.
3582                    if trimmed.starts_with("#[") || pending_attr_paren_depth > 0 {
3583                        pending_attr_paren_depth =
3584                            pending_attr_paren_depth.saturating_add(paren_delta);
3585                    } else if trimmed.is_empty() || trimmed.starts_with("//") {
3586                        // keep pending
3587                    } else if trimmed.contains('{') {
3588                        pending_cfg_test = false;
3589                        pending_attr_paren_depth = 0;
3590                        skip_until_depth = Some(brace_depth);
3591                    } else {
3592                        pending_cfg_test = false;
3593                        pending_attr_paren_depth = 0;
3594                    }
3595                } else {
3596                    for &pat in patterns {
3597                        if line.contains(pat) {
3598                            hits.push((idx + 1, pat.to_string()));
3599                        }
3600                    }
3601                }
3602            }
3603
3604            // Update brace depth (coarse; sufficient for `#[cfg(test)] mod ... {}` blocks).
3605            let opens = i32::try_from(line.matches('{').count()).unwrap_or(i32::MAX);
3606            let closes = i32::try_from(line.matches('}').count()).unwrap_or(i32::MAX);
3607            brace_depth = brace_depth.saturating_add(opens).saturating_sub(closes);
3608
3609            if let Some(until) = skip_until_depth
3610                && brace_depth <= until
3611            {
3612                skip_until_depth = None;
3613            }
3614        }
3615
3616        hits
3617    }
3618
3619    #[test]
3620    fn test_scan_file_outside_cfg_test_items_skips_cfg_test_functions_and_modules() {
3621        let src = r"
3622fn production_path() {
3623    let _ = Cx::new();
3624}
3625
3626#[cfg(test)]
3627fn test_only_helper() {
3628    let _ = Cx::new();
3629}
3630
3631#[cfg(test)]
3632mod tests {
3633    fn nested_test_helper() {
3634        let _ = Cx::default();
3635    }
3636}
3637";
3638
3639        let hits = scan_file_outside_cfg_test_items(src, &["Cx::new(", "Cx::default("]);
3640        assert_eq!(hits, vec![(3, "Cx::new(".to_string())]);
3641    }
3642
3643    #[test]
3644    fn test_no_direct_cx_constructors_in_runtime_production_code() {
3645        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3646        let repo_root = manifest_dir
3647            .parent()
3648            .and_then(Path::parent)
3649            .expect("fsqlite-types manifest dir must be crates/<name>");
3650        let crates_dir = repo_root.join("crates");
3651        let runtime_crates = [
3652            "fsqlite-core",
3653            "fsqlite-vdbe",
3654            "fsqlite-btree",
3655            "fsqlite-pager",
3656            "fsqlite-wal",
3657            "fsqlite-mvcc",
3658        ];
3659        let forbidden = ["Cx::new(", "Cx::default("];
3660
3661        let mut violations: Vec<String> = Vec::new();
3662        let mut crate_dirs: Vec<PathBuf> = Vec::new();
3663        for entry in std::fs::read_dir(&crates_dir).expect("read crates/ dir") {
3664            let entry = entry.expect("read crates/ entry");
3665            let path = entry.path();
3666            if path.is_dir() {
3667                crate_dirs.push(path);
3668            }
3669        }
3670
3671        for crate_dir in crate_dirs {
3672            let crate_name = crate_dir
3673                .file_name()
3674                .and_then(|s| s.to_str())
3675                .unwrap_or("<unknown>");
3676            if !runtime_crates.contains(&crate_name) {
3677                continue;
3678            }
3679
3680            let src_dir = crate_dir.join("src");
3681            if !src_dir.is_dir() {
3682                continue;
3683            }
3684
3685            let mut files = Vec::new();
3686            collect_rs_files(&src_dir, &mut files).expect("collect rs files");
3687
3688            for file in files {
3689                if file
3690                    .file_name()
3691                    .and_then(|name| name.to_str())
3692                    .is_some_and(|name| name.contains("test"))
3693                {
3694                    continue;
3695                }
3696
3697                let src = std::fs::read_to_string(&file).expect("read file");
3698                let rel_path = file.strip_prefix(repo_root).unwrap_or(&file);
3699
3700                for (line, pat) in scan_file_outside_cfg_test_items(&src, &forbidden) {
3701                    let line_text = src.lines().nth(line - 1).unwrap_or("").trim();
3702                    let allowed_detached_root_constructor = rel_path
3703                        == Path::new("crates/fsqlite-core/src/connection.rs")
3704                        && pat == "Cx::new("
3705                        && line_text.contains("Cx::new().with_trace_context(");
3706
3707                    if allowed_detached_root_constructor {
3708                        continue;
3709                    }
3710
3711                    violations.push(format!(
3712                        "{crate_name}:{path}:{line} uses forbidden `{pat}` outside cfg(test) code: {line_text}",
3713                        path = rel_path.display()
3714                    ));
3715                }
3716            }
3717        }
3718
3719        assert!(
3720            violations.is_empty(),
3721            "direct `Cx::new()` / `Cx::default()` production-path violations:\n{}",
3722            violations.join("\n")
3723        );
3724    }
3725
3726    #[test]
3727    fn test_ambient_authority_audit_gate() {
3728        // Scan `crates/*/src/**/*.rs` for ambient-authority usage, excluding
3729        // `#[cfg(test)]`-gated items.
3730        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3731        let repo_root = manifest_dir
3732            .parent()
3733            .and_then(Path::parent)
3734            .expect("fsqlite-types manifest dir must be crates/<name>");
3735        let crates_dir = repo_root.join("crates");
3736
3737        // Always forbidden everywhere (outside cfg(test) modules).
3738        let always_forbidden = [
3739            "SystemTime::now(",
3740            "Instant::now(",
3741            "thread_rng(",
3742            "getrandom",
3743            "std::net::",
3744            "std::thread::spawn",
3745            "tokio::spawn",
3746        ];
3747
3748        // Forbidden outside VFS boundary (outside cfg(test) modules).
3749        let non_vfs_forbidden = ["std::fs::"];
3750
3751        // Crates exempt from ambient-authority scanning:
3752        // - test infrastructure (harness, cli, e2e)
3753        // - observability (pure diagnostics, needs Instant::now for timing)
3754        // - core (needs std::fs for WAL bootstrap/MVCC key, Instant::now for tracing)
3755        // - vdbe (needs std::fs for sorter temp files, Instant::now for tracing)
3756        // - mvcc (Instant::now in flat_combining/rcu for latency metrics)
3757        // - parser (Instant::now for lexer span timing)
3758        // - planner (Instant::now for access-path selection, SystemTime for contracts)
3759        // - wal (Instant::now for checkpoint timing)
3760        // - vfs (Instant::now for VFS operation metrics, std::fs allowed by design)
3761        // - types (capability-context implementation and its test audit)
3762        // - func (SQL date/time functions are wall-clock by definition:
3763        //   datetime('now'), unixepoch(), strftime('now', ...))
3764        // - fsqlite (migration busy-retry timeout: Instant::now bounds the
3765        //   SQLITE_BUSY retry loop in apply_one)
3766        // - btree (B-tree cursor/instrumentation latency metrics)
3767        // - c-api (FFI boundary, like vfs: the C ABI shim does C-style time/file
3768        //   ops and carries its own local unsafe_code override)
3769        // - pager (pager/page-cache latency metrics + shared_file_state_key
3770        //   canonicalize; the one control-flow time use — eviction shard-probe
3771        //   start — was replaced with a deterministic round-robin, bd-w4yc9)
3772        //
3773        // The gate still guards the extension crates (fts3/fts5/rtree/json/
3774        // session/icu/misc), ast, error, and wasm, where ambient authority must
3775        // not appear. bd-w4yc9.
3776        let exempt_crates = [
3777            "fsqlite-harness",
3778            "fsqlite-cli",
3779            "fsqlite-e2e",
3780            "fsqlite-observability",
3781            "fsqlite-core",
3782            "fsqlite-vdbe",
3783            "fsqlite-mvcc",
3784            "fsqlite-parser",
3785            "fsqlite-planner",
3786            "fsqlite-wal",
3787            "fsqlite-vfs",
3788            "fsqlite-types",
3789            "fsqlite-func",
3790            "fsqlite",
3791            "fsqlite-btree",
3792            "fsqlite-c-api",
3793            "fsqlite-pager",
3794        ];
3795
3796        let mut violations: Vec<String> = Vec::new();
3797        let mut crate_dirs: Vec<PathBuf> = Vec::new();
3798        for entry in std::fs::read_dir(&crates_dir).expect("read crates/ dir") {
3799            let entry = entry.expect("read crates/ entry");
3800            let path = entry.path();
3801            if path.is_dir() {
3802                crate_dirs.push(path);
3803            }
3804        }
3805
3806        for crate_dir in crate_dirs {
3807            let crate_name = crate_dir
3808                .file_name()
3809                .and_then(|s| s.to_str())
3810                .unwrap_or("<unknown>");
3811            if exempt_crates.contains(&crate_name) {
3812                continue;
3813            }
3814            let src_dir = crate_dir.join("src");
3815            if !src_dir.is_dir() {
3816                continue;
3817            }
3818
3819            let mut files = Vec::new();
3820            collect_rs_files(&src_dir, &mut files).expect("collect rs files");
3821
3822            for file in files {
3823                let src = std::fs::read_to_string(&file).expect("read file");
3824                for (line, pat) in scan_file_outside_cfg_test_items(&src, &always_forbidden) {
3825                    violations.push(format!(
3826                        "{crate_name}:{path}:{line} uses forbidden `{pat}`",
3827                        path = file.display()
3828                    ));
3829                }
3830
3831                if crate_name != "fsqlite-vfs" {
3832                    for (line, pat) in scan_file_outside_cfg_test_items(&src, &non_vfs_forbidden) {
3833                        violations.push(format!(
3834                            "{crate_name}:{path}:{line} uses forbidden `{pat}` (non-vfs crate)",
3835                            path = file.display()
3836                        ));
3837                    }
3838                }
3839            }
3840        }
3841
3842        assert!(
3843            violations.is_empty(),
3844            "ambient authority violations (outside cfg(test) modules):\n{}",
3845            violations.join("\n")
3846        );
3847    }
3848
3849    // ===================================================================
3850    // §4.12 Cancellation Protocol Tests (bd-samf)
3851    // ===================================================================
3852
3853    const BEAD_ID: &str = "bd-samf";
3854
3855    #[test]
3856    fn test_cancel_state_machine_all_transitions() {
3857        // Test 1: State machine transitions through all 6 states.
3858        let cx = Cx::<FullCaps>::new();
3859        assert_eq!(
3860            cx.cancel_state(),
3861            CancelState::Created,
3862            "bead_id={BEAD_ID} initial_state"
3863        );
3864
3865        cx.transition_to_running();
3866        assert_eq!(
3867            cx.cancel_state(),
3868            CancelState::Running,
3869            "bead_id={BEAD_ID} after_start"
3870        );
3871
3872        cx.cancel_with_reason(CancelReason::UserInterrupt);
3873        assert_eq!(
3874            cx.cancel_state(),
3875            CancelState::CancelRequested,
3876            "bead_id={BEAD_ID} after_cancel"
3877        );
3878
3879        // Observing cancellation via checkpoint transitions to Cancelling.
3880        let err = cx.checkpoint();
3881        assert!(err.is_err(), "bead_id={BEAD_ID} checkpoint_returns_err");
3882        assert_eq!(
3883            cx.cancel_state(),
3884            CancelState::Cancelling,
3885            "bead_id={BEAD_ID} after_checkpoint_observation"
3886        );
3887
3888        cx.transition_to_finalizing();
3889        assert_eq!(
3890            cx.cancel_state(),
3891            CancelState::Finalizing,
3892            "bead_id={BEAD_ID} after_finalize_start"
3893        );
3894
3895        cx.transition_to_completed();
3896        assert_eq!(
3897            cx.cancel_state(),
3898            CancelState::Completed,
3899            "bead_id={BEAD_ID} after_complete"
3900        );
3901    }
3902
3903    #[test]
3904    fn test_cancel_propagates_to_children() {
3905        // Test 2: Cancel propagates to 3 children within one call.
3906        let parent = Cx::<FullCaps>::new();
3907        parent.transition_to_running();
3908
3909        let child1 = parent.create_child();
3910        child1.transition_to_running();
3911        let child2 = parent.create_child();
3912        child2.transition_to_running();
3913        let child3 = parent.create_child();
3914        child3.transition_to_running();
3915
3916        assert!(!child1.is_cancel_requested());
3917        assert!(!child2.is_cancel_requested());
3918        assert!(!child3.is_cancel_requested());
3919
3920        parent.cancel_with_reason(CancelReason::RegionClose);
3921
3922        // All children must see cancellation (INV-CANCEL-PROPAGATES).
3923        assert!(
3924            child1.is_cancel_requested(),
3925            "bead_id={BEAD_ID} child1_cancelled"
3926        );
3927        assert!(
3928            child2.is_cancel_requested(),
3929            "bead_id={BEAD_ID} child2_cancelled"
3930        );
3931        assert!(
3932            child3.is_cancel_requested(),
3933            "bead_id={BEAD_ID} child3_cancelled"
3934        );
3935
3936        // Children must be in CancelRequested state.
3937        assert_eq!(child1.cancel_state(), CancelState::CancelRequested);
3938        assert_eq!(child2.cancel_state(), CancelState::CancelRequested);
3939        assert_eq!(child3.cancel_state(), CancelState::CancelRequested);
3940
3941        // Reason must propagate.
3942        assert_eq!(child1.cancel_reason(), Some(CancelReason::RegionClose));
3943    }
3944
3945    #[test]
3946    fn test_dropped_children_are_pruned_from_parent_links() {
3947        let parent = Cx::<FullCaps>::new();
3948
3949        let live_child = parent.create_child();
3950        let dropped_child = parent.create_child();
3951        drop(dropped_child);
3952
3953        // Trigger propagation pass, which prunes dead weak child links.
3954        parent.cancel_with_reason(CancelReason::RegionClose);
3955
3956        let live_count = {
3957            let children = parent
3958                .inner
3959                .children
3960                .lock()
3961                .unwrap_or_else(std::sync::PoisonError::into_inner);
3962            children.iter().filter_map(Weak::upgrade).count()
3963        };
3964        assert_eq!(live_count, 1, "only the live child should remain linked");
3965        assert!(live_child.is_cancel_requested());
3966    }
3967
3968    #[test]
3969    fn dropped_children_are_pruned_before_registry_growth_without_cancellation() {
3970        let parent = Cx::<FullCaps>::new();
3971        drop(parent.create_child());
3972        let initial_capacity = parent
3973            .inner
3974            .children
3975            .lock()
3976            .unwrap_or_else(std::sync::PoisonError::into_inner)
3977            .capacity();
3978        assert!(initial_capacity > 0);
3979
3980        for _ in 0..4_096 {
3981            drop(parent.create_child());
3982        }
3983
3984        let children = parent
3985            .inner
3986            .children
3987            .lock()
3988            .unwrap_or_else(std::sync::PoisonError::into_inner);
3989        assert_eq!(
3990            children.capacity(),
3991            initial_capacity,
3992            "historical dead children must not grow an uncancelled family registry"
3993        );
3994        assert!(
3995            children.len() <= initial_capacity,
3996            "only the current bounded batch of dead weak links may remain"
3997        );
3998    }
3999
4000    #[test]
4001    fn test_cancel_idempotent_strongest_wins() {
4002        // Test 3: Strongest cancel reason wins, cannot get weaker.
4003        let cx = Cx::<FullCaps>::new();
4004        cx.transition_to_running();
4005
4006        cx.cancel_with_reason(CancelReason::Timeout);
4007        assert_eq!(
4008            cx.cancel_reason(),
4009            Some(CancelReason::Timeout),
4010            "bead_id={BEAD_ID} first_reason"
4011        );
4012
4013        // Stronger reason upgrades.
4014        cx.cancel_with_reason(CancelReason::Abort);
4015        assert_eq!(
4016            cx.cancel_reason(),
4017            Some(CancelReason::Abort),
4018            "bead_id={BEAD_ID} upgraded_reason"
4019        );
4020
4021        // Weaker reason does NOT downgrade.
4022        cx.cancel_with_reason(CancelReason::UserInterrupt);
4023        assert_eq!(
4024            cx.cancel_reason(),
4025            Some(CancelReason::Abort),
4026            "bead_id={BEAD_ID} reason_stays_strongest"
4027        );
4028    }
4029
4030    #[test]
4031    fn test_losers_drain_on_race() {
4032        // Test 4: Simulate race combinator — loser with obligation resolves
4033        // before race returns.
4034        use std::sync::atomic::AtomicBool;
4035
4036        let loser_cx = Cx::<FullCaps>::new();
4037        loser_cx.transition_to_running();
4038
4039        // Simulate an obligation on the loser.
4040        let obligation_resolved = Arc::new(AtomicBool::new(false));
4041        let ob_clone = Arc::clone(&obligation_resolved);
4042
4043        // Winner finishes → cancel loser.
4044        loser_cx.cancel_with_reason(CancelReason::RegionClose);
4045
4046        // Loser observes cancellation at next checkpoint.
4047        assert!(loser_cx.checkpoint().is_err());
4048        assert_eq!(loser_cx.cancel_state(), CancelState::Cancelling);
4049
4050        // Loser drains: resolves obligation.
4051        ob_clone.store(true, Ordering::Release);
4052        loser_cx.transition_to_finalizing();
4053        loser_cx.transition_to_completed();
4054
4055        assert!(
4056            obligation_resolved.load(Ordering::Acquire),
4057            "bead_id={BEAD_ID} loser_obligation_resolved"
4058        );
4059        assert_eq!(
4060            loser_cx.cancel_state(),
4061            CancelState::Completed,
4062            "bead_id={BEAD_ID} loser_drained"
4063        );
4064    }
4065
4066    #[test]
4067    fn test_vdbe_checkpoint_cancel_observed_at_next_opcode() {
4068        // Test 5: Simulate VDBE opcode loop — cancel after opcode 50,
4069        // observed at opcode 51.
4070        let cx = Cx::<FullCaps>::new();
4071        cx.transition_to_running();
4072
4073        let mut last_executed = 0u32;
4074        for opcode in 0..100u32 {
4075            // Checkpoint at start of each opcode.
4076            if cx.checkpoint_with(format!("vdbe pc={opcode}")).is_err() {
4077                last_executed = opcode;
4078                break;
4079            }
4080            // Execute opcode.
4081            last_executed = opcode;
4082            // Cancel arrives at end of opcode 50.
4083            if opcode == 50 {
4084                cx.cancel_with_reason(CancelReason::UserInterrupt);
4085            }
4086        }
4087
4088        assert_eq!(
4089            last_executed, 51,
4090            "bead_id={BEAD_ID} cancel_observed_at_opcode_51"
4091        );
4092    }
4093
4094    #[test]
4095    fn test_btree_checkpoint_cancel_within_one_node() {
4096        // Test 6: Simulate B-tree descent — cancel mid-descent, observed
4097        // within 1 node visit.
4098        let cx = Cx::<FullCaps>::new();
4099        cx.transition_to_running();
4100
4101        let nodes = ["root", "internal_l", "internal_r", "leaf_a", "leaf_b"];
4102        let cancel_at = 2; // Cancel after visiting internal_r.
4103        let mut observed_at = None;
4104
4105        for (i, node) in nodes.iter().enumerate() {
4106            // Checkpoint at start of each node visit.
4107            if cx.checkpoint_with(format!("btree node={node}")).is_err() {
4108                observed_at = Some(i);
4109                break;
4110            }
4111            // Visit node.
4112            // Cancel arrives after visiting node at index cancel_at.
4113            if i == cancel_at {
4114                cx.cancel_with_reason(CancelReason::UserInterrupt);
4115            }
4116        }
4117
4118        assert_eq!(
4119            observed_at,
4120            Some(cancel_at + 1),
4121            "bead_id={BEAD_ID} btree_cancel_within_one_node"
4122        );
4123    }
4124
4125    #[test]
4126    fn test_masked_section_defers_cancel() {
4127        // Test 7: Masked section defers cancel — checkpoint returns Ok inside
4128        // mask, Err after exit.
4129        let cx = Cx::<FullCaps>::new();
4130        cx.transition_to_running();
4131
4132        cx.cancel_with_reason(CancelReason::UserInterrupt);
4133        assert!(cx.is_cancel_requested());
4134
4135        // Enter masked section.
4136        {
4137            let _guard = cx.masked();
4138            assert_eq!(cx.mask_depth(), 1);
4139
4140            // Inside mask, checkpoint succeeds despite cancellation.
4141            assert!(
4142                cx.checkpoint().is_ok(),
4143                "bead_id={BEAD_ID} checkpoint_ok_while_masked"
4144            );
4145
4146            // Nested mask.
4147            {
4148                let _inner = cx.masked();
4149                assert_eq!(cx.mask_depth(), 2);
4150                assert!(cx.checkpoint().is_ok());
4151            }
4152            assert_eq!(cx.mask_depth(), 1);
4153        }
4154        assert_eq!(cx.mask_depth(), 0);
4155
4156        // After mask exit, checkpoint observes cancellation.
4157        assert!(
4158            cx.checkpoint().is_err(),
4159            "bead_id={BEAD_ID} checkpoint_err_after_mask_exit"
4160        );
4161    }
4162
4163    #[test]
4164    #[should_panic(expected = "MAX_MASK_DEPTH")]
4165    #[allow(clippy::collection_is_never_read)]
4166    fn test_max_mask_depth_exceeded_panics() {
4167        // Test 8: MAX_MASK_DEPTH=64 exceeded panics in lab mode.
4168        let cx = Cx::<FullCaps>::new();
4169        let mut guards = Vec::new();
4170        for _ in 0..MAX_MASK_DEPTH {
4171            guards.push(cx.masked());
4172        }
4173        // This 65th mask should panic.
4174        let _overflow = cx.masked();
4175    }
4176
4177    #[test]
4178    fn test_commit_section_completes_under_cancel() {
4179        // Test 9: Cancel after op 1 of 3, all 3 complete + finalizers run.
4180        let cx = Cx::<FullCaps>::new();
4181        cx.transition_to_running();
4182
4183        let ops_completed = Arc::new(AtomicU32::new(0));
4184        let finalizer_ran = Arc::new(AtomicBool::new(false));
4185
4186        let ops = Arc::clone(&ops_completed);
4187        let fin = Arc::clone(&finalizer_ran);
4188
4189        cx.commit_section(
4190            10,
4191            |ctx| {
4192                // Op 1.
4193                assert!(ctx.tick());
4194                ops.fetch_add(1, Ordering::Release);
4195
4196                // Cancel mid-section.
4197                cx.cancel_with_reason(CancelReason::UserInterrupt);
4198
4199                // Op 2: still succeeds because commit section is masked.
4200                assert!(ctx.tick());
4201                ops.fetch_add(1, Ordering::Release);
4202                assert!(
4203                    cx.checkpoint().is_ok(),
4204                    "bead_id={BEAD_ID} masked_during_commit"
4205                );
4206
4207                // Op 3.
4208                assert!(ctx.tick());
4209                ops.fetch_add(1, Ordering::Release);
4210            },
4211            move || {
4212                fin.store(true, Ordering::Release);
4213            },
4214        );
4215
4216        assert_eq!(
4217            ops_completed.load(Ordering::Acquire),
4218            3,
4219            "bead_id={BEAD_ID} all_ops_completed"
4220        );
4221        assert!(
4222            finalizer_ran.load(Ordering::Acquire),
4223            "bead_id={BEAD_ID} finalizer_ran"
4224        );
4225
4226        // After commit section, masking is removed — checkpoint should fail.
4227        assert!(cx.checkpoint().is_err());
4228    }
4229
4230    #[test]
4231    fn test_commit_section_enforces_poll_quota() {
4232        // Test 10: Commit section poll quota is bounded.
4233        let cx = Cx::<FullCaps>::new();
4234        cx.transition_to_running();
4235
4236        let ticks_succeeded = Arc::new(AtomicU32::new(0));
4237        let ts = Arc::clone(&ticks_succeeded);
4238
4239        cx.commit_section(
4240            3,
4241            |ctx| {
4242                assert_eq!(ctx.poll_remaining(), 3);
4243                for _ in 0..5 {
4244                    if ctx.tick() {
4245                        ts.fetch_add(1, Ordering::Release);
4246                    }
4247                }
4248            },
4249            || {},
4250        );
4251
4252        assert_eq!(
4253            ticks_succeeded.load(Ordering::Acquire),
4254            3,
4255            "bead_id={BEAD_ID} poll_quota_enforced"
4256        );
4257    }
4258
4259    #[test]
4260    fn test_cancel_unaware_hot_loop_detected() {
4261        // Test 11: Simulate harness detecting a hot loop that never
4262        // calls checkpoint.
4263        let cx = Cx::<FullCaps>::new();
4264        cx.transition_to_running();
4265
4266        // Harness deadline: if 100 iterations pass without checkpoint,
4267        // the loop is cancel-unaware.
4268        let deadline = 100u32;
4269        let mut iterations_without_checkpoint = 0u32;
4270        let mut detected_unaware = false;
4271
4272        cx.cancel_with_reason(CancelReason::UserInterrupt);
4273
4274        for _i in 0..200u32 {
4275            iterations_without_checkpoint += 1;
4276            if iterations_without_checkpoint >= deadline {
4277                detected_unaware = true;
4278                break;
4279            }
4280            // Bug: no cx.checkpoint() call in the loop body.
4281        }
4282
4283        assert!(
4284            detected_unaware,
4285            "bead_id={BEAD_ID} cancel_unaware_loop_detected"
4286        );
4287
4288        // Contrast: a compliant loop would checkpoint and exit.
4289        let cx2 = Cx::<FullCaps>::new();
4290        cx2.transition_to_running();
4291        cx2.cancel_with_reason(CancelReason::UserInterrupt);
4292        let mut compliant_iters = 0u32;
4293        for _ in 0..200u32 {
4294            if cx2.checkpoint().is_err() {
4295                break;
4296            }
4297            compliant_iters += 1;
4298        }
4299        assert_eq!(
4300            compliant_iters, 0,
4301            "bead_id={BEAD_ID} compliant_loop_exits_immediately"
4302        );
4303    }
4304
4305    #[test]
4306    fn test_write_coordinator_commit_section() {
4307        // Test 12: Simulate WriteCoordinator — cancel mid-publish,
4308        // proof+marker completes atomically via commit section.
4309        let cx = Cx::<FullCaps>::new();
4310        cx.transition_to_running();
4311
4312        let proof_published = Arc::new(AtomicBool::new(false));
4313        let marker_published = Arc::new(AtomicBool::new(false));
4314        let reservation_released = Arc::new(AtomicBool::new(false));
4315
4316        let proof = Arc::clone(&proof_published);
4317        let marker = Arc::clone(&marker_published);
4318        let release = Arc::clone(&reservation_released);
4319
4320        cx.commit_section(
4321            10,
4322            |ctx| {
4323                // Step 1: FCW validation passed, commit_seq allocated.
4324                assert!(ctx.tick());
4325
4326                // Cancel arrives mid-publish.
4327                cx.cancel_with_reason(CancelReason::RegionClose);
4328
4329                // Step 2: Publish proof (must complete).
4330                assert!(ctx.tick());
4331                proof.store(true, Ordering::Release);
4332                // Checkpoint inside commit section succeeds (masked).
4333                assert!(cx.checkpoint().is_ok());
4334
4335                // Step 3: Publish marker (must complete).
4336                assert!(ctx.tick());
4337                marker.store(true, Ordering::Release);
4338            },
4339            move || {
4340                // Finalizer: release reservation.
4341                release.store(true, Ordering::Release);
4342            },
4343        );
4344
4345        assert!(
4346            proof_published.load(Ordering::Acquire),
4347            "bead_id={BEAD_ID} proof_published"
4348        );
4349        assert!(
4350            marker_published.load(Ordering::Acquire),
4351            "bead_id={BEAD_ID} marker_published"
4352        );
4353        assert!(
4354            reservation_released.load(Ordering::Acquire),
4355            "bead_id={BEAD_ID} reservation_released"
4356        );
4357
4358        // After commit section, cancellation is visible.
4359        assert!(cx.checkpoint().is_err());
4360    }
4361
4362    // ===================================================================
4363    // Tracing ID propagation tests (bd-2g5.6)
4364    // ===================================================================
4365
4366    #[test]
4367    fn test_trace_ids_default_to_zero() {
4368        let cx = Cx::<FullCaps>::new();
4369        assert_eq!(cx.trace_id(), 0);
4370        assert_eq!(cx.decision_id(), 0);
4371        assert_eq!(cx.policy_id(), 0);
4372    }
4373
4374    #[test]
4375    fn test_with_trace_context_sets_all_ids() {
4376        let cx = Cx::<FullCaps>::new().with_trace_context(42, 99, 7);
4377        assert_eq!(cx.trace_id(), 42);
4378        assert_eq!(cx.decision_id(), 99);
4379        assert_eq!(cx.policy_id(), 7);
4380    }
4381
4382    #[test]
4383    fn test_with_decision_id_preserves_other_ids() {
4384        let cx = Cx::<FullCaps>::new()
4385            .with_trace_context(10, 20, 30)
4386            .with_decision_id(55);
4387        assert_eq!(cx.trace_id(), 10);
4388        assert_eq!(cx.decision_id(), 55);
4389        assert_eq!(cx.policy_id(), 30);
4390    }
4391
4392    #[test]
4393    fn test_with_policy_id_preserves_other_ids() {
4394        let cx = Cx::<FullCaps>::new()
4395            .with_trace_context(100, 200, 300)
4396            .with_policy_id(88);
4397        assert_eq!(cx.trace_id(), 100);
4398        assert_eq!(cx.decision_id(), 200);
4399        assert_eq!(cx.policy_id(), 88);
4400    }
4401
4402    #[test]
4403    #[allow(clippy::redundant_clone)]
4404    fn test_clone_propagates_trace_ids() {
4405        let cx = Cx::<FullCaps>::new().with_trace_context(1, 2, 3);
4406        let cloned = cx.clone();
4407        assert_eq!(cloned.trace_id(), 1);
4408        assert_eq!(cloned.decision_id(), 2);
4409        assert_eq!(cloned.policy_id(), 3);
4410    }
4411
4412    #[test]
4413    fn test_restrict_propagates_trace_ids() {
4414        let cx = Cx::<FullCaps>::new();
4415        let compute = cx.restrict::<ComputeCaps>();
4416        assert_eq!(compute.trace_id(), 0);
4417        assert_eq!(compute.decision_id(), 0);
4418        assert_eq!(compute.policy_id(), 0);
4419    }
4420
4421    #[test]
4422    fn test_scope_with_budget_propagates_trace_ids() {
4423        let cx = Cx::<FullCaps>::new().with_trace_context(5, 6, 7);
4424        let scoped = cx.scope_with_budget(Budget::MINIMAL);
4425        assert_eq!(scoped.trace_id(), 5);
4426        assert_eq!(scoped.decision_id(), 6);
4427        assert_eq!(scoped.policy_id(), 7);
4428        // Budget should be tightened.
4429        assert_eq!(scoped.budget().poll_quota, Budget::MINIMAL.poll_quota);
4430    }
4431
4432    #[test]
4433    fn test_cleanup_scope_propagates_trace_ids() {
4434        let cx = Cx::<FullCaps>::new().with_trace_context(11, 22, 33);
4435        let cleanup = cx.cleanup_scope();
4436        assert_eq!(cleanup.trace_id(), 11);
4437        assert_eq!(cleanup.decision_id(), 22);
4438        assert_eq!(cleanup.policy_id(), 33);
4439    }
4440
4441    #[test]
4442    fn test_create_child_propagates_trace_ids() {
4443        let parent = Cx::<FullCaps>::new().with_trace_context(50, 60, 70);
4444        let child = parent.create_child();
4445        assert_eq!(child.trace_id(), 50);
4446        assert_eq!(child.decision_id(), 60);
4447        assert_eq!(child.policy_id(), 70);
4448        // Child should have independent cancellation.
4449        parent.cancel();
4450        assert!(parent.is_cancel_requested());
4451        assert!(child.is_cancel_requested()); // Propagated.
4452    }
4453
4454    #[test]
4455    fn test_trace_ids_independent_across_children() {
4456        let parent = Cx::<FullCaps>::new().with_trace_context(1, 2, 3);
4457        let child1 = parent.create_child().with_decision_id(100);
4458        let child2 = parent.create_child().with_decision_id(200);
4459        // Children share trace_id but have different decision_ids.
4460        assert_eq!(child1.trace_id(), 1);
4461        assert_eq!(child2.trace_id(), 1);
4462        assert_eq!(child1.decision_id(), 100);
4463        assert_eq!(child2.decision_id(), 200);
4464        // Parent's decision_id unchanged.
4465        assert_eq!(parent.decision_id(), 2);
4466    }
4467
4468    #[test]
4469    fn test_with_budget_starts_at_zero_trace_ids() {
4470        let cx = Cx::<FullCaps>::with_budget(Budget::MINIMAL);
4471        assert_eq!(cx.trace_id(), 0);
4472        assert_eq!(cx.decision_id(), 0);
4473        assert_eq!(cx.policy_id(), 0);
4474    }
4475    // === bd-bjm5d: blocking_io_inline_safe marker ===
4476
4477    #[test]
4478    fn blocking_io_inline_safe_defaults_false() {
4479        let cx = Cx::new();
4480        assert!(!cx.blocking_io_inline_safe());
4481    }
4482
4483    #[test]
4484    fn blocking_io_inline_safe_shared_through_clone() {
4485        let cx = Cx::new();
4486        let clone = cx.clone();
4487        cx.mark_blocking_io_inline_safe();
4488        assert!(clone.blocking_io_inline_safe());
4489    }
4490
4491    #[test]
4492    fn blocking_io_inline_safe_inherited_by_create_child() {
4493        let cx = Cx::new();
4494        cx.mark_blocking_io_inline_safe();
4495        let child = cx.create_child();
4496        assert!(child.blocking_io_inline_safe());
4497    }
4498
4499    #[test]
4500    fn blocking_io_inline_safe_not_invented_by_child_of_unset_parent() {
4501        let cx = Cx::new();
4502        let child = cx.create_child();
4503        assert!(!child.blocking_io_inline_safe());
4504        // Marking the child later must not leak back to the parent: the
4505        // child has its own CxInner.
4506        child.mark_blocking_io_inline_safe();
4507        assert!(!cx.blocking_io_inline_safe());
4508    }
4509}