Skip to main content

asupersync/trace/
event.rs

1//! Trace events and data types.
2//!
3//! Each event in the trace represents an observable action in the runtime.
4//! Events carry sufficient information for replay and analysis.
5
6use crate::monitor::DownReason;
7use crate::record::{ObligationAbortReason, ObligationKind, ObligationState};
8use crate::trace::distributed::LogicalTime;
9use crate::types::{CancelReason, ObligationId, PanicPayload, RegionId, TaskId, Time};
10use core::fmt;
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13
14/// Current schema version for trace events.
15pub const TRACE_EVENT_SCHEMA_VERSION: u32 = 1;
16/// Browser trace contract schema version.
17pub const BROWSER_TRACE_SCHEMA_VERSION: &str = "browser-trace-schema-v1";
18const MAX_BROWSER_TRACE_ATTRIBUTE_BYTES: usize = 128;
19
20/// Browser trace event category for deterministic diagnostics.
21#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[serde(rename_all = "snake_case")]
23pub enum BrowserTraceCategory {
24    /// Scheduler decisions and task lifecycle.
25    Scheduler,
26    /// Timer and virtual-time transitions.
27    Timer,
28    /// Host callback and host-signal integration events.
29    HostCallback,
30    /// Capability/authority mediated runtime effects.
31    CapabilityInvocation,
32    /// Cancellation protocol transitions.
33    CancellationTransition,
34}
35
36/// One browser-trace event taxonomy entry.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
38pub struct BrowserTraceEventSpec {
39    /// Stable event-kind identifier.
40    pub event_kind: String,
41    /// High-level event category.
42    pub category: BrowserTraceCategory,
43    /// Required event data fields in lexical order.
44    pub required_fields: Vec<String>,
45    /// Fields that must be redacted in browser-friendly logs.
46    pub redacted_fields: Vec<String>,
47}
48
49/// Browser trace schema compatibility policy.
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51pub struct BrowserTraceCompatibility {
52    /// Oldest reader version that must be supported.
53    pub minimum_reader_version: String,
54    /// Supported reader versions in lexical order.
55    pub supported_reader_versions: Vec<String>,
56    /// Legacy aliases that decode into v1 semantics.
57    pub backward_decode_aliases: Vec<String>,
58}
59
60/// Browser trace schema v1 contract for deterministic diagnostics and replay.
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub struct BrowserTraceSchema {
63    /// Contract version identifier.
64    pub schema_version: String,
65    /// Required envelope metadata fields in lexical order.
66    pub required_envelope_fields: Vec<String>,
67    /// Required ordering semantics in lexical order.
68    pub ordering_semantics: Vec<String>,
69    /// Required structured-log fields for trace diagnostics.
70    pub structured_log_required_fields: Vec<String>,
71    /// Validation-failure categories in lexical order.
72    pub validation_failure_categories: Vec<String>,
73    /// Canonical event taxonomy in lexical `event_kind` order.
74    pub event_specs: Vec<BrowserTraceEventSpec>,
75    /// Compatibility policy for readers/writers.
76    pub compatibility: BrowserTraceCompatibility,
77}
78
79/// Browser capture source for deterministic replay reconstruction.
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
81#[serde(rename_all = "snake_case")]
82pub enum BrowserCaptureSource {
83    /// Runtime-originated event without explicit host sample metadata.
84    Runtime,
85    /// Host-time sample capture.
86    Time,
87    /// Host callback/event-loop sample capture.
88    Event,
89    /// External host input capture (user/input/network-originated trigger).
90    HostInput,
91}
92
93/// Deterministic capture metadata for browser trace events.
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
95pub struct BrowserCaptureMetadata {
96    /// Monotonic host turn sequence provided by the browser adapter.
97    pub host_turn_seq: u64,
98    /// Capture source class.
99    pub source: BrowserCaptureSource,
100    /// Monotonic source-local sequence number.
101    pub source_seq: u64,
102    /// Host time sample in nanoseconds (`performance.now()`-derived).
103    pub host_time_ns: u64,
104}
105
106/// The kind of trace event.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum TraceEventKind {
110    /// A task was spawned.
111    Spawn,
112    /// A task was scheduled for execution.
113    Schedule,
114    /// A task voluntarily yielded.
115    Yield,
116    /// A task was woken by a waker.
117    Wake,
118    /// A task was polled.
119    Poll,
120    /// A task completed.
121    Complete,
122    /// Cancellation was requested.
123    CancelRequest,
124    /// Cancellation was acknowledged.
125    CancelAck,
126    /// Worker-offload cancellation was requested across the browser boundary.
127    WorkerCancelRequested,
128    /// Worker-offload cancellation was acknowledged by the worker coordinator.
129    WorkerCancelAcknowledged,
130    /// Worker-offload drain phase started after cancellation acknowledgement.
131    WorkerDrainStarted,
132    /// Worker-offload drain phase completed.
133    WorkerDrainCompleted,
134    /// Worker-offload finalize phase completed.
135    WorkerFinalizeCompleted,
136    /// A region began closing.
137    RegionCloseBegin,
138    /// A region completed closing.
139    RegionCloseComplete,
140    /// A region was created.
141    RegionCreated,
142    /// A region received a cancellation request.
143    RegionCancelled,
144    /// An obligation was reserved.
145    ObligationReserve,
146    /// An obligation was committed.
147    ObligationCommit,
148    /// An obligation was aborted.
149    ObligationAbort,
150    /// An obligation was leaked (error).
151    ObligationLeak,
152    /// Time advanced.
153    TimeAdvance,
154    /// A timer was scheduled.
155    TimerScheduled,
156    /// A timer fired.
157    TimerFired,
158    /// A timer was cancelled.
159    TimerCancelled,
160    /// I/O interest was requested.
161    IoRequested,
162    /// I/O became ready.
163    IoReady,
164    /// I/O result (bytes transferred).
165    IoResult,
166    /// I/O error injected/observed.
167    IoError,
168    /// RNG was seeded.
169    RngSeed,
170    /// RNG value generated.
171    RngValue,
172    /// Replay checkpoint event.
173    Checkpoint,
174    /// A task held obligations but stopped being polled (futurelock).
175    FuturelockDetected,
176    /// Chaos injection occurred.
177    ChaosInjection,
178    /// User-defined trace point.
179    UserTrace,
180    /// A monitor was established.
181    MonitorCreated,
182    /// A monitor was removed.
183    MonitorDropped,
184    /// A Down notification was delivered.
185    DownDelivered,
186    /// A link was established.
187    LinkCreated,
188    /// A link was removed.
189    LinkDropped,
190    /// An exit signal was delivered to a linked task.
191    ExitDelivered,
192    /// A spawn request was enqueued onto the spawn mailbox (pre-admission).
193    TaskSpawnEnqueued,
194    /// A mailbox spawn request was admitted into its region (task created).
195    TaskAdmitted,
196    /// A server request region installed its request budget (per-request
197    /// deadline / poll / cost quota minted at the runtime boundary).
198    BudgetInstalled,
199    /// A server request region's budget was consumed/resolved (the request
200    /// finished on some path, including the force-closed backstop).
201    BudgetConsumed,
202}
203
204impl TraceEventKind {
205    /// Canonical list of all trace event kinds.
206    ///
207    /// Keep this list in sync with the enum definition and
208    /// `docs/spork_deterministic_ordering.md` taxonomy section.
209    pub const ALL: [Self; 45] = [
210        Self::Spawn,
211        Self::Schedule,
212        Self::Yield,
213        Self::Wake,
214        Self::Poll,
215        Self::Complete,
216        Self::CancelRequest,
217        Self::CancelAck,
218        Self::WorkerCancelRequested,
219        Self::WorkerCancelAcknowledged,
220        Self::WorkerDrainStarted,
221        Self::WorkerDrainCompleted,
222        Self::WorkerFinalizeCompleted,
223        Self::RegionCloseBegin,
224        Self::RegionCloseComplete,
225        Self::RegionCreated,
226        Self::RegionCancelled,
227        Self::ObligationReserve,
228        Self::ObligationCommit,
229        Self::ObligationAbort,
230        Self::ObligationLeak,
231        Self::TimeAdvance,
232        Self::TimerScheduled,
233        Self::TimerFired,
234        Self::TimerCancelled,
235        Self::IoRequested,
236        Self::IoReady,
237        Self::IoResult,
238        Self::IoError,
239        Self::RngSeed,
240        Self::RngValue,
241        Self::Checkpoint,
242        Self::FuturelockDetected,
243        Self::ChaosInjection,
244        Self::UserTrace,
245        Self::MonitorCreated,
246        Self::MonitorDropped,
247        Self::DownDelivered,
248        Self::LinkCreated,
249        Self::LinkDropped,
250        Self::ExitDelivered,
251        Self::TaskSpawnEnqueued,
252        Self::TaskAdmitted,
253        Self::BudgetInstalled,
254        Self::BudgetConsumed,
255    ];
256
257    /// Stable, grep-friendly taxonomy name.
258    #[must_use]
259    pub const fn stable_name(self) -> &'static str {
260        match self {
261            Self::Spawn => "spawn",
262            Self::Schedule => "schedule",
263            Self::Yield => "yield",
264            Self::Wake => "wake",
265            Self::Poll => "poll",
266            Self::Complete => "complete",
267            Self::CancelRequest => "cancel_request",
268            Self::CancelAck => "cancel_ack",
269            Self::WorkerCancelRequested => "worker_cancel_requested",
270            Self::WorkerCancelAcknowledged => "worker_cancel_acknowledged",
271            Self::WorkerDrainStarted => "worker_drain_started",
272            Self::WorkerDrainCompleted => "worker_drain_completed",
273            Self::WorkerFinalizeCompleted => "worker_finalize_completed",
274            Self::RegionCloseBegin => "region_close_begin",
275            Self::RegionCloseComplete => "region_close_complete",
276            Self::RegionCreated => "region_created",
277            Self::RegionCancelled => "region_cancelled",
278            Self::ObligationReserve => "obligation_reserve",
279            Self::ObligationCommit => "obligation_commit",
280            Self::ObligationAbort => "obligation_abort",
281            Self::ObligationLeak => "obligation_leak",
282            Self::TimeAdvance => "time_advance",
283            Self::TimerScheduled => "timer_scheduled",
284            Self::TimerFired => "timer_fired",
285            Self::TimerCancelled => "timer_cancelled",
286            Self::IoRequested => "io_requested",
287            Self::IoReady => "io_ready",
288            Self::IoResult => "io_result",
289            Self::IoError => "io_error",
290            Self::RngSeed => "rng_seed",
291            Self::RngValue => "rng_value",
292            Self::Checkpoint => "checkpoint",
293            Self::FuturelockDetected => "futurelock_detected",
294            Self::ChaosInjection => "chaos_injection",
295            Self::UserTrace => "user_trace",
296            Self::MonitorCreated => "monitor_created",
297            Self::MonitorDropped => "monitor_dropped",
298            Self::DownDelivered => "down_delivered",
299            Self::LinkCreated => "link_created",
300            Self::LinkDropped => "link_dropped",
301            Self::ExitDelivered => "exit_delivered",
302            Self::TaskSpawnEnqueued => "task_spawn_enqueued",
303            Self::TaskAdmitted => "task_admitted",
304            Self::BudgetInstalled => "budget_installed",
305            Self::BudgetConsumed => "budget_consumed",
306        }
307    }
308
309    /// Stable required field set for taxonomy documentation.
310    #[must_use]
311    pub const fn required_fields(self) -> &'static str {
312        match self {
313            Self::Spawn
314            | Self::Schedule
315            | Self::Yield
316            | Self::Wake
317            | Self::Poll
318            | Self::Complete
319            | Self::TaskSpawnEnqueued
320            | Self::TaskAdmitted => "task, region",
321            Self::CancelRequest | Self::CancelAck => "task, region, reason",
322            Self::WorkerCancelRequested
323            | Self::WorkerCancelAcknowledged
324            | Self::WorkerDrainStarted
325            | Self::WorkerDrainCompleted
326            | Self::WorkerFinalizeCompleted => {
327                "decision_seq, job_id, obligation, region, replay_hash, task, worker_id"
328            }
329            Self::RegionCloseBegin | Self::RegionCloseComplete | Self::RegionCreated => {
330                "region, parent"
331            }
332            Self::RegionCancelled => "region, reason",
333            Self::ObligationReserve => "obligation, task, region, kind, state",
334            Self::ObligationCommit | Self::ObligationLeak => {
335                "obligation, task, region, kind, state, duration_ns"
336            }
337            Self::ObligationAbort => {
338                "obligation, task, region, kind, state, duration_ns, abort_reason"
339            }
340            Self::TimeAdvance => "old, new",
341            Self::TimerScheduled => "timer_id, deadline",
342            Self::TimerFired | Self::TimerCancelled => "timer_id",
343            Self::IoRequested => "token, interest",
344            Self::IoReady => "token, readiness",
345            Self::IoResult => "token, bytes",
346            Self::IoError => "token, kind",
347            Self::RngSeed => "seed",
348            Self::RngValue => "value",
349            Self::Checkpoint => "sequence, active_tasks, active_regions",
350            Self::FuturelockDetected => "task, region, idle_steps, held",
351            Self::ChaosInjection => "kind, task, detail",
352            Self::UserTrace => "message",
353            Self::MonitorCreated | Self::MonitorDropped => {
354                "monitor_ref, watcher, watcher_region, monitored"
355            }
356            Self::DownDelivered => "monitor_ref, watcher, monitored, completion_vt, reason",
357            Self::LinkCreated | Self::LinkDropped => "link_ref, task_a, region_a, task_b, region_b",
358            Self::ExitDelivered => "link_ref, from, to, failure_vt, reason",
359            Self::BudgetInstalled => {
360                "task, region, protocol, deadline_ns, poll_quota, cost_quota, priority, source"
361            }
362            Self::BudgetConsumed => "task, region, protocol, deadline_ns, elapsed_ns, outcome",
363        }
364    }
365}
366
367/// Returns the browser trace category for one trace event kind.
368#[must_use]
369pub const fn browser_trace_category_for_kind(kind: TraceEventKind) -> BrowserTraceCategory {
370    match kind {
371        TraceEventKind::Spawn
372        | TraceEventKind::Schedule
373        | TraceEventKind::Yield
374        | TraceEventKind::Wake
375        | TraceEventKind::Poll
376        | TraceEventKind::Complete
377        | TraceEventKind::Checkpoint
378        | TraceEventKind::FuturelockDetected
379        | TraceEventKind::TaskSpawnEnqueued
380        | TraceEventKind::TaskAdmitted
381        | TraceEventKind::BudgetInstalled
382        | TraceEventKind::BudgetConsumed => BrowserTraceCategory::Scheduler,
383        TraceEventKind::TimeAdvance
384        | TraceEventKind::TimerScheduled
385        | TraceEventKind::TimerFired
386        | TraceEventKind::TimerCancelled => BrowserTraceCategory::Timer,
387        TraceEventKind::IoRequested
388        | TraceEventKind::IoReady
389        | TraceEventKind::IoResult
390        | TraceEventKind::IoError
391        | TraceEventKind::RngSeed
392        | TraceEventKind::RngValue
393        | TraceEventKind::UserTrace
394        | TraceEventKind::ChaosInjection => BrowserTraceCategory::HostCallback,
395        TraceEventKind::ObligationReserve
396        | TraceEventKind::ObligationCommit
397        | TraceEventKind::ObligationAbort
398        | TraceEventKind::ObligationLeak
399        | TraceEventKind::RegionCreated
400        | TraceEventKind::MonitorCreated
401        | TraceEventKind::MonitorDropped
402        | TraceEventKind::DownDelivered
403        | TraceEventKind::LinkCreated
404        | TraceEventKind::LinkDropped
405        | TraceEventKind::ExitDelivered => BrowserTraceCategory::CapabilityInvocation,
406        TraceEventKind::CancelRequest
407        | TraceEventKind::CancelAck
408        | TraceEventKind::WorkerCancelRequested
409        | TraceEventKind::WorkerCancelAcknowledged
410        | TraceEventKind::WorkerDrainStarted
411        | TraceEventKind::WorkerDrainCompleted
412        | TraceEventKind::WorkerFinalizeCompleted
413        | TraceEventKind::RegionCloseBegin
414        | TraceEventKind::RegionCloseComplete
415        | TraceEventKind::RegionCancelled => BrowserTraceCategory::CancellationTransition,
416    }
417}
418
419/// Returns stable snake_case category name for structured logs.
420#[must_use]
421pub const fn browser_trace_category_name(category: BrowserTraceCategory) -> &'static str {
422    match category {
423        BrowserTraceCategory::Scheduler => "scheduler",
424        BrowserTraceCategory::Timer => "timer",
425        BrowserTraceCategory::HostCallback => "host_callback",
426        BrowserTraceCategory::CapabilityInvocation => "capability_invocation",
427        BrowserTraceCategory::CancellationTransition => "cancellation_transition",
428    }
429}
430
431fn redacted_fields_for_kind(kind: TraceEventKind) -> Vec<String> {
432    match kind {
433        TraceEventKind::UserTrace => vec!["message".to_string()],
434        TraceEventKind::ChaosInjection => vec!["detail".to_string()],
435        _ => Vec::new(),
436    }
437}
438
439fn split_required_fields_csv(csv: &str) -> Vec<String> {
440    let mut fields = csv
441        .split(',')
442        .map(str::trim)
443        .filter(|value| !value.is_empty())
444        .map(ToString::to_string)
445        .collect::<Vec<_>>();
446    sort_and_dedup_strings(&mut fields);
447    fields
448}
449
450fn sort_and_dedup_strings(values: &mut Vec<String>) {
451    values.sort();
452    values.dedup();
453}
454
455fn trace_event_kind_from_stable_name(name: &str) -> Option<TraceEventKind> {
456    TraceEventKind::ALL
457        .iter()
458        .copied()
459        .find(|kind| kind.stable_name() == name)
460}
461
462fn validate_lexical_string_set(values: &[String], field: &str) -> Result<(), String> {
463    if values.is_empty() {
464        return Err(format!("{field} must be non-empty"));
465    }
466    for value in values {
467        if value.trim().is_empty() {
468            return Err(format!("{field} must not contain empty values"));
469        }
470    }
471    for window in values.windows(2) {
472        if window[0] >= window[1] {
473            return Err(format!("{field} must be lexically sorted and unique"));
474        }
475    }
476    Ok(())
477}
478
479/// Returns canonical browser-trace schema v1 contract.
480#[must_use]
481pub fn browser_trace_schema_v1() -> BrowserTraceSchema {
482    let mut event_specs = TraceEventKind::ALL
483        .iter()
484        .map(|kind| {
485            let mut redacted_fields = redacted_fields_for_kind(*kind);
486            sort_and_dedup_strings(&mut redacted_fields);
487            BrowserTraceEventSpec {
488                event_kind: kind.stable_name().to_string(),
489                category: browser_trace_category_for_kind(*kind),
490                required_fields: split_required_fields_csv(kind.required_fields()),
491                redacted_fields,
492            }
493        })
494        .collect::<Vec<_>>();
495    event_specs.sort_by(|left, right| left.event_kind.cmp(&right.event_kind));
496
497    BrowserTraceSchema {
498        schema_version: BROWSER_TRACE_SCHEMA_VERSION.to_string(),
499        required_envelope_fields: vec![
500            "event_kind".to_string(),
501            "schema_version".to_string(),
502            "seq".to_string(),
503            "time_ns".to_string(),
504            "trace_id".to_string(),
505        ],
506        ordering_semantics: vec![
507            "events must be strictly ordered by seq ascending".to_string(),
508            "logical_time must be monotonic for comparable causal domains".to_string(),
509            "trace streams must be deterministic for identical seed/config/replay inputs"
510                .to_string(),
511        ],
512        structured_log_required_fields: vec![
513            "capture_host_time_ns".to_string(),
514            "capture_host_turn_seq".to_string(),
515            "capture_replay_key".to_string(),
516            "capture_source".to_string(),
517            "capture_source_seq".to_string(),
518            "event_kind".to_string(),
519            "schema_version".to_string(),
520            "seq".to_string(),
521            "sequence_group".to_string(),
522            "time_ns".to_string(),
523            "trace_id".to_string(),
524            "validation_failure_category".to_string(),
525            "validation_status".to_string(),
526        ],
527        validation_failure_categories: vec![
528            "invalid_event_payload".to_string(),
529            "missing_required_field".to_string(),
530            "schema_version_mismatch".to_string(),
531            "sequence_regression".to_string(),
532        ],
533        event_specs,
534        compatibility: BrowserTraceCompatibility {
535            minimum_reader_version: "browser-trace-schema-v0".to_string(),
536            supported_reader_versions: vec![
537                "browser-trace-schema-v0".to_string(),
538                BROWSER_TRACE_SCHEMA_VERSION.to_string(),
539            ],
540            backward_decode_aliases: vec!["browser-trace-schema-v0".to_string()],
541        },
542    }
543}
544
545/// Validates browser trace schema invariants.
546///
547/// # Errors
548///
549/// Returns `Err` when deterministic ordering, schema, or compatibility
550/// invariants are violated.
551#[allow(clippy::too_many_lines)]
552pub fn validate_browser_trace_schema(schema: &BrowserTraceSchema) -> Result<(), String> {
553    if schema.schema_version != BROWSER_TRACE_SCHEMA_VERSION {
554        return Err(format!(
555            "unsupported browser trace schema version {}",
556            schema.schema_version
557        ));
558    }
559
560    validate_lexical_string_set(&schema.required_envelope_fields, "required_envelope_fields")?;
561    validate_lexical_string_set(&schema.ordering_semantics, "ordering_semantics")?;
562    validate_lexical_string_set(
563        &schema.structured_log_required_fields,
564        "structured_log_required_fields",
565    )?;
566    validate_lexical_string_set(
567        &schema.validation_failure_categories,
568        "validation_failure_categories",
569    )?;
570
571    for required in [
572        "capture_host_time_ns",
573        "capture_host_turn_seq",
574        "capture_replay_key",
575        "capture_source",
576        "capture_source_seq",
577        "trace_id",
578        "time_ns",
579        "seq",
580        "sequence_group",
581        "event_kind",
582        "schema_version",
583        "validation_failure_category",
584        "validation_status",
585    ] {
586        if !schema
587            .structured_log_required_fields
588            .iter()
589            .any(|field| field == required)
590        {
591            return Err(format!("structured_log_required_fields missing {required}"));
592        }
593    }
594
595    if schema.event_specs.is_empty() {
596        return Err("event_specs must be non-empty".to_string());
597    }
598    let event_kinds = schema
599        .event_specs
600        .iter()
601        .map(|entry| entry.event_kind.clone())
602        .collect::<Vec<_>>();
603    validate_lexical_string_set(&event_kinds, "event_specs.event_kind")?;
604
605    let expected = TraceEventKind::ALL
606        .iter()
607        .map(|kind| kind.stable_name().to_string())
608        .collect::<BTreeSet<_>>();
609    let observed = event_kinds.into_iter().collect::<BTreeSet<_>>();
610    if expected != observed {
611        return Err("event_specs must include exactly all TraceEventKind stable names".to_string());
612    }
613
614    for entry in &schema.event_specs {
615        validate_lexical_string_set(
616            &entry.required_fields,
617            &format!("event_specs[{}].required_fields", entry.event_kind),
618        )?;
619        if !entry.redacted_fields.is_empty() {
620            validate_lexical_string_set(
621                &entry.redacted_fields,
622                &format!("event_specs[{}].redacted_fields", entry.event_kind),
623            )?;
624            for field in &entry.redacted_fields {
625                if !entry
626                    .required_fields
627                    .iter()
628                    .any(|required| required == field)
629                {
630                    return Err(format!(
631                        "event_specs[{}].redacted_fields contains unknown field {}",
632                        entry.event_kind, field
633                    ));
634                }
635            }
636        }
637    }
638
639    if schema
640        .compatibility
641        .minimum_reader_version
642        .trim()
643        .is_empty()
644    {
645        return Err("compatibility.minimum_reader_version must be non-empty".to_string());
646    }
647    validate_lexical_string_set(
648        &schema.compatibility.supported_reader_versions,
649        "compatibility.supported_reader_versions",
650    )?;
651    if !schema
652        .compatibility
653        .supported_reader_versions
654        .iter()
655        .any(|version| version == &schema.compatibility.minimum_reader_version)
656    {
657        return Err("minimum_reader_version missing from supported_reader_versions".to_string());
658    }
659    if !schema
660        .compatibility
661        .supported_reader_versions
662        .iter()
663        .any(|version| version == BROWSER_TRACE_SCHEMA_VERSION)
664    {
665        return Err("supported_reader_versions must include browser-trace-schema-v1".to_string());
666    }
667    validate_lexical_string_set(
668        &schema.compatibility.backward_decode_aliases,
669        "compatibility.backward_decode_aliases",
670    )?;
671
672    Ok(())
673}
674
675#[derive(Debug, Deserialize)]
676struct BrowserTraceSchemaLegacyV0 {
677    schema_version: String,
678    required_envelope_fields: Vec<String>,
679    ordering_semantics: Vec<String>,
680    event_specs: Vec<BrowserTraceEventSpecLegacyV0>,
681}
682
683#[derive(Debug, Deserialize)]
684struct BrowserTraceEventSpecLegacyV0 {
685    event_kind: String,
686    category: Option<BrowserTraceCategory>,
687    required_fields: Option<Vec<String>>,
688    redacted_fields: Option<Vec<String>>,
689}
690
691fn upgrade_legacy_event_specs(
692    legacy_specs: Vec<BrowserTraceEventSpecLegacyV0>,
693) -> Result<Vec<BrowserTraceEventSpec>, String> {
694    let mut event_specs = Vec::with_capacity(legacy_specs.len());
695    for legacy in legacy_specs {
696        let kind = trace_event_kind_from_stable_name(legacy.event_kind.as_str())
697            .ok_or_else(|| format!("unknown legacy event kind {}", legacy.event_kind))?;
698
699        let mut required_fields = legacy
700            .required_fields
701            .unwrap_or_else(|| split_required_fields_csv(kind.required_fields()));
702        sort_and_dedup_strings(&mut required_fields);
703
704        let mut redacted_fields = legacy
705            .redacted_fields
706            .unwrap_or_else(|| redacted_fields_for_kind(kind));
707        sort_and_dedup_strings(&mut redacted_fields);
708
709        event_specs.push(BrowserTraceEventSpec {
710            event_kind: kind.stable_name().to_string(),
711            category: legacy
712                .category
713                .unwrap_or_else(|| browser_trace_category_for_kind(kind)),
714            required_fields,
715            redacted_fields,
716        });
717    }
718    event_specs.sort_by(|left, right| left.event_kind.cmp(&right.event_kind));
719    Ok(event_specs)
720}
721
722/// Decodes browser trace schema payload with backwards-compatible v0 support.
723///
724/// # Errors
725///
726/// Returns `Err` when JSON decoding fails or schema version is unsupported.
727pub fn decode_browser_trace_schema(payload: &str) -> Result<BrowserTraceSchema, String> {
728    let value: serde_json::Value =
729        serde_json::from_str(payload).map_err(|err| format!("invalid schema JSON: {err}"))?;
730    let version = value
731        .get("schema_version")
732        .and_then(serde_json::Value::as_str)
733        .ok_or_else(|| "schema_version must be a string".to_string())?;
734
735    let schema = match version {
736        BROWSER_TRACE_SCHEMA_VERSION => serde_json::from_value::<BrowserTraceSchema>(value)
737            .map_err(|err| format!("invalid browser-trace-schema-v1 payload: {err}"))?,
738        "browser-trace-schema-v0" => {
739            let legacy = serde_json::from_value::<BrowserTraceSchemaLegacyV0>(value)
740                .map_err(|err| format!("invalid browser-trace-schema-v0 payload: {err}"))?;
741            if legacy.schema_version != "browser-trace-schema-v0" {
742                return Err(format!(
743                    "invalid legacy schema version {}",
744                    legacy.schema_version
745                ));
746            }
747            let mut schema = browser_trace_schema_v1();
748            schema.required_envelope_fields = legacy.required_envelope_fields;
749            schema.ordering_semantics = legacy.ordering_semantics;
750            schema.event_specs = upgrade_legacy_event_specs(legacy.event_specs)?;
751            schema.compatibility.backward_decode_aliases =
752                vec!["browser-trace-schema-v0".to_string()];
753            schema.compatibility.minimum_reader_version = "browser-trace-schema-v0".to_string();
754            schema
755        }
756        other => {
757            return Err(format!("unsupported browser trace schema version {other}"));
758        }
759    };
760
761    validate_browser_trace_schema(&schema)?;
762    Ok(schema)
763}
764
765/// Returns redacted trace event suitable for browser diagnostics.
766///
767/// br-asupersync-92qzak: pre-fix this function only redacted
768/// `UserTrace::Message` and `ChaosInjection::Chaos.detail`, leaving
769/// every other TraceData variant's free-form String fields
770/// (Cancel.reason.message, RegionCancel.reason.message,
771/// Worker.worker_id, Down.reason / Exit.reason payload strings)
772/// unredacted. Production browser-trace exporters that bypass this
773/// function entirely (the only caller in the repo was a single test)
774/// shipped raw events to less-trusted destinations — sensitive
775/// strings (CancelReasons containing stack-frame paths or user
776/// tags, IoError payloads, panic payloads) leaked to whatever
777/// collector the browser surface forwarded to.
778///
779/// The post-fix function is EXHAUSTIVE — `match` over every
780/// TraceData variant with NO `_ =>` fallthrough — so any future
781/// variant addition is a compile error until the redactor declares
782/// its policy. Variants carrying potentially sensitive String
783/// fields (Cancel, RegionCancel, Worker, Down, Exit) have their
784/// strings rewritten to `"<redacted>"` while structural fields
785/// (TaskId, RegionId, sequence numbers) are preserved so causality
786/// can still be reconstructed by browser-side debug tooling.
787#[must_use]
788pub fn redact_browser_trace_event(event: &TraceEvent) -> TraceEvent {
789    let mut redacted = event.clone();
790    redacted.data = redact_browser_trace_data(&event.data);
791    redacted
792}
793
794/// br-asupersync-92qzak: variant-exhaustive redactor for TraceData.
795/// Returns a copy with every free-form String field replaced by
796/// `"<redacted>"` (or the equivalent in nested types). Structural
797/// identifiers (TaskId, RegionId, ObligationId, u64 sequence
798/// numbers, ObligationKind, ObligationState, ObligationAbortReason,
799/// CancelKind enum variant, ErrorKind discriminants) are preserved
800/// so causality and lifecycle reconstruction stay possible
801/// browser-side without leaking the message payloads.
802///
803/// The match has NO `_ => {}` fallthrough: every variant must
804/// declare its redaction policy explicitly so the next addition
805/// to TraceData is a compile-time prompt to think about
806/// confidentiality.
807fn redact_browser_trace_data(data: &TraceData) -> TraceData {
808    match data {
809        TraceData::None => TraceData::None,
810        TraceData::Task { task, region } => TraceData::Task {
811            task: *task,
812            region: *region,
813        },
814        TraceData::Region { region, parent } => TraceData::Region {
815            region: *region,
816            parent: *parent,
817        },
818        TraceData::Obligation {
819            obligation,
820            task,
821            region,
822            kind,
823            state,
824            duration_ns,
825            abort_reason,
826        } => TraceData::Obligation {
827            obligation: *obligation,
828            task: *task,
829            region: *region,
830            kind: *kind,
831            state: *state,
832            duration_ns: *duration_ns,
833            abort_reason: *abort_reason,
834        },
835        TraceData::Cancel {
836            task,
837            region,
838            reason,
839        } => TraceData::Cancel {
840            task: *task,
841            region: *region,
842            reason: redact_cancel_reason(reason),
843        },
844        TraceData::Worker {
845            worker_id: _,
846            job_id,
847            decision_seq,
848            replay_hash,
849            task,
850            region,
851            obligation,
852        } => TraceData::Worker {
853            worker_id: "<redacted>".to_string(),
854            job_id: *job_id,
855            decision_seq: *decision_seq,
856            replay_hash: *replay_hash,
857            task: *task,
858            region: *region,
859            obligation: *obligation,
860        },
861        TraceData::RegionCancel { region, reason } => TraceData::RegionCancel {
862            region: *region,
863            reason: redact_cancel_reason(reason),
864        },
865        TraceData::Time { old, new } => TraceData::Time {
866            old: *old,
867            new: *new,
868        },
869        TraceData::Timer { timer_id, deadline } => TraceData::Timer {
870            timer_id: *timer_id,
871            deadline: *deadline,
872        },
873        TraceData::IoRequested { token, interest } => TraceData::IoRequested {
874            token: *token,
875            interest: *interest,
876        },
877        TraceData::IoReady { token, readiness } => TraceData::IoReady {
878            token: *token,
879            readiness: *readiness,
880        },
881        TraceData::IoResult { token, bytes } => TraceData::IoResult {
882            token: *token,
883            bytes: *bytes,
884        },
885        TraceData::IoError { token, kind } => TraceData::IoError {
886            token: *token,
887            kind: *kind,
888        },
889        TraceData::RngSeed { seed } => TraceData::RngSeed { seed: *seed },
890        TraceData::RngValue { value } => TraceData::RngValue { value: *value },
891        TraceData::Checkpoint {
892            sequence,
893            active_tasks,
894            active_regions,
895        } => TraceData::Checkpoint {
896            sequence: *sequence,
897            active_tasks: *active_tasks,
898            active_regions: *active_regions,
899        },
900        TraceData::Futurelock {
901            task,
902            region,
903            idle_steps,
904            held,
905        } => TraceData::Futurelock {
906            task: *task,
907            region: *region,
908            idle_steps: *idle_steps,
909            held: held.clone(),
910        },
911        TraceData::Monitor {
912            monitor_ref,
913            watcher,
914            watcher_region,
915            monitored,
916        } => TraceData::Monitor {
917            monitor_ref: *monitor_ref,
918            watcher: *watcher,
919            watcher_region: *watcher_region,
920            monitored: *monitored,
921        },
922        TraceData::Down {
923            monitor_ref,
924            watcher,
925            monitored,
926            completion_vt,
927            reason,
928        } => TraceData::Down {
929            monitor_ref: *monitor_ref,
930            watcher: *watcher,
931            monitored: *monitored,
932            completion_vt: *completion_vt,
933            reason: redact_down_reason(reason),
934        },
935        TraceData::Link {
936            link_ref,
937            task_a,
938            region_a,
939            task_b,
940            region_b,
941        } => TraceData::Link {
942            link_ref: *link_ref,
943            task_a: *task_a,
944            region_a: *region_a,
945            task_b: *task_b,
946            region_b: *region_b,
947        },
948        TraceData::Exit {
949            link_ref,
950            from,
951            to,
952            failure_vt,
953            reason,
954        } => TraceData::Exit {
955            link_ref: *link_ref,
956            from: *from,
957            to: *to,
958            failure_vt: *failure_vt,
959            reason: redact_down_reason(reason),
960        },
961        TraceData::Message(_) => TraceData::Message("<redacted>".to_string()),
962        TraceData::Chaos {
963            kind,
964            task,
965            detail: _,
966        } => TraceData::Chaos {
967            kind: kind.clone(),
968            task: *task,
969            detail: "<redacted>".to_string(),
970        },
971        // Budget payloads are task/region ids, budget quotas, and stable
972        // source/outcome tokens — no free-form/user data to redact.
973        TraceData::Budget {
974            task,
975            region,
976            protocol,
977            deadline_ns,
978            poll_quota,
979            cost_quota,
980            priority,
981            source,
982            elapsed_ns,
983            outcome,
984        } => TraceData::Budget {
985            task: *task,
986            region: *region,
987            protocol: protocol.clone(),
988            deadline_ns: *deadline_ns,
989            poll_quota: *poll_quota,
990            cost_quota: *cost_quota,
991            priority: *priority,
992            source: source.clone(),
993            elapsed_ns: *elapsed_ns,
994            outcome: outcome.clone(),
995        },
996    }
997}
998
999/// Replace any free-form payload inside a CancelReason with a fixed
1000/// sentinel; preserve the kind enum (which is finite and non-secret).
1001fn redact_cancel_reason(reason: &CancelReason) -> CancelReason {
1002    let mut redacted = reason.clone();
1003    if redacted.message.is_some() {
1004        redacted.message = Some("<redacted>".to_string());
1005    }
1006    redacted
1007}
1008
1009/// Replace free-form String fields inside a DownReason with sentinels.
1010/// `Normal` carries no payload; `Error(String)`, `Cancelled(CancelReason)`,
1011/// and `Panicked(PanicPayload)` all do.
1012fn redact_down_reason(reason: &DownReason) -> DownReason {
1013    match reason {
1014        DownReason::Normal => DownReason::Normal,
1015        DownReason::Error(_) => DownReason::Error("<redacted>".to_string()),
1016        DownReason::Cancelled(cr) => DownReason::Cancelled(redact_cancel_reason(cr)),
1017        DownReason::Panicked(payload) => DownReason::Panicked(redact_panic_payload(payload)),
1018    }
1019}
1020
1021/// Replace any String inside a PanicPayload with the redaction
1022/// sentinel. PanicPayload's message field is private, so we
1023/// construct a fresh payload via the public constructor.
1024fn redact_panic_payload(_payload: &PanicPayload) -> PanicPayload {
1025    PanicPayload::new("<redacted>")
1026}
1027
1028fn default_browser_capture_metadata(event: &TraceEvent) -> BrowserCaptureMetadata {
1029    BrowserCaptureMetadata {
1030        host_turn_seq: event.seq,
1031        source: BrowserCaptureSource::Runtime,
1032        source_seq: event.seq,
1033        host_time_ns: event.time.as_nanos(),
1034    }
1035}
1036
1037fn stable_browser_trace_hash(bytes: &[u8]) -> u64 {
1038    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
1039    for &byte in bytes {
1040        hash ^= u64::from(byte);
1041        hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
1042    }
1043    hash
1044}
1045
1046fn cap_browser_trace_attribute(value: &str) -> String {
1047    if value.len() <= MAX_BROWSER_TRACE_ATTRIBUTE_BYTES {
1048        return value.to_string();
1049    }
1050
1051    let suffix = format!("#{:016x}", stable_browser_trace_hash(value.as_bytes()));
1052    let mut cut = MAX_BROWSER_TRACE_ATTRIBUTE_BYTES.saturating_sub(suffix.len());
1053    while cut > 0 && !value.is_char_boundary(cut) {
1054        cut -= 1;
1055    }
1056
1057    let mut capped = value[..cut].to_string();
1058    capped.push_str(&suffix);
1059    capped
1060}
1061
1062fn obligation_state_name(state: ObligationState) -> &'static str {
1063    match state {
1064        ObligationState::Reserved => "reserved",
1065        ObligationState::Committed => "committed",
1066        ObligationState::Aborted => "aborted",
1067        ObligationState::Leaked => "leaked",
1068    }
1069}
1070
1071fn optional_time_field(value: Option<Time>) -> String {
1072    value.map_or_else(|| "none".to_string(), |time| time.as_nanos().to_string())
1073}
1074
1075fn optional_display_field<T: fmt::Display>(value: Option<T>) -> String {
1076    value.map_or_else(|| "none".to_string(), |value| value.to_string())
1077}
1078
1079fn futurelock_held_field(held: &[(ObligationId, ObligationKind)]) -> String {
1080    let held = held
1081        .iter()
1082        .map(|(obligation, kind)| format!("{obligation}:{kind}"))
1083        .collect::<Vec<_>>();
1084    serde_json::to_string(&held).expect("futurelock held obligations serialize")
1085}
1086
1087fn insert_browser_trace_payload_fields(fields: &mut BTreeMap<String, String>, event: &TraceEvent) {
1088    match &event.data {
1089        TraceData::None => {}
1090        TraceData::Task { task, region } => {
1091            fields.insert("task".to_string(), task.to_string());
1092            fields.insert("region".to_string(), region.to_string());
1093        }
1094        TraceData::Budget {
1095            task,
1096            region,
1097            protocol,
1098            deadline_ns,
1099            poll_quota,
1100            cost_quota,
1101            priority,
1102            source,
1103            elapsed_ns,
1104            outcome,
1105        } => {
1106            fields.insert("task".to_string(), task.to_string());
1107            fields.insert("region".to_string(), region.to_string());
1108            fields.insert("protocol".to_string(), protocol.clone());
1109            fields.insert(
1110                "deadline_ns".to_string(),
1111                optional_display_field(*deadline_ns),
1112            );
1113            fields.insert("poll_quota".to_string(), poll_quota.to_string());
1114            fields.insert(
1115                "cost_quota".to_string(),
1116                optional_display_field(*cost_quota),
1117            );
1118            fields.insert("priority".to_string(), priority.to_string());
1119            if let Some(source) = source {
1120                fields.insert("source".to_string(), source.clone());
1121            }
1122            if let Some(elapsed_ns) = elapsed_ns {
1123                fields.insert("elapsed_ns".to_string(), elapsed_ns.to_string());
1124            }
1125            if let Some(outcome) = outcome {
1126                fields.insert("outcome".to_string(), outcome.clone());
1127            }
1128        }
1129        TraceData::Region { region, parent } => {
1130            fields.insert("region".to_string(), region.to_string());
1131            fields.insert("parent".to_string(), optional_display_field(*parent));
1132        }
1133        TraceData::Obligation {
1134            obligation,
1135            task,
1136            region,
1137            kind,
1138            state,
1139            duration_ns,
1140            abort_reason,
1141        } => {
1142            fields.insert("obligation".to_string(), obligation.to_string());
1143            fields.insert("task".to_string(), task.to_string());
1144            fields.insert("region".to_string(), region.to_string());
1145            fields.insert("kind".to_string(), kind.to_string());
1146            fields.insert(
1147                "state".to_string(),
1148                obligation_state_name(*state).to_string(),
1149            );
1150
1151            if matches!(
1152                event.kind,
1153                TraceEventKind::ObligationCommit
1154                    | TraceEventKind::ObligationAbort
1155                    | TraceEventKind::ObligationLeak
1156            ) {
1157                fields.insert(
1158                    "duration_ns".to_string(),
1159                    duration_ns.map_or_else(|| "none".to_string(), |value| value.to_string()),
1160                );
1161            }
1162
1163            if matches!(event.kind, TraceEventKind::ObligationAbort) {
1164                fields.insert(
1165                    "abort_reason".to_string(),
1166                    abort_reason.map_or_else(|| "none".to_string(), |reason| reason.to_string()),
1167                );
1168            }
1169        }
1170        TraceData::Cancel {
1171            task,
1172            region,
1173            reason,
1174        } => {
1175            fields.insert("task".to_string(), task.to_string());
1176            fields.insert("region".to_string(), region.to_string());
1177            fields.insert("reason".to_string(), reason.to_string());
1178        }
1179        TraceData::Worker {
1180            worker_id,
1181            job_id,
1182            decision_seq,
1183            replay_hash,
1184            task,
1185            region,
1186            obligation,
1187        } => {
1188            fields.insert("decision_seq".to_string(), decision_seq.to_string());
1189            fields.insert("job_id".to_string(), job_id.to_string());
1190            fields.insert("obligation".to_string(), obligation.to_string());
1191            fields.insert("region".to_string(), region.to_string());
1192            fields.insert("replay_hash".to_string(), replay_hash.to_string());
1193            fields.insert("task".to_string(), task.to_string());
1194            fields.insert(
1195                "worker_id".to_string(),
1196                cap_browser_trace_attribute(worker_id),
1197            );
1198        }
1199        TraceData::RegionCancel { region, reason } => {
1200            fields.insert("region".to_string(), region.to_string());
1201            fields.insert("reason".to_string(), reason.to_string());
1202        }
1203        TraceData::Time { old, new } => {
1204            fields.insert("old".to_string(), old.as_nanos().to_string());
1205            fields.insert("new".to_string(), new.as_nanos().to_string());
1206        }
1207        TraceData::Timer { timer_id, deadline } => {
1208            fields.insert("timer_id".to_string(), timer_id.to_string());
1209            if matches!(event.kind, TraceEventKind::TimerScheduled) || deadline.is_some() {
1210                fields.insert("deadline".to_string(), optional_time_field(*deadline));
1211            }
1212        }
1213        TraceData::IoRequested { token, interest } => {
1214            fields.insert("token".to_string(), token.to_string());
1215            fields.insert("interest".to_string(), interest.to_string());
1216        }
1217        TraceData::IoReady { token, readiness } => {
1218            fields.insert("token".to_string(), token.to_string());
1219            fields.insert("readiness".to_string(), readiness.to_string());
1220        }
1221        TraceData::IoResult { token, bytes } => {
1222            fields.insert("token".to_string(), token.to_string());
1223            fields.insert("bytes".to_string(), bytes.to_string());
1224        }
1225        TraceData::IoError { token, kind } => {
1226            fields.insert("token".to_string(), token.to_string());
1227            fields.insert("kind".to_string(), kind.to_string());
1228        }
1229        TraceData::RngSeed { seed } => {
1230            fields.insert("seed".to_string(), seed.to_string());
1231        }
1232        TraceData::RngValue { value } => {
1233            fields.insert("value".to_string(), value.to_string());
1234        }
1235        TraceData::Checkpoint {
1236            sequence,
1237            active_tasks,
1238            active_regions,
1239        } => {
1240            fields.insert("sequence".to_string(), sequence.to_string());
1241            fields.insert("active_tasks".to_string(), active_tasks.to_string());
1242            fields.insert("active_regions".to_string(), active_regions.to_string());
1243        }
1244        TraceData::Futurelock {
1245            task,
1246            region,
1247            idle_steps,
1248            held,
1249        } => {
1250            fields.insert("task".to_string(), task.to_string());
1251            fields.insert("region".to_string(), region.to_string());
1252            fields.insert("idle_steps".to_string(), idle_steps.to_string());
1253            fields.insert("held".to_string(), futurelock_held_field(held));
1254        }
1255        TraceData::Monitor {
1256            monitor_ref,
1257            watcher,
1258            watcher_region,
1259            monitored,
1260        } => {
1261            fields.insert("monitor_ref".to_string(), monitor_ref.to_string());
1262            fields.insert("watcher".to_string(), watcher.to_string());
1263            fields.insert("watcher_region".to_string(), watcher_region.to_string());
1264            fields.insert("monitored".to_string(), monitored.to_string());
1265        }
1266        TraceData::Down {
1267            monitor_ref,
1268            watcher,
1269            monitored,
1270            completion_vt,
1271            reason,
1272        } => {
1273            fields.insert("monitor_ref".to_string(), monitor_ref.to_string());
1274            fields.insert("watcher".to_string(), watcher.to_string());
1275            fields.insert("monitored".to_string(), monitored.to_string());
1276            fields.insert(
1277                "completion_vt".to_string(),
1278                completion_vt.as_nanos().to_string(),
1279            );
1280            fields.insert("reason".to_string(), reason.to_string());
1281        }
1282        TraceData::Link {
1283            link_ref,
1284            task_a,
1285            region_a,
1286            task_b,
1287            region_b,
1288        } => {
1289            fields.insert("link_ref".to_string(), link_ref.to_string());
1290            fields.insert("task_a".to_string(), task_a.to_string());
1291            fields.insert("region_a".to_string(), region_a.to_string());
1292            fields.insert("task_b".to_string(), task_b.to_string());
1293            fields.insert("region_b".to_string(), region_b.to_string());
1294        }
1295        TraceData::Exit {
1296            link_ref,
1297            from,
1298            to,
1299            failure_vt,
1300            reason,
1301        } => {
1302            fields.insert("link_ref".to_string(), link_ref.to_string());
1303            fields.insert("from".to_string(), from.to_string());
1304            fields.insert("to".to_string(), to.to_string());
1305            fields.insert("failure_vt".to_string(), failure_vt.as_nanos().to_string());
1306            fields.insert("reason".to_string(), reason.to_string());
1307        }
1308        TraceData::Message(message) => {
1309            fields.insert("message".to_string(), message.clone());
1310        }
1311        TraceData::Chaos { kind, task, detail } => {
1312            fields.insert("kind".to_string(), kind.clone());
1313            fields.insert("task".to_string(), optional_display_field(*task));
1314            fields.insert("detail".to_string(), detail.clone());
1315        }
1316    }
1317}
1318
1319fn browser_trace_sequence_group(event: &TraceEvent) -> String {
1320    // Sequence groups must identify the causal or relationship domain for
1321    // ordering checks; category labels are too coarse and collapse independent
1322    // streams into the same group.
1323    let raw = match &event.data {
1324        TraceData::Task { task, .. }
1325        | TraceData::Cancel { task, .. }
1326        | TraceData::Futurelock { task, .. }
1327        | TraceData::Budget { task, .. } => format!("task:{task}"),
1328        TraceData::Region { region, .. } | TraceData::RegionCancel { region, .. } => {
1329            format!("region:{region}")
1330        }
1331        TraceData::Obligation { obligation, .. } => format!("obligation:{obligation}"),
1332        TraceData::Worker {
1333            worker_id, job_id, ..
1334        } => format!("worker_job:{job_id}:{worker_id}"),
1335        TraceData::Time { .. } => "time".to_string(),
1336        TraceData::Timer { timer_id, .. } => format!("timer:{timer_id}"),
1337        TraceData::IoRequested { token, .. }
1338        | TraceData::IoReady { token, .. }
1339        | TraceData::IoResult { token, .. }
1340        | TraceData::IoError { token, .. } => format!("io:{token}"),
1341        TraceData::RngSeed { .. } | TraceData::RngValue { .. } => "rng".to_string(),
1342        TraceData::Checkpoint { sequence, .. } => format!("checkpoint:{sequence}"),
1343        TraceData::Monitor { monitor_ref, .. } | TraceData::Down { monitor_ref, .. } => {
1344            format!("monitor:{monitor_ref}")
1345        }
1346        TraceData::Link { link_ref, .. } | TraceData::Exit { link_ref, .. } => {
1347            format!("link:{link_ref}")
1348        }
1349        TraceData::Message(_) => "user_trace".to_string(),
1350        TraceData::Chaos {
1351            task: Some(task), ..
1352        } => format!("task:{task}"),
1353        TraceData::Chaos { task: None, .. } => "chaos".to_string(),
1354        TraceData::None => format!("kind:{}", event.kind.stable_name()),
1355    };
1356    cap_browser_trace_attribute(&raw)
1357}
1358
1359fn browser_capture_replay_key(metadata: &BrowserCaptureMetadata) -> String {
1360    format!(
1361        "{}:{}:{}:{}",
1362        match metadata.source {
1363            BrowserCaptureSource::Runtime => "runtime",
1364            BrowserCaptureSource::Time => "time",
1365            BrowserCaptureSource::Event => "event",
1366            BrowserCaptureSource::HostInput => "host_input",
1367        },
1368        metadata.host_turn_seq,
1369        metadata.source_seq,
1370        metadata.host_time_ns
1371    )
1372}
1373
1374/// Returns deterministic structured-log fields for one browser trace event.
1375///
1376/// When `capture_metadata` is not provided, deterministic fallback values are
1377/// reconstructed from event sequence and event time.
1378#[must_use]
1379pub fn browser_trace_log_fields_with_capture(
1380    event: &TraceEvent,
1381    trace_id: &str,
1382    validation_failure_category: Option<&str>,
1383    capture_metadata: Option<&BrowserCaptureMetadata>,
1384) -> BTreeMap<String, String> {
1385    // br-asupersync-92qzak: every public browser-trace export now
1386    // routes through the redactor before any payload-field
1387    // serialization. Pre-fix this function read event.data verbatim,
1388    // bypassing redact_browser_trace_event entirely (the redactor
1389    // had only a single test caller). Post-fix the redacted variant
1390    // is the *only* event passed to insert_browser_trace_payload_fields,
1391    // so any free-form String inside event.data (cancel reasons,
1392    // worker IDs, panic payloads, error strings) lands as
1393    // `<redacted>` in the exported log fields.
1394    let event = &redact_browser_trace_event(event);
1395    let capture = capture_metadata
1396        .cloned()
1397        .unwrap_or_else(|| default_browser_capture_metadata(event));
1398    let mut fields = BTreeMap::new();
1399    fields.insert(
1400        "capture_host_time_ns".to_string(),
1401        capture.host_time_ns.to_string(),
1402    );
1403    fields.insert(
1404        "capture_host_turn_seq".to_string(),
1405        capture.host_turn_seq.to_string(),
1406    );
1407    fields.insert(
1408        "capture_replay_key".to_string(),
1409        browser_capture_replay_key(&capture),
1410    );
1411    fields.insert(
1412        "capture_source".to_string(),
1413        match capture.source {
1414            BrowserCaptureSource::Runtime => "runtime".to_string(),
1415            BrowserCaptureSource::Time => "time".to_string(),
1416            BrowserCaptureSource::Event => "event".to_string(),
1417            BrowserCaptureSource::HostInput => "host_input".to_string(),
1418        },
1419    );
1420    fields.insert(
1421        "capture_source_seq".to_string(),
1422        capture.source_seq.to_string(),
1423    );
1424    fields.insert(
1425        "event_kind".to_string(),
1426        event.kind.stable_name().to_string(),
1427    );
1428    fields.insert(
1429        "schema_version".to_string(),
1430        BROWSER_TRACE_SCHEMA_VERSION.to_string(),
1431    );
1432    fields.insert("seq".to_string(), event.seq.to_string());
1433    fields.insert("time_ns".to_string(), event.time.as_nanos().to_string());
1434    fields.insert("trace_id".to_string(), trace_id.to_string());
1435    fields.insert(
1436        "sequence_group".to_string(),
1437        browser_trace_sequence_group(event),
1438    );
1439    let failure_category = validation_failure_category
1440        .filter(|category| !category.trim().is_empty())
1441        .unwrap_or("none");
1442    fields.insert(
1443        "validation_failure_category".to_string(),
1444        failure_category.to_string(),
1445    );
1446    fields.insert(
1447        "validation_status".to_string(),
1448        if failure_category == "none" {
1449            "valid".to_string()
1450        } else {
1451            "invalid".to_string()
1452        },
1453    );
1454    insert_browser_trace_payload_fields(&mut fields, event);
1455    fields
1456}
1457
1458/// Returns deterministic structured-log fields for one browser trace event.
1459#[must_use]
1460pub fn browser_trace_log_fields(
1461    event: &TraceEvent,
1462    trace_id: &str,
1463    validation_failure_category: Option<&str>,
1464) -> BTreeMap<String, String> {
1465    browser_trace_log_fields_with_capture(event, trace_id, validation_failure_category, None)
1466}
1467
1468impl fmt::Display for TraceEventKind {
1469    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1470        f.write_str(self.stable_name())
1471    }
1472}
1473
1474/// Additional data carried by a trace event.
1475#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1476pub enum TraceData {
1477    /// No additional data.
1478    None,
1479    /// Task-related data.
1480    Task {
1481        /// The task involved.
1482        task: TaskId,
1483        /// The region the task belongs to.
1484        region: RegionId,
1485    },
1486    /// Region-related data.
1487    Region {
1488        /// The region involved.
1489        region: RegionId,
1490        /// The parent region, if any.
1491        parent: Option<RegionId>,
1492    },
1493    /// Obligation-related data.
1494    Obligation {
1495        /// The obligation involved.
1496        obligation: ObligationId,
1497        /// The task holding the obligation.
1498        task: TaskId,
1499        /// The region that owns the obligation.
1500        region: RegionId,
1501        /// The kind of obligation.
1502        kind: ObligationKind,
1503        /// The obligation state at this event.
1504        state: ObligationState,
1505        /// Duration held in nanoseconds, if resolved.
1506        duration_ns: Option<u64>,
1507        /// Abort reason, if aborted.
1508        abort_reason: Option<ObligationAbortReason>,
1509    },
1510    /// Cancellation data.
1511    Cancel {
1512        /// The task involved.
1513        task: TaskId,
1514        /// The region involved.
1515        region: RegionId,
1516        /// The reason for cancellation.
1517        reason: CancelReason,
1518    },
1519    /// Worker-offload lifecycle data across the browser boundary.
1520    Worker {
1521        /// Worker runtime instance identifier.
1522        worker_id: String,
1523        /// Offloaded job identifier within the worker coordinator.
1524        job_id: u64,
1525        /// Deterministic decision sequence carried by the worker envelope.
1526        decision_seq: u64,
1527        /// Stable replay digest carried by the worker envelope.
1528        replay_hash: u64,
1529        /// The originating task that owns the offloaded work.
1530        task: TaskId,
1531        /// The originating region that owns the task.
1532        region: RegionId,
1533        /// The originating obligation that must be drained/finalized.
1534        obligation: ObligationId,
1535    },
1536    /// Region cancellation data.
1537    RegionCancel {
1538        /// The region involved.
1539        region: RegionId,
1540        /// The reason for cancellation.
1541        reason: CancelReason,
1542    },
1543    /// Time data.
1544    Time {
1545        /// The previous time.
1546        old: Time,
1547        /// The new time.
1548        new: Time,
1549    },
1550    /// Timer data.
1551    Timer {
1552        /// Timer identifier.
1553        timer_id: u64,
1554        /// Deadline, if applicable.
1555        deadline: Option<Time>,
1556    },
1557    /// I/O interest request data.
1558    IoRequested {
1559        /// I/O token.
1560        token: u64,
1561        /// Interest bitflags (readable=1, writable=2, error=4, hangup=8).
1562        interest: u8,
1563    },
1564    /// I/O readiness data.
1565    IoReady {
1566        /// I/O token.
1567        token: u64,
1568        /// Readiness bitflags (readable=1, writable=2, error=4, hangup=8).
1569        readiness: u8,
1570    },
1571    /// I/O result data.
1572    IoResult {
1573        /// I/O token.
1574        token: u64,
1575        /// Bytes transferred (negative for errors).
1576        bytes: i64,
1577    },
1578    /// I/O error data.
1579    IoError {
1580        /// I/O token.
1581        token: u64,
1582        /// Error kind as u8 (maps to io::ErrorKind).
1583        kind: u8,
1584    },
1585    /// RNG seed data.
1586    RngSeed {
1587        /// Seed value.
1588        seed: u64,
1589    },
1590    /// RNG value data.
1591    RngValue {
1592        /// Generated value.
1593        value: u64,
1594    },
1595    /// Checkpoint data.
1596    Checkpoint {
1597        /// Monotonic sequence number.
1598        sequence: u64,
1599        /// Active task count.
1600        active_tasks: u32,
1601        /// Active region count.
1602        active_regions: u32,
1603    },
1604    /// Futurelock detection data.
1605    Futurelock {
1606        /// The task that futurelocked.
1607        task: TaskId,
1608        /// The owning region of the task.
1609        region: RegionId,
1610        /// How many lab steps since the task was last polled.
1611        idle_steps: u64,
1612        /// Obligations held by the task at detection time.
1613        held: Vec<(ObligationId, ObligationKind)>,
1614    },
1615    /// Monitor lifecycle event.
1616    Monitor {
1617        /// Monitor reference id.
1618        monitor_ref: u64,
1619        /// The task watching for termination.
1620        watcher: TaskId,
1621        /// The region owning the watcher (for region-close cleanup).
1622        watcher_region: RegionId,
1623        /// The task being monitored.
1624        monitored: TaskId,
1625    },
1626    /// Down notification delivery.
1627    ///
1628    /// Includes the deterministic ordering key (`completion_vt`, `monitored`).
1629    Down {
1630        /// Monitor reference id from establishment.
1631        monitor_ref: u64,
1632        /// The task receiving the notification.
1633        watcher: TaskId,
1634        /// The task that terminated.
1635        monitored: TaskId,
1636        /// Virtual time of monitored task completion.
1637        completion_vt: Time,
1638        /// Why it terminated.
1639        reason: DownReason,
1640    },
1641    /// Link lifecycle event.
1642    Link {
1643        /// Link reference id.
1644        link_ref: u64,
1645        /// One side of the link.
1646        task_a: TaskId,
1647        /// Region owning task_a (for region-close cleanup).
1648        region_a: RegionId,
1649        /// The other side of the link.
1650        task_b: TaskId,
1651        /// Region owning task_b (for region-close cleanup).
1652        region_b: RegionId,
1653    },
1654    /// Exit signal delivery to a linked task.
1655    ///
1656    /// Includes the deterministic ordering key (`failure_vt`, `from`).
1657    Exit {
1658        /// Link reference id.
1659        link_ref: u64,
1660        /// The task that terminated (source of the exit).
1661        from: TaskId,
1662        /// The linked task receiving the exit signal.
1663        to: TaskId,
1664        /// Virtual time of failure used for deterministic ordering.
1665        failure_vt: Time,
1666        /// Why it terminated.
1667        reason: DownReason,
1668    },
1669    /// User message.
1670    Message(String),
1671    /// Chaos injection data.
1672    Chaos {
1673        /// Kind of chaos injected (e.g., "cancel", "delay", "budget_exhaust", "wakeup_storm").
1674        kind: String,
1675        /// The task affected, if any.
1676        task: Option<TaskId>,
1677        /// Additional detail.
1678        detail: String,
1679    },
1680    /// Server request-region budget data for [`TraceEventKind::BudgetInstalled`]
1681    /// and [`TraceEventKind::BudgetConsumed`].
1682    Budget {
1683        /// The request task.
1684        task: TaskId,
1685        /// The request region.
1686        region: RegionId,
1687        /// Transport protocol token (e.g. `h1`, `h2`, `grpc`).
1688        protocol: String,
1689        /// Budget deadline in nanoseconds, if any.
1690        deadline_ns: Option<u64>,
1691        /// Poll quota.
1692        poll_quota: u64,
1693        /// Cost quota, if any.
1694        cost_quota: Option<u64>,
1695        /// Scheduling priority.
1696        priority: u8,
1697        /// Budget source token (install events; `None` for consume).
1698        source: Option<String>,
1699        /// Elapsed nanoseconds at consumption (`None` for install).
1700        elapsed_ns: Option<u64>,
1701        /// Outcome token at consumption (`None` for install): one of
1702        /// `ok`/`err`/`cancelled`/`panicked`/`deadline_exceeded`/
1703        /// `connection_lost`/`force_closed`.
1704        outcome: Option<String>,
1705    },
1706}
1707
1708/// A trace event in the runtime.
1709#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1710pub struct TraceEvent {
1711    /// Event schema version.
1712    pub version: u32,
1713    /// Sequence number (monotonically increasing).
1714    pub seq: u64,
1715    /// Timestamp when the event occurred.
1716    pub time: Time,
1717    /// Logical clock timestamp for causal ordering.
1718    ///
1719    /// When set, enables causal consistency verification across distributed
1720    /// traces. The logical time is ticked when the event is recorded and
1721    /// can be used to establish happens-before relationships.
1722    pub logical_time: Option<LogicalTime>,
1723    /// The kind of event.
1724    pub kind: TraceEventKind,
1725    /// Additional data.
1726    pub data: TraceData,
1727}
1728
1729macro_rules! trace_event_constructors {
1730    ($(
1731        $(#[$meta:meta])*
1732        $name:ident($($arg:ident: $ty:ty),* $(,)?) => $kind:ident, $data:expr;
1733    )*) => {
1734        $(
1735            $(#[$meta])*
1736            #[must_use]
1737            pub fn $name(seq: u64, time: Time, $($arg: $ty),*) -> Self {
1738                Self::new(seq, time, TraceEventKind::$kind, $data)
1739            }
1740        )*
1741    };
1742}
1743
1744macro_rules! worker_lifecycle_constructors {
1745    ($(
1746        $(#[$meta:meta])*
1747        $name:ident => $kind:ident;
1748    )*) => {
1749        $(
1750            $(#[$meta])*
1751            #[allow(clippy::too_many_arguments)]
1752            #[must_use]
1753            pub fn $name(
1754                seq: u64,
1755                time: Time,
1756                worker_id: impl Into<String>,
1757                job_id: u64,
1758                decision_seq: u64,
1759                replay_hash: u64,
1760                task: TaskId,
1761                region: RegionId,
1762                obligation: ObligationId,
1763            ) -> Self {
1764                Self::worker_lifecycle(
1765                    seq,
1766                    time,
1767                    TraceEventKind::$kind,
1768                    worker_id,
1769                    job_id,
1770                    decision_seq,
1771                    replay_hash,
1772                    task,
1773                    region,
1774                    obligation,
1775                )
1776            }
1777        )*
1778    };
1779}
1780
1781impl TraceEvent {
1782    /// Creates a new trace event.
1783    #[must_use]
1784    #[inline]
1785    pub fn new(seq: u64, time: Time, kind: TraceEventKind, data: TraceData) -> Self {
1786        Self {
1787            version: TRACE_EVENT_SCHEMA_VERSION,
1788            seq,
1789            time,
1790            logical_time: None,
1791            kind,
1792            data,
1793        }
1794    }
1795
1796    /// Attaches a logical clock timestamp to this event for causal ordering.
1797    #[inline]
1798    #[must_use]
1799    pub fn with_logical_time(mut self, logical_time: LogicalTime) -> Self {
1800        self.logical_time = Some(logical_time);
1801        self
1802    }
1803
1804    trace_event_constructors! {
1805        /// Creates a spawn event.
1806        spawn(task: TaskId, region: RegionId) => Spawn, TraceData::Task { task, region };
1807        /// Creates a schedule event.
1808        schedule(task: TaskId, region: RegionId) => Schedule, TraceData::Task { task, region };
1809        /// Creates a yield event.
1810        yield_task(task: TaskId, region: RegionId) => Yield, TraceData::Task { task, region };
1811        /// Creates a wake event.
1812        wake(task: TaskId, region: RegionId) => Wake, TraceData::Task { task, region };
1813        /// Creates a poll event.
1814        poll(task: TaskId, region: RegionId) => Poll, TraceData::Task { task, region };
1815        /// Creates a complete event.
1816        complete(task: TaskId, region: RegionId) => Complete, TraceData::Task { task, region };
1817        /// Creates a task-spawn-enqueued event (spawn-mailbox intake, pre-admission).
1818        task_spawn_enqueued(task: TaskId, region: RegionId) => TaskSpawnEnqueued,
1819            TraceData::Task { task, region };
1820        /// Creates a task-admitted event (spawn-mailbox admission; task carries the
1821        /// canonical arena id, pairing with the enqueue event by per-region FIFO order).
1822        task_admitted(task: TaskId, region: RegionId) => TaskAdmitted,
1823            TraceData::Task { task, region };
1824        /// Creates a cancel request event.
1825        cancel_request(task: TaskId, region: RegionId, reason: CancelReason) => CancelRequest,
1826            TraceData::Cancel { task, region, reason };
1827    }
1828
1829    /// Creates a server-budget-installed event (per-request budget minted at the
1830    /// runtime boundary).
1831    #[allow(clippy::too_many_arguments)]
1832    #[must_use]
1833    pub fn budget_installed(
1834        seq: u64,
1835        time: Time,
1836        task: TaskId,
1837        region: RegionId,
1838        protocol: impl Into<String>,
1839        deadline_ns: Option<u64>,
1840        poll_quota: u64,
1841        cost_quota: Option<u64>,
1842        priority: u8,
1843        source: impl Into<String>,
1844    ) -> Self {
1845        Self::new(
1846            seq,
1847            time,
1848            TraceEventKind::BudgetInstalled,
1849            TraceData::Budget {
1850                task,
1851                region,
1852                protocol: protocol.into(),
1853                deadline_ns,
1854                poll_quota,
1855                cost_quota,
1856                priority,
1857                source: Some(source.into()),
1858                elapsed_ns: None,
1859                outcome: None,
1860            },
1861        )
1862    }
1863
1864    /// Creates a server-budget-consumed event (request resolved on some path,
1865    /// including the force-closed Drop backstop).
1866    #[allow(clippy::too_many_arguments)]
1867    #[must_use]
1868    pub fn budget_consumed(
1869        seq: u64,
1870        time: Time,
1871        task: TaskId,
1872        region: RegionId,
1873        protocol: impl Into<String>,
1874        deadline_ns: Option<u64>,
1875        poll_quota: u64,
1876        cost_quota: Option<u64>,
1877        priority: u8,
1878        elapsed_ns: Option<u64>,
1879        outcome: impl Into<String>,
1880    ) -> Self {
1881        Self::new(
1882            seq,
1883            time,
1884            TraceEventKind::BudgetConsumed,
1885            TraceData::Budget {
1886                task,
1887                region,
1888                protocol: protocol.into(),
1889                deadline_ns,
1890                poll_quota,
1891                cost_quota,
1892                priority,
1893                source: None,
1894                elapsed_ns,
1895                outcome: Some(outcome.into()),
1896            },
1897        )
1898    }
1899
1900    #[allow(clippy::too_many_arguments)]
1901    fn worker_lifecycle(
1902        seq: u64,
1903        time: Time,
1904        kind: TraceEventKind,
1905        worker_id: impl Into<String>,
1906        job_id: u64,
1907        decision_seq: u64,
1908        replay_hash: u64,
1909        task: TaskId,
1910        region: RegionId,
1911        obligation: ObligationId,
1912    ) -> Self {
1913        Self::new(
1914            seq,
1915            time,
1916            kind,
1917            TraceData::Worker {
1918                worker_id: worker_id.into(),
1919                job_id,
1920                decision_seq,
1921                replay_hash,
1922                task,
1923                region,
1924                obligation,
1925            },
1926        )
1927    }
1928
1929    worker_lifecycle_constructors! {
1930        /// Creates a worker-offload cancel-requested event.
1931        worker_cancel_requested => WorkerCancelRequested;
1932        /// Creates a worker-offload cancel-acknowledged event.
1933        worker_cancel_acknowledged => WorkerCancelAcknowledged;
1934        /// Creates a worker-offload drain-started event.
1935        worker_drain_started => WorkerDrainStarted;
1936        /// Creates a worker-offload drain-completed event.
1937        worker_drain_completed => WorkerDrainCompleted;
1938        /// Creates a worker-offload finalize-completed event.
1939        worker_finalize_completed => WorkerFinalizeCompleted;
1940    }
1941
1942    trace_event_constructors! {
1943        /// Creates a region created event.
1944        region_created(region: RegionId, parent: Option<RegionId>) => RegionCreated,
1945            TraceData::Region { region, parent };
1946        /// Creates a region cancelled event.
1947        region_cancelled(region: RegionId, reason: CancelReason) => RegionCancelled,
1948            TraceData::RegionCancel { region, reason };
1949        /// Creates a time advance event.
1950        time_advance(old: Time, new: Time) => TimeAdvance, TraceData::Time { old, new };
1951        /// Creates a timer scheduled event.
1952        timer_scheduled(timer_id: u64, deadline: Time) => TimerScheduled,
1953            TraceData::Timer { timer_id, deadline: Some(deadline) };
1954        /// Creates a timer fired event.
1955        timer_fired(timer_id: u64) => TimerFired, TraceData::Timer { timer_id, deadline: None };
1956        /// Creates a timer cancelled event.
1957        timer_cancelled(timer_id: u64) => TimerCancelled,
1958            TraceData::Timer { timer_id, deadline: None };
1959        /// Creates an I/O requested event.
1960        io_requested(token: u64, interest: u8) => IoRequested,
1961            TraceData::IoRequested { token, interest };
1962        /// Creates an I/O ready event.
1963        io_ready(token: u64, readiness: u8) => IoReady, TraceData::IoReady { token, readiness };
1964        /// Creates an I/O result event.
1965        io_result(token: u64, bytes: i64) => IoResult, TraceData::IoResult { token, bytes };
1966        /// Creates an I/O error event.
1967        io_error(token: u64, kind: u8) => IoError, TraceData::IoError { token, kind };
1968        /// Creates an RNG seed event.
1969        rng_seed(seed: u64) => RngSeed, TraceData::RngSeed { seed };
1970        /// Creates an RNG value event.
1971        rng_value(value: u64) => RngValue, TraceData::RngValue { value };
1972        /// Creates a checkpoint event.
1973        checkpoint(sequence: u64, active_tasks: u32, active_regions: u32) => Checkpoint,
1974            TraceData::Checkpoint { sequence, active_tasks, active_regions };
1975        /// Creates an obligation reserve event.
1976        obligation_reserve(
1977            obligation: ObligationId,
1978            task: TaskId,
1979            region: RegionId,
1980            kind: ObligationKind,
1981        ) => ObligationReserve,
1982            TraceData::Obligation {
1983                obligation,
1984                task,
1985                region,
1986                kind,
1987                state: ObligationState::Reserved,
1988                duration_ns: None,
1989                abort_reason: None,
1990            };
1991        /// Creates an obligation commit event.
1992        obligation_commit(
1993            obligation: ObligationId,
1994            task: TaskId,
1995            region: RegionId,
1996            kind: ObligationKind,
1997            duration_ns: u64,
1998        ) => ObligationCommit,
1999            TraceData::Obligation {
2000                obligation,
2001                task,
2002                region,
2003                kind,
2004                state: ObligationState::Committed,
2005                duration_ns: Some(duration_ns),
2006                abort_reason: None,
2007            };
2008        /// Creates an obligation abort event.
2009        #[allow(clippy::too_many_arguments)]
2010        obligation_abort(
2011            obligation: ObligationId,
2012            task: TaskId,
2013            region: RegionId,
2014            kind: ObligationKind,
2015            duration_ns: u64,
2016            reason: ObligationAbortReason,
2017        ) => ObligationAbort,
2018            TraceData::Obligation {
2019                obligation,
2020                task,
2021                region,
2022                kind,
2023                state: ObligationState::Aborted,
2024                duration_ns: Some(duration_ns),
2025                abort_reason: Some(reason),
2026            };
2027        /// Creates an obligation leak event.
2028        obligation_leak(
2029            obligation: ObligationId,
2030            task: TaskId,
2031            region: RegionId,
2032            kind: ObligationKind,
2033            duration_ns: u64,
2034        ) => ObligationLeak,
2035            TraceData::Obligation {
2036                obligation,
2037                task,
2038                region,
2039                kind,
2040                state: ObligationState::Leaked,
2041                duration_ns: Some(duration_ns),
2042                abort_reason: None,
2043            };
2044        /// Creates a monitor created event.
2045        monitor_created(
2046            monitor_ref: u64,
2047            watcher: TaskId,
2048            watcher_region: RegionId,
2049            monitored: TaskId,
2050        ) => MonitorCreated,
2051            TraceData::Monitor { monitor_ref, watcher, watcher_region, monitored };
2052        /// Creates a monitor dropped event.
2053        monitor_dropped(
2054            monitor_ref: u64,
2055            watcher: TaskId,
2056            watcher_region: RegionId,
2057            monitored: TaskId,
2058        ) => MonitorDropped,
2059            TraceData::Monitor { monitor_ref, watcher, watcher_region, monitored };
2060        /// Creates a down delivered event.
2061        down_delivered(
2062            monitor_ref: u64,
2063            watcher: TaskId,
2064            monitored: TaskId,
2065            completion_vt: Time,
2066            reason: DownReason,
2067        ) => DownDelivered,
2068            TraceData::Down { monitor_ref, watcher, monitored, completion_vt, reason };
2069        /// Creates a link created event.
2070        link_created(
2071            link_ref: u64,
2072            task_a: TaskId,
2073            region_a: RegionId,
2074            task_b: TaskId,
2075            region_b: RegionId,
2076        ) => LinkCreated,
2077            TraceData::Link { link_ref, task_a, region_a, task_b, region_b };
2078        /// Creates a link dropped event.
2079        link_dropped(
2080            link_ref: u64,
2081            task_a: TaskId,
2082            region_a: RegionId,
2083            task_b: TaskId,
2084            region_b: RegionId,
2085        ) => LinkDropped,
2086            TraceData::Link { link_ref, task_a, region_a, task_b, region_b };
2087        /// Creates an exit delivered event.
2088        exit_delivered(link_ref: u64, from: TaskId, to: TaskId, failure_vt: Time, reason: DownReason)
2089            => ExitDelivered, TraceData::Exit { link_ref, from, to, failure_vt, reason };
2090        /// Creates a user trace event.
2091        user_trace(message: impl Into<String>) => UserTrace, TraceData::Message(message.into());
2092    }
2093}
2094
2095impl fmt::Display for TraceEvent {
2096    #[allow(clippy::too_many_lines)]
2097    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2098        write!(f, "[{:06}] {} {}", self.seq, self.time, self.kind)?;
2099        if let Some(ref lt) = self.logical_time {
2100            write!(f, " @{lt:?}")?;
2101        }
2102        match &self.data {
2103            TraceData::None => {}
2104            TraceData::Task { task, region } => write!(f, " {task} in {region}")?,
2105            TraceData::Budget {
2106                task,
2107                region,
2108                protocol,
2109                outcome,
2110                ..
2111            } => {
2112                write!(f, " {task} in {region} [{protocol}]")?;
2113                if let Some(outcome) = outcome {
2114                    write!(f, " outcome={outcome}")?;
2115                }
2116            }
2117            TraceData::Region { region, parent } => {
2118                write!(f, " {region}")?;
2119                if let Some(p) = parent {
2120                    write!(f, " (parent: {p})")?;
2121                }
2122            }
2123            TraceData::Obligation {
2124                obligation,
2125                task,
2126                region,
2127                kind,
2128                state,
2129                duration_ns,
2130                abort_reason,
2131            } => {
2132                write!(
2133                    f,
2134                    " {obligation} {kind:?} {state:?} holder={task} region={region}"
2135                )?;
2136                if let Some(duration) = duration_ns {
2137                    write!(f, " duration={duration}ns")?;
2138                }
2139                if let Some(reason) = abort_reason {
2140                    write!(f, " abort_reason={reason}")?;
2141                }
2142            }
2143            TraceData::Cancel {
2144                task,
2145                region,
2146                reason,
2147            } => write!(f, " {task} in {region} reason={reason}")?,
2148            TraceData::Worker {
2149                worker_id,
2150                job_id,
2151                decision_seq,
2152                replay_hash,
2153                task,
2154                region,
2155                obligation,
2156            } => write!(
2157                f,
2158                " worker={worker_id} job_id={job_id} {task} in {region} obligation={obligation} decision_seq={decision_seq} replay_hash={replay_hash}"
2159            )?,
2160            TraceData::RegionCancel { region, reason } => {
2161                write!(f, " {region} reason={reason}")?;
2162            }
2163            TraceData::Time { old, new } => write!(f, " {old} -> {new}")?,
2164            TraceData::Timer { timer_id, deadline } => {
2165                write!(f, " timer={timer_id}")?;
2166                if let Some(dl) = deadline {
2167                    write!(f, " deadline={dl}")?;
2168                }
2169            }
2170            TraceData::IoRequested { token, interest } => {
2171                write!(f, " io_requested token={token} interest={interest}")?;
2172            }
2173            TraceData::IoReady { token, readiness } => {
2174                write!(f, " io_ready token={token} readiness={readiness}")?;
2175            }
2176            TraceData::IoResult { token, bytes } => {
2177                write!(f, " io_result token={token} bytes={bytes}")?;
2178            }
2179            TraceData::IoError { token, kind } => {
2180                write!(f, " io_error token={token} kind={kind}")?;
2181            }
2182            TraceData::RngSeed { seed } => write!(f, " rng_seed={seed}")?,
2183            TraceData::RngValue { value } => write!(f, " rng_value={value}")?,
2184            TraceData::Checkpoint {
2185                sequence,
2186                active_tasks,
2187                active_regions,
2188            } => write!(
2189                f,
2190                " checkpoint seq={sequence} tasks={active_tasks} regions={active_regions}"
2191            )?,
2192            TraceData::Futurelock {
2193                task,
2194                region,
2195                idle_steps,
2196                held,
2197            } => {
2198                write!(f, " futurelock: {task} in {region} idle={idle_steps}")?;
2199                write!(f, " held=[")?;
2200                for (i, (oid, kind)) in held.iter().enumerate() {
2201                    if i > 0 {
2202                        write!(f, ", ")?;
2203                    }
2204                    write!(f, "{oid}:{kind:?}")?;
2205                }
2206                write!(f, "]")?;
2207            }
2208            TraceData::Monitor {
2209                monitor_ref,
2210                watcher,
2211                watcher_region,
2212                monitored,
2213            } => write!(
2214                f,
2215                " monitor_ref={monitor_ref} watcher={watcher} watcher_region={watcher_region} monitored={monitored}"
2216            )?,
2217            TraceData::Down {
2218                monitor_ref,
2219                watcher,
2220                monitored,
2221                completion_vt,
2222                reason,
2223            } => write!(
2224                f,
2225                " down monitor_ref={monitor_ref} watcher={watcher} monitored={monitored} completion_vt={completion_vt} reason={reason}"
2226            )?,
2227            TraceData::Link {
2228                link_ref,
2229                task_a,
2230                region_a,
2231                task_b,
2232                region_b,
2233            } => write!(
2234                f,
2235                " link_ref={link_ref} a={task_a} region_a={region_a} b={task_b} region_b={region_b}"
2236            )?,
2237            TraceData::Exit {
2238                link_ref,
2239                from,
2240                to,
2241                failure_vt,
2242                reason,
2243            } => write!(
2244                f,
2245                " exit link_ref={link_ref} from={from} to={to} failure_vt={failure_vt} reason={reason}"
2246            )?,
2247            TraceData::Message(msg) => write!(f, " \"{msg}\"")?,
2248            TraceData::Chaos { kind, task, detail } => {
2249                write!(f, " chaos:{kind}")?;
2250                if let Some(t) = task {
2251                    write!(f, " task={t}")?;
2252                }
2253                write!(f, " {detail}")?;
2254            }
2255        }
2256        Ok(())
2257    }
2258}
2259
2260#[cfg(test)]
2261mod tests {
2262    #![allow(
2263        clippy::pedantic,
2264        clippy::nursery,
2265        clippy::expect_fun_call,
2266        clippy::map_unwrap_or,
2267        clippy::cast_possible_wrap,
2268        clippy::future_not_send
2269    )]
2270    use super::*;
2271    use crate::monitor::DownReason;
2272    use crate::record::{ObligationAbortReason, ObligationKind, ObligationState};
2273    use crate::trace::distributed::LamportTime;
2274    use crate::types::CancelReason;
2275    use serde_json::Value;
2276    use std::collections::BTreeSet;
2277
2278    fn task(n: u32) -> TaskId {
2279        TaskId::new_for_test(n, 1)
2280    }
2281    fn region(n: u32) -> RegionId {
2282        RegionId::new_for_test(n, 1)
2283    }
2284    fn obligation(n: u32) -> ObligationId {
2285        ObligationId::new_for_test(n, 1)
2286    }
2287
2288    fn scrub_browser_trace_fields(fields: &std::collections::BTreeMap<String, String>) -> Value {
2289        let mut value = serde_json::to_value(fields).expect("serialize browser trace fields");
2290        let obj = value
2291            .as_object_mut()
2292            .expect("browser trace fields serialize to an object");
2293
2294        for key in [
2295            "capture_host_time_ns",
2296            "capture_replay_key",
2297            "completion_vt",
2298            "deadline",
2299            "failure_vt",
2300            "from",
2301            "monitored",
2302            "new",
2303            "old",
2304            "parent",
2305            "region_a",
2306            "region_b",
2307            "seq",
2308            "task_a",
2309            "task_b",
2310            "to",
2311            "time_ns",
2312            "trace_id",
2313            "task",
2314            "region",
2315            "obligation",
2316            "sequence_group",
2317            "watcher",
2318            "watcher_region",
2319        ] {
2320            if obj.contains_key(key) {
2321                obj.insert(key.to_string(), Value::String(format!("[{key}]")));
2322            }
2323        }
2324
2325        value
2326    }
2327
2328    // ── TraceEventKind basics ──────────────────────────────────────
2329
2330    #[test]
2331    fn trace_event_version_is_set() {
2332        let event = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
2333        assert_eq!(event.version, TRACE_EVENT_SCHEMA_VERSION);
2334    }
2335
2336    #[test]
2337    fn trace_event_kind_stable_names_are_unique() {
2338        let mut names = BTreeSet::new();
2339        for kind in TraceEventKind::ALL {
2340            assert!(names.insert(kind.stable_name()));
2341        }
2342    }
2343
2344    #[test]
2345    fn trace_event_taxonomy_is_documented() {
2346        const DOC: &str = include_str!("../../docs/spork_deterministic_ordering.md");
2347        for kind in TraceEventKind::ALL {
2348            let marker = format!("- `{}` => `{}`", kind.stable_name(), kind.required_fields());
2349            assert!(
2350                DOC.contains(&marker),
2351                "missing taxonomy entry in docs/spork_deterministic_ordering.md for {}",
2352                kind.stable_name()
2353            );
2354        }
2355    }
2356
2357    #[test]
2358    fn all_array_has_42_kinds() {
2359        assert_eq!(TraceEventKind::ALL.len(), 45);
2360    }
2361
2362    #[test]
2363    fn all_kinds_are_distinct() {
2364        let set: BTreeSet<TraceEventKind> = TraceEventKind::ALL.iter().copied().collect();
2365        assert_eq!(set.len(), TraceEventKind::ALL.len());
2366    }
2367
2368    #[test]
2369    fn display_delegates_to_stable_name() {
2370        for kind in TraceEventKind::ALL {
2371            assert_eq!(format!("{kind}"), kind.stable_name());
2372        }
2373    }
2374
2375    #[test]
2376    fn kind_ord_is_consistent_with_eq() {
2377        for a in TraceEventKind::ALL {
2378            for b in TraceEventKind::ALL {
2379                if a == b {
2380                    assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
2381                } else {
2382                    assert_ne!(a.cmp(&b), std::cmp::Ordering::Equal);
2383                }
2384            }
2385        }
2386    }
2387
2388    #[test]
2389    fn required_fields_non_empty_for_all() {
2390        for kind in TraceEventKind::ALL {
2391            assert!(
2392                !kind.required_fields().is_empty(),
2393                "required_fields empty for {kind:?}"
2394            );
2395        }
2396    }
2397
2398    // ── Constructor tests ──────────────────────────────────────────
2399
2400    #[test]
2401    fn spawn_constructor() {
2402        let e = TraceEvent::spawn(1, Time::ZERO, task(10), region(20));
2403        assert_eq!(e.kind, TraceEventKind::Spawn);
2404        assert_eq!(e.seq, 1);
2405        assert_eq!(
2406            e.data,
2407            TraceData::Task {
2408                task: task(10),
2409                region: region(20)
2410            }
2411        );
2412    }
2413
2414    #[test]
2415    fn schedule_constructor() {
2416        let e = TraceEvent::schedule(2, Time::from_nanos(100), task(1), region(2));
2417        assert_eq!(e.kind, TraceEventKind::Schedule);
2418        assert_eq!(
2419            e.data,
2420            TraceData::Task {
2421                task: task(1),
2422                region: region(2)
2423            }
2424        );
2425    }
2426
2427    #[test]
2428    fn yield_task_constructor() {
2429        let e = TraceEvent::yield_task(3, Time::ZERO, task(5), region(6));
2430        assert_eq!(e.kind, TraceEventKind::Yield);
2431        assert_eq!(
2432            e.data,
2433            TraceData::Task {
2434                task: task(5),
2435                region: region(6)
2436            }
2437        );
2438    }
2439
2440    #[test]
2441    fn wake_constructor() {
2442        let e = TraceEvent::wake(4, Time::ZERO, task(7), region(8));
2443        assert_eq!(e.kind, TraceEventKind::Wake);
2444        assert_eq!(
2445            e.data,
2446            TraceData::Task {
2447                task: task(7),
2448                region: region(8)
2449            }
2450        );
2451    }
2452
2453    #[test]
2454    fn poll_constructor() {
2455        let e = TraceEvent::poll(5, Time::ZERO, task(9), region(10));
2456        assert_eq!(e.kind, TraceEventKind::Poll);
2457        assert_eq!(
2458            e.data,
2459            TraceData::Task {
2460                task: task(9),
2461                region: region(10)
2462            }
2463        );
2464    }
2465
2466    #[test]
2467    fn complete_constructor() {
2468        let e = TraceEvent::complete(6, Time::ZERO, task(11), region(12));
2469        assert_eq!(e.kind, TraceEventKind::Complete);
2470        assert_eq!(
2471            e.data,
2472            TraceData::Task {
2473                task: task(11),
2474                region: region(12)
2475            }
2476        );
2477    }
2478
2479    #[test]
2480    fn cancel_request_constructor() {
2481        let e =
2482            TraceEvent::cancel_request(7, Time::ZERO, task(1), region(2), CancelReason::timeout());
2483        assert_eq!(e.kind, TraceEventKind::CancelRequest);
2484        match &e.data {
2485            TraceData::Cancel {
2486                task: t,
2487                region: r,
2488                reason,
2489            } => {
2490                assert_eq!(*t, task(1));
2491                assert_eq!(*r, region(2));
2492                assert_eq!(reason.kind(), crate::types::CancelKind::Timeout);
2493            }
2494            other => panic!("expected Cancel, got {other:?}"),
2495        }
2496    }
2497
2498    #[test]
2499    fn region_created_constructor_with_parent() {
2500        let e = TraceEvent::region_created(8, Time::ZERO, region(3), Some(region(1)));
2501        assert_eq!(e.kind, TraceEventKind::RegionCreated);
2502        assert_eq!(
2503            e.data,
2504            TraceData::Region {
2505                region: region(3),
2506                parent: Some(region(1))
2507            }
2508        );
2509    }
2510
2511    #[test]
2512    fn region_created_constructor_without_parent() {
2513        let e = TraceEvent::region_created(9, Time::ZERO, region(3), None);
2514        assert_eq!(e.kind, TraceEventKind::RegionCreated);
2515        assert_eq!(
2516            e.data,
2517            TraceData::Region {
2518                region: region(3),
2519                parent: None
2520            }
2521        );
2522    }
2523
2524    #[test]
2525    fn region_cancelled_constructor() {
2526        let e = TraceEvent::region_cancelled(10, Time::ZERO, region(5), CancelReason::shutdown());
2527        assert_eq!(e.kind, TraceEventKind::RegionCancelled);
2528        match &e.data {
2529            TraceData::RegionCancel { region: r, .. } => assert_eq!(*r, region(5)),
2530            other => panic!("expected RegionCancel, got {other:?}"),
2531        }
2532    }
2533
2534    #[test]
2535    fn time_advance_constructor() {
2536        let e =
2537            TraceEvent::time_advance(11, Time::ZERO, Time::from_nanos(0), Time::from_nanos(100));
2538        assert_eq!(e.kind, TraceEventKind::TimeAdvance);
2539        assert_eq!(
2540            e.data,
2541            TraceData::Time {
2542                old: Time::from_nanos(0),
2543                new: Time::from_nanos(100)
2544            }
2545        );
2546    }
2547
2548    #[test]
2549    fn timer_scheduled_constructor() {
2550        let e = TraceEvent::timer_scheduled(12, Time::ZERO, 42, Time::from_millis(500));
2551        assert_eq!(e.kind, TraceEventKind::TimerScheduled);
2552        assert_eq!(
2553            e.data,
2554            TraceData::Timer {
2555                timer_id: 42,
2556                deadline: Some(Time::from_millis(500))
2557            }
2558        );
2559    }
2560
2561    #[test]
2562    fn timer_fired_constructor() {
2563        let e = TraceEvent::timer_fired(13, Time::ZERO, 42);
2564        assert_eq!(e.kind, TraceEventKind::TimerFired);
2565        assert_eq!(
2566            e.data,
2567            TraceData::Timer {
2568                timer_id: 42,
2569                deadline: None
2570            }
2571        );
2572    }
2573
2574    #[test]
2575    fn timer_cancelled_constructor() {
2576        let e = TraceEvent::timer_cancelled(14, Time::ZERO, 42);
2577        assert_eq!(e.kind, TraceEventKind::TimerCancelled);
2578        assert_eq!(
2579            e.data,
2580            TraceData::Timer {
2581                timer_id: 42,
2582                deadline: None
2583            }
2584        );
2585    }
2586
2587    #[test]
2588    fn io_requested_constructor() {
2589        let e = TraceEvent::io_requested(15, Time::ZERO, 99, 0x03);
2590        assert_eq!(e.kind, TraceEventKind::IoRequested);
2591        assert_eq!(
2592            e.data,
2593            TraceData::IoRequested {
2594                token: 99,
2595                interest: 0x03
2596            }
2597        );
2598    }
2599
2600    #[test]
2601    fn io_ready_constructor() {
2602        let e = TraceEvent::io_ready(16, Time::ZERO, 99, 0x01);
2603        assert_eq!(e.kind, TraceEventKind::IoReady);
2604        assert_eq!(
2605            e.data,
2606            TraceData::IoReady {
2607                token: 99,
2608                readiness: 0x01
2609            }
2610        );
2611    }
2612
2613    #[test]
2614    fn io_result_constructor() {
2615        let e = TraceEvent::io_result(17, Time::ZERO, 99, 1024);
2616        assert_eq!(e.kind, TraceEventKind::IoResult);
2617        assert_eq!(
2618            e.data,
2619            TraceData::IoResult {
2620                token: 99,
2621                bytes: 1024
2622            }
2623        );
2624    }
2625
2626    #[test]
2627    fn io_result_negative_bytes() {
2628        let e = TraceEvent::io_result(18, Time::ZERO, 99, -1);
2629        assert_eq!(
2630            e.data,
2631            TraceData::IoResult {
2632                token: 99,
2633                bytes: -1
2634            }
2635        );
2636    }
2637
2638    #[test]
2639    fn io_error_constructor() {
2640        let e = TraceEvent::io_error(19, Time::ZERO, 99, 13);
2641        assert_eq!(e.kind, TraceEventKind::IoError);
2642        assert_eq!(
2643            e.data,
2644            TraceData::IoError {
2645                token: 99,
2646                kind: 13
2647            }
2648        );
2649    }
2650
2651    #[test]
2652    fn rng_seed_constructor() {
2653        let e = TraceEvent::rng_seed(20, Time::ZERO, 0xDEAD_BEEF);
2654        assert_eq!(e.kind, TraceEventKind::RngSeed);
2655        assert_eq!(e.data, TraceData::RngSeed { seed: 0xDEAD_BEEF });
2656    }
2657
2658    #[test]
2659    fn rng_value_constructor() {
2660        let e = TraceEvent::rng_value(21, Time::ZERO, 42);
2661        assert_eq!(e.kind, TraceEventKind::RngValue);
2662        assert_eq!(e.data, TraceData::RngValue { value: 42 });
2663    }
2664
2665    #[test]
2666    fn checkpoint_constructor() {
2667        let e = TraceEvent::checkpoint(22, Time::ZERO, 7, 3, 2);
2668        assert_eq!(e.kind, TraceEventKind::Checkpoint);
2669        assert_eq!(
2670            e.data,
2671            TraceData::Checkpoint {
2672                sequence: 7,
2673                active_tasks: 3,
2674                active_regions: 2
2675            }
2676        );
2677    }
2678
2679    #[test]
2680    fn obligation_reserve_constructor() {
2681        let e = TraceEvent::obligation_reserve(
2682            23,
2683            Time::ZERO,
2684            obligation(1),
2685            task(2),
2686            region(3),
2687            ObligationKind::SendPermit,
2688        );
2689        assert_eq!(e.kind, TraceEventKind::ObligationReserve);
2690        match &e.data {
2691            TraceData::Obligation {
2692                state,
2693                duration_ns,
2694                abort_reason,
2695                ..
2696            } => {
2697                assert_eq!(*state, ObligationState::Reserved);
2698                assert_eq!(*duration_ns, None);
2699                assert_eq!(*abort_reason, None);
2700            }
2701            other => panic!("expected Obligation, got {other:?}"),
2702        }
2703    }
2704
2705    #[test]
2706    fn obligation_commit_constructor() {
2707        let e = TraceEvent::obligation_commit(
2708            24,
2709            Time::ZERO,
2710            obligation(1),
2711            task(2),
2712            region(3),
2713            ObligationKind::Ack,
2714            5000,
2715        );
2716        assert_eq!(e.kind, TraceEventKind::ObligationCommit);
2717        match &e.data {
2718            TraceData::Obligation {
2719                state,
2720                duration_ns,
2721                abort_reason,
2722                ..
2723            } => {
2724                assert_eq!(*state, ObligationState::Committed);
2725                assert_eq!(*duration_ns, Some(5000));
2726                assert_eq!(*abort_reason, None);
2727            }
2728            other => panic!("expected Obligation, got {other:?}"),
2729        }
2730    }
2731
2732    #[test]
2733    fn obligation_abort_constructor() {
2734        let e = TraceEvent::obligation_abort(
2735            25,
2736            Time::ZERO,
2737            obligation(1),
2738            task(2),
2739            region(3),
2740            ObligationKind::Lease,
2741            3000,
2742            ObligationAbortReason::Cancel,
2743        );
2744        assert_eq!(e.kind, TraceEventKind::ObligationAbort);
2745        match &e.data {
2746            TraceData::Obligation {
2747                state,
2748                duration_ns,
2749                abort_reason,
2750                ..
2751            } => {
2752                assert_eq!(*state, ObligationState::Aborted);
2753                assert_eq!(*duration_ns, Some(3000));
2754                assert_eq!(*abort_reason, Some(ObligationAbortReason::Cancel));
2755            }
2756            other => panic!("expected Obligation, got {other:?}"),
2757        }
2758    }
2759
2760    #[test]
2761    fn obligation_leak_constructor() {
2762        let e = TraceEvent::obligation_leak(
2763            26,
2764            Time::ZERO,
2765            obligation(1),
2766            task(2),
2767            region(3),
2768            ObligationKind::IoOp,
2769            9000,
2770        );
2771        assert_eq!(e.kind, TraceEventKind::ObligationLeak);
2772        match &e.data {
2773            TraceData::Obligation {
2774                state,
2775                duration_ns,
2776                abort_reason,
2777                ..
2778            } => {
2779                assert_eq!(*state, ObligationState::Leaked);
2780                assert_eq!(*duration_ns, Some(9000));
2781                assert_eq!(*abort_reason, None);
2782            }
2783            other => panic!("expected Obligation, got {other:?}"),
2784        }
2785    }
2786
2787    #[test]
2788    fn monitor_created_constructor() {
2789        let e = TraceEvent::monitor_created(27, Time::ZERO, 100, task(1), region(2), task(3));
2790        assert_eq!(e.kind, TraceEventKind::MonitorCreated);
2791        assert_eq!(
2792            e.data,
2793            TraceData::Monitor {
2794                monitor_ref: 100,
2795                watcher: task(1),
2796                watcher_region: region(2),
2797                monitored: task(3),
2798            }
2799        );
2800    }
2801
2802    #[test]
2803    fn monitor_dropped_constructor() {
2804        let e = TraceEvent::monitor_dropped(28, Time::ZERO, 100, task(1), region(2), task(3));
2805        assert_eq!(e.kind, TraceEventKind::MonitorDropped);
2806        assert_eq!(
2807            e.data,
2808            TraceData::Monitor {
2809                monitor_ref: 100,
2810                watcher: task(1),
2811                watcher_region: region(2),
2812                monitored: task(3),
2813            }
2814        );
2815    }
2816
2817    #[test]
2818    fn down_delivered_constructor() {
2819        let e = TraceEvent::down_delivered(
2820            29,
2821            Time::ZERO,
2822            100,
2823            task(1),
2824            task(3),
2825            Time::from_nanos(500),
2826            DownReason::Normal,
2827        );
2828        assert_eq!(e.kind, TraceEventKind::DownDelivered);
2829        assert_eq!(
2830            e.data,
2831            TraceData::Down {
2832                monitor_ref: 100,
2833                watcher: task(1),
2834                monitored: task(3),
2835                completion_vt: Time::from_nanos(500),
2836                reason: DownReason::Normal,
2837            }
2838        );
2839    }
2840
2841    #[test]
2842    fn link_created_constructor() {
2843        let e =
2844            TraceEvent::link_created(30, Time::ZERO, 200, task(1), region(2), task(3), region(4));
2845        assert_eq!(e.kind, TraceEventKind::LinkCreated);
2846        assert_eq!(
2847            e.data,
2848            TraceData::Link {
2849                link_ref: 200,
2850                task_a: task(1),
2851                region_a: region(2),
2852                task_b: task(3),
2853                region_b: region(4),
2854            }
2855        );
2856    }
2857
2858    #[test]
2859    fn link_dropped_constructor() {
2860        let e =
2861            TraceEvent::link_dropped(31, Time::ZERO, 200, task(1), region(2), task(3), region(4));
2862        assert_eq!(e.kind, TraceEventKind::LinkDropped);
2863        assert_eq!(
2864            e.data,
2865            TraceData::Link {
2866                link_ref: 200,
2867                task_a: task(1),
2868                region_a: region(2),
2869                task_b: task(3),
2870                region_b: region(4),
2871            }
2872        );
2873    }
2874
2875    #[test]
2876    fn exit_delivered_constructor() {
2877        let e = TraceEvent::exit_delivered(
2878            32,
2879            Time::ZERO,
2880            200,
2881            task(1),
2882            task(3),
2883            Time::from_nanos(999),
2884            DownReason::Normal,
2885        );
2886        assert_eq!(e.kind, TraceEventKind::ExitDelivered);
2887        assert_eq!(
2888            e.data,
2889            TraceData::Exit {
2890                link_ref: 200,
2891                from: task(1),
2892                to: task(3),
2893                failure_vt: Time::from_nanos(999),
2894                reason: DownReason::Normal,
2895            }
2896        );
2897    }
2898
2899    #[test]
2900    fn user_trace_constructor() {
2901        let e = TraceEvent::user_trace(33, Time::ZERO, "hello");
2902        assert_eq!(e.kind, TraceEventKind::UserTrace);
2903        assert_eq!(e.data, TraceData::Message("hello".into()));
2904    }
2905
2906    #[test]
2907    fn user_trace_accepts_string() {
2908        let e = TraceEvent::user_trace(34, Time::ZERO, String::from("world"));
2909        assert_eq!(e.data, TraceData::Message("world".into()));
2910    }
2911
2912    #[test]
2913    fn worker_lifecycle_constructors_preserve_payload_shape() {
2914        let e = TraceEvent::worker_cancel_requested(
2915            35,
2916            Time::ZERO,
2917            "worker-a",
2918            77,
2919            91,
2920            0x00C0_FFEE,
2921            task(9),
2922            region(10),
2923            obligation(11),
2924        );
2925        assert_eq!(e.kind, TraceEventKind::WorkerCancelRequested);
2926        assert_eq!(
2927            e.data,
2928            TraceData::Worker {
2929                worker_id: "worker-a".into(),
2930                job_id: 77,
2931                decision_seq: 91,
2932                replay_hash: 0x00C0_FFEE,
2933                task: task(9),
2934                region: region(10),
2935                obligation: obligation(11),
2936            }
2937        );
2938    }
2939
2940    // ── with_logical_time ──────────────────────────────────────────
2941
2942    #[test]
2943    fn with_logical_time_sets_field() {
2944        let lt = LogicalTime::Lamport(LamportTime::from_raw(42));
2945        let e = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None)
2946            .with_logical_time(lt);
2947        assert_eq!(
2948            e.logical_time,
2949            Some(LogicalTime::Lamport(LamportTime::from_raw(42)))
2950        );
2951    }
2952
2953    #[test]
2954    fn default_logical_time_is_none() {
2955        let e = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
2956        assert_eq!(e.logical_time, None);
2957    }
2958
2959    // ── Display formatting ─────────────────────────────────────────
2960
2961    #[test]
2962    fn display_task_event() {
2963        let e = TraceEvent::spawn(1, Time::ZERO, task(10), region(20));
2964        let s = format!("{e}");
2965        assert!(s.contains("spawn"), "expected 'spawn' in {s}");
2966        assert!(s.contains("[000001]"), "expected seq in {s}");
2967    }
2968
2969    #[test]
2970    fn display_region_with_parent() {
2971        let e = TraceEvent::region_created(2, Time::ZERO, region(3), Some(region(1)));
2972        let s = format!("{e}");
2973        assert!(s.contains("region_created"), "expected kind in {s}");
2974        assert!(s.contains("parent"), "expected parent in {s}");
2975    }
2976
2977    #[test]
2978    fn display_region_without_parent() {
2979        let e = TraceEvent::region_created(3, Time::ZERO, region(3), None);
2980        let s = format!("{e}");
2981        assert!(s.contains("region_created"), "expected kind in {s}");
2982        assert!(!s.contains("parent"), "should not contain parent: {s}");
2983    }
2984
2985    #[test]
2986    fn display_obligation_with_duration_and_abort() {
2987        let e = TraceEvent::obligation_abort(
2988            4,
2989            Time::ZERO,
2990            obligation(1),
2991            task(2),
2992            region(3),
2993            ObligationKind::Lease,
2994            5000,
2995            ObligationAbortReason::Error,
2996        );
2997        let s = format!("{e}");
2998        assert!(s.contains("obligation_abort"), "expected kind in {s}");
2999        assert!(s.contains("duration=5000ns"), "expected duration in {s}");
3000        assert!(s.contains("abort_reason="), "expected abort_reason in {s}");
3001    }
3002
3003    #[test]
3004    fn display_obligation_reserve_no_duration() {
3005        let e = TraceEvent::obligation_reserve(
3006            5,
3007            Time::ZERO,
3008            obligation(1),
3009            task(2),
3010            region(3),
3011            ObligationKind::SendPermit,
3012        );
3013        let s = format!("{e}");
3014        assert!(
3015            !s.contains("duration="),
3016            "reserve should not show duration: {s}"
3017        );
3018        assert!(
3019            !s.contains("abort_reason="),
3020            "reserve should not show abort_reason: {s}"
3021        );
3022    }
3023
3024    #[test]
3025    fn display_cancel_event() {
3026        let e =
3027            TraceEvent::cancel_request(6, Time::ZERO, task(1), region(2), CancelReason::timeout());
3028        let s = format!("{e}");
3029        assert!(s.contains("cancel_request"), "expected kind in {s}");
3030        assert!(s.contains("reason="), "expected reason in {s}");
3031    }
3032
3033    #[test]
3034    fn display_region_cancel() {
3035        let e = TraceEvent::region_cancelled(7, Time::ZERO, region(5), CancelReason::shutdown());
3036        let s = format!("{e}");
3037        assert!(s.contains("region_cancelled"), "expected kind in {s}");
3038        assert!(s.contains("reason="), "expected reason in {s}");
3039    }
3040
3041    #[test]
3042    fn display_time_advance() {
3043        let e = TraceEvent::time_advance(8, Time::ZERO, Time::from_nanos(0), Time::from_nanos(100));
3044        let s = format!("{e}");
3045        assert!(s.contains("time_advance"), "expected kind in {s}");
3046        assert!(s.contains("->"), "expected arrow in {s}");
3047    }
3048
3049    #[test]
3050    fn display_timer_with_deadline() {
3051        let e = TraceEvent::timer_scheduled(9, Time::ZERO, 42, Time::from_millis(500));
3052        let s = format!("{e}");
3053        assert!(s.contains("timer=42"), "expected timer id in {s}");
3054        assert!(s.contains("deadline="), "expected deadline in {s}");
3055    }
3056
3057    #[test]
3058    fn display_timer_without_deadline() {
3059        let e = TraceEvent::timer_fired(10, Time::ZERO, 42);
3060        let s = format!("{e}");
3061        assert!(s.contains("timer=42"), "expected timer id in {s}");
3062        assert!(!s.contains("deadline="), "should not show deadline: {s}");
3063    }
3064
3065    #[test]
3066    fn display_io_requested() {
3067        let e = TraceEvent::io_requested(11, Time::ZERO, 99, 0x03);
3068        let s = format!("{e}");
3069        assert!(s.contains("io_requested"), "expected kind in {s}");
3070        assert!(s.contains("token=99"), "expected token in {s}");
3071        assert!(s.contains("interest=3"), "expected interest in {s}");
3072    }
3073
3074    #[test]
3075    fn display_io_ready() {
3076        let e = TraceEvent::io_ready(12, Time::ZERO, 99, 0x01);
3077        let s = format!("{e}");
3078        assert!(s.contains("io_ready"), "expected kind in {s}");
3079        assert!(s.contains("readiness=1"), "expected readiness in {s}");
3080    }
3081
3082    #[test]
3083    fn display_io_result() {
3084        let e = TraceEvent::io_result(13, Time::ZERO, 99, 1024);
3085        let s = format!("{e}");
3086        assert!(s.contains("io_result"), "expected kind in {s}");
3087        assert!(s.contains("bytes=1024"), "expected bytes in {s}");
3088    }
3089
3090    #[test]
3091    fn display_io_error() {
3092        let e = TraceEvent::io_error(14, Time::ZERO, 99, 13);
3093        let s = format!("{e}");
3094        assert!(s.contains("io_error"), "expected kind in {s}");
3095        assert!(s.contains("kind=13"), "expected kind in {s}");
3096    }
3097
3098    #[test]
3099    fn display_rng_seed() {
3100        let e = TraceEvent::rng_seed(15, Time::ZERO, 0xCAFE);
3101        let s = format!("{e}");
3102        assert!(s.contains("rng_seed=51966"), "expected seed in {s}");
3103    }
3104
3105    #[test]
3106    fn display_rng_value() {
3107        let e = TraceEvent::rng_value(16, Time::ZERO, 42);
3108        let s = format!("{e}");
3109        assert!(s.contains("rng_value=42"), "expected value in {s}");
3110    }
3111
3112    #[test]
3113    fn display_checkpoint() {
3114        let e = TraceEvent::checkpoint(17, Time::ZERO, 7, 3, 2);
3115        let s = format!("{e}");
3116        assert!(s.contains("checkpoint"), "expected kind in {s}");
3117        assert!(s.contains("seq=7"), "expected seq in {s}");
3118        assert!(s.contains("tasks=3"), "expected tasks in {s}");
3119        assert!(s.contains("regions=2"), "expected regions in {s}");
3120    }
3121
3122    #[test]
3123    fn display_futurelock_empty_held() {
3124        let e = TraceEvent::new(
3125            18,
3126            Time::ZERO,
3127            TraceEventKind::FuturelockDetected,
3128            TraceData::Futurelock {
3129                task: task(1),
3130                region: region(2),
3131                idle_steps: 10,
3132                held: vec![],
3133            },
3134        );
3135        let s = format!("{e}");
3136        assert!(s.contains("futurelock"), "expected kind in {s}");
3137        assert!(s.contains("idle=10"), "expected idle in {s}");
3138        assert!(s.contains("held=[]"), "expected empty held in {s}");
3139    }
3140
3141    #[test]
3142    fn display_futurelock_with_held() {
3143        let e = TraceEvent::new(
3144            19,
3145            Time::ZERO,
3146            TraceEventKind::FuturelockDetected,
3147            TraceData::Futurelock {
3148                task: task(1),
3149                region: region(2),
3150                idle_steps: 5,
3151                held: vec![(obligation(10), ObligationKind::SendPermit)],
3152            },
3153        );
3154        let s = format!("{e}");
3155        assert!(s.contains("held=["), "expected held in {s}");
3156        assert!(s.contains("SendPermit"), "expected kind in {s}");
3157    }
3158
3159    #[test]
3160    fn display_monitor() {
3161        let e = TraceEvent::monitor_created(20, Time::ZERO, 100, task(1), region(2), task(3));
3162        let s = format!("{e}");
3163        assert!(s.contains("monitor_ref=100"), "expected ref in {s}");
3164    }
3165
3166    #[test]
3167    fn display_down() {
3168        let e = TraceEvent::down_delivered(
3169            21,
3170            Time::ZERO,
3171            100,
3172            task(1),
3173            task(3),
3174            Time::from_nanos(500),
3175            DownReason::Normal,
3176        );
3177        let s = format!("{e}");
3178        assert!(s.contains("down"), "expected down in {s}");
3179        assert!(s.contains("monitor_ref=100"), "expected ref in {s}");
3180    }
3181
3182    #[test]
3183    fn display_link() {
3184        let e =
3185            TraceEvent::link_created(22, Time::ZERO, 200, task(1), region(2), task(3), region(4));
3186        let s = format!("{e}");
3187        assert!(s.contains("link_ref=200"), "expected ref in {s}");
3188    }
3189
3190    #[test]
3191    fn display_exit() {
3192        let e = TraceEvent::exit_delivered(
3193            23,
3194            Time::ZERO,
3195            200,
3196            task(1),
3197            task(3),
3198            Time::from_nanos(999),
3199            DownReason::Normal,
3200        );
3201        let s = format!("{e}");
3202        assert!(s.contains("exit"), "expected exit in {s}");
3203        assert!(s.contains("link_ref=200"), "expected ref in {s}");
3204    }
3205
3206    #[test]
3207    fn display_message() {
3208        let e = TraceEvent::user_trace(24, Time::ZERO, "hello world");
3209        let s = format!("{e}");
3210        assert!(s.contains("\"hello world\""), "expected msg in {s}");
3211    }
3212
3213    #[test]
3214    fn display_chaos_with_task() {
3215        let e = TraceEvent::new(
3216            25,
3217            Time::ZERO,
3218            TraceEventKind::ChaosInjection,
3219            TraceData::Chaos {
3220                kind: "delay".into(),
3221                task: Some(task(1)),
3222                detail: "200ns".into(),
3223            },
3224        );
3225        let s = format!("{e}");
3226        assert!(s.contains("chaos:delay"), "expected kind in {s}");
3227        assert!(s.contains("task="), "expected task in {s}");
3228        assert!(s.contains("200ns"), "expected detail in {s}");
3229    }
3230
3231    #[test]
3232    fn display_chaos_without_task() {
3233        let e = TraceEvent::new(
3234            26,
3235            Time::ZERO,
3236            TraceEventKind::ChaosInjection,
3237            TraceData::Chaos {
3238                kind: "budget_exhaust".into(),
3239                task: None,
3240                detail: "all".into(),
3241            },
3242        );
3243        let s = format!("{e}");
3244        assert!(s.contains("chaos:budget_exhaust"), "expected kind in {s}");
3245        assert!(!s.contains("task="), "should not show task: {s}");
3246    }
3247
3248    #[test]
3249    fn display_none_data() {
3250        let e = TraceEvent::new(27, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
3251        let s = format!("{e}");
3252        // Should have seq, time, kind but nothing else
3253        assert!(s.contains("user_trace"), "expected kind in {s}");
3254    }
3255
3256    #[test]
3257    fn display_with_logical_time() {
3258        let lt = LogicalTime::Lamport(LamportTime::from_raw(42));
3259        let e = TraceEvent::new(28, Time::ZERO, TraceEventKind::UserTrace, TraceData::None)
3260            .with_logical_time(lt);
3261        let s = format!("{e}");
3262        assert!(s.contains('@'), "expected @lt in {s}");
3263    }
3264
3265    // ── Equality and Clone ─────────────────────────────────────────
3266
3267    #[test]
3268    fn events_equal_same_fields() {
3269        let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3270        let b = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3271        assert_eq!(a, b);
3272    }
3273
3274    #[test]
3275    fn events_differ_on_seq() {
3276        let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3277        let b = TraceEvent::spawn(2, Time::ZERO, task(1), region(2));
3278        assert_ne!(a, b);
3279    }
3280
3281    #[test]
3282    fn events_differ_on_kind() {
3283        let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3284        let b = TraceEvent::schedule(1, Time::ZERO, task(1), region(2));
3285        assert_ne!(a, b);
3286    }
3287
3288    #[test]
3289    fn events_differ_on_data() {
3290        let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3291        let b = TraceEvent::spawn(1, Time::ZERO, task(1), region(3));
3292        assert_ne!(a, b);
3293    }
3294
3295    #[test]
3296    fn trace_data_clone() {
3297        let data = TraceData::Task {
3298            task: task(1),
3299            region: region(2),
3300        };
3301        let cloned = data.clone();
3302        assert_eq!(data, cloned);
3303    }
3304
3305    #[test]
3306    fn trace_data_message_eq() {
3307        let a = TraceData::Message("hello".into());
3308        let b = TraceData::Message("hello".into());
3309        assert_eq!(a, b);
3310    }
3311
3312    #[test]
3313    fn trace_data_message_ne() {
3314        let a = TraceData::Message("hello".into());
3315        let b = TraceData::Message("world".into());
3316        assert_ne!(a, b);
3317    }
3318
3319    #[test]
3320    fn trace_data_none_variant() {
3321        assert_eq!(TraceData::None, TraceData::None);
3322    }
3323
3324    #[test]
3325    fn trace_event_clone() {
3326        let e = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3327        let c = e.clone();
3328        assert_eq!(e, c);
3329    }
3330
3331    // ── Obligation all-kinds coverage ──────────────────────────────
3332
3333    #[test]
3334    fn obligation_reserve_all_kinds() {
3335        for kind in [
3336            ObligationKind::SendPermit,
3337            ObligationKind::Ack,
3338            ObligationKind::Lease,
3339            ObligationKind::IoOp,
3340        ] {
3341            let e = TraceEvent::obligation_reserve(
3342                1,
3343                Time::ZERO,
3344                obligation(1),
3345                task(2),
3346                region(3),
3347                kind,
3348            );
3349            match &e.data {
3350                TraceData::Obligation { kind: k, .. } => assert_eq!(*k, kind),
3351                _ => panic!("wrong variant"),
3352            }
3353        }
3354    }
3355
3356    #[test]
3357    fn obligation_abort_all_reasons() {
3358        for reason in [
3359            ObligationAbortReason::Cancel,
3360            ObligationAbortReason::Error,
3361            ObligationAbortReason::Explicit,
3362        ] {
3363            let e = TraceEvent::obligation_abort(
3364                1,
3365                Time::ZERO,
3366                obligation(1),
3367                task(2),
3368                region(3),
3369                ObligationKind::SendPermit,
3370                1000,
3371                reason,
3372            );
3373            match &e.data {
3374                TraceData::Obligation { abort_reason, .. } => {
3375                    assert_eq!(*abort_reason, Some(reason));
3376                }
3377                _ => panic!("wrong variant"),
3378            }
3379        }
3380    }
3381
3382    // ── Down with error variant ────────────────────────────────────
3383
3384    #[test]
3385    fn down_delivered_with_error_reason() {
3386        let e = TraceEvent::down_delivered(
3387            1,
3388            Time::ZERO,
3389            50,
3390            task(1),
3391            task(2),
3392            Time::from_nanos(100),
3393            DownReason::Error("boom".into()),
3394        );
3395        match &e.data {
3396            TraceData::Down { reason, .. } => {
3397                assert_eq!(*reason, DownReason::Error("boom".into()));
3398            }
3399            _ => panic!("wrong variant"),
3400        }
3401    }
3402
3403    #[test]
3404    fn exit_delivered_with_cancelled_reason() {
3405        let e = TraceEvent::exit_delivered(
3406            1,
3407            Time::ZERO,
3408            50,
3409            task(1),
3410            task(2),
3411            Time::from_nanos(100),
3412            DownReason::Cancelled(CancelReason::timeout()),
3413        );
3414        match &e.data {
3415            TraceData::Exit { reason, .. } => {
3416                assert!(matches!(reason, DownReason::Cancelled(_)));
3417            }
3418            _ => panic!("wrong variant"),
3419        }
3420    }
3421
3422    // ── Edge cases ─────────────────────────────────────────────────
3423
3424    #[test]
3425    fn seq_zero() {
3426        let e = TraceEvent::new(0, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
3427        assert_eq!(e.seq, 0);
3428    }
3429
3430    #[test]
3431    fn seq_max() {
3432        let e = TraceEvent::new(
3433            u64::MAX,
3434            Time::ZERO,
3435            TraceEventKind::UserTrace,
3436            TraceData::None,
3437        );
3438        assert_eq!(e.seq, u64::MAX);
3439    }
3440
3441    #[test]
3442    fn time_max() {
3443        let e = TraceEvent::new(1, Time::MAX, TraceEventKind::UserTrace, TraceData::None);
3444        assert_eq!(e.time, Time::MAX);
3445    }
3446
3447    #[test]
3448    fn io_result_zero_bytes() {
3449        let e = TraceEvent::io_result(1, Time::ZERO, 0, 0);
3450        assert_eq!(e.data, TraceData::IoResult { token: 0, bytes: 0 });
3451    }
3452
3453    #[test]
3454    fn checkpoint_zero_counts() {
3455        let e = TraceEvent::checkpoint(1, Time::ZERO, 0, 0, 0);
3456        assert_eq!(
3457            e.data,
3458            TraceData::Checkpoint {
3459                sequence: 0,
3460                active_tasks: 0,
3461                active_regions: 0
3462            }
3463        );
3464    }
3465
3466    #[test]
3467    fn futurelock_many_held() {
3468        let held: Vec<_> = (0..100)
3469            .map(|i| (obligation(i), ObligationKind::SendPermit))
3470            .collect();
3471        let e = TraceEvent::new(
3472            1,
3473            Time::ZERO,
3474            TraceEventKind::FuturelockDetected,
3475            TraceData::Futurelock {
3476                task: task(1),
3477                region: region(2),
3478                idle_steps: 1000,
3479                held,
3480            },
3481        );
3482        let s = format!("{e}");
3483        // Should contain all 100 entries
3484        assert!(s.matches("SendPermit").count() == 100);
3485    }
3486
3487    // --- wave 78 trait coverage ---
3488
3489    #[test]
3490    fn trace_event_kind_debug_clone_copy_eq_ord_hash() {
3491        use std::collections::HashSet;
3492        let k = TraceEventKind::Spawn;
3493        let k2 = k; // Copy
3494        let k3 = k;
3495        assert_eq!(k, k2);
3496        assert_eq!(k, k3);
3497        assert_ne!(k, TraceEventKind::Complete);
3498        assert!(k < TraceEventKind::Complete);
3499        let dbg = format!("{k:?}");
3500        assert!(dbg.contains("Spawn"));
3501        let mut set = HashSet::new();
3502        set.insert(k);
3503        assert!(set.contains(&k2));
3504    }
3505
3506    #[test]
3507    fn trace_data_debug_clone_eq() {
3508        let d = TraceData::None;
3509        let d2 = d.clone();
3510        assert_eq!(d, d2);
3511        assert_ne!(d, TraceData::Message("hi".into()));
3512        let dbg = format!("{d:?}");
3513        assert!(dbg.contains("None"));
3514    }
3515
3516    #[test]
3517    fn trace_event_debug_clone_eq() {
3518        let e = TraceEvent::new(
3519            0,
3520            Time::from_nanos(100),
3521            TraceEventKind::UserTrace,
3522            TraceData::Message("hello".into()),
3523        );
3524        let e2 = e.clone();
3525        assert_eq!(e, e2);
3526        let dbg = format!("{e:?}");
3527        assert!(dbg.contains("TraceEvent"));
3528    }
3529
3530    #[test]
3531    fn browser_trace_schema_v1_validates() {
3532        let schema = browser_trace_schema_v1();
3533        validate_browser_trace_schema(&schema).expect("browser schema should validate");
3534    }
3535
3536    #[test]
3537    fn browser_trace_schema_round_trip_json() {
3538        let schema = browser_trace_schema_v1();
3539        let payload = serde_json::to_string(&schema).expect("serialize schema");
3540        let decoded = decode_browser_trace_schema(&payload).expect("decode schema");
3541        assert_eq!(schema, decoded);
3542    }
3543
3544    #[test]
3545    fn browser_trace_schema_timer_required_fields_match_payload_shape() {
3546        let schema = browser_trace_schema_v1();
3547        let scheduled = schema
3548            .event_specs
3549            .iter()
3550            .find(|entry| entry.event_kind == "timer_scheduled")
3551            .expect("timer_scheduled entry should exist");
3552        let fired = schema
3553            .event_specs
3554            .iter()
3555            .find(|entry| entry.event_kind == "timer_fired")
3556            .expect("timer_fired entry should exist");
3557        let cancelled = schema
3558            .event_specs
3559            .iter()
3560            .find(|entry| entry.event_kind == "timer_cancelled")
3561            .expect("timer_cancelled entry should exist");
3562
3563        assert_eq!(
3564            scheduled.required_fields,
3565            vec!["deadline".to_string(), "timer_id".to_string()]
3566        );
3567        assert_eq!(fired.required_fields, vec!["timer_id".to_string()]);
3568        assert_eq!(cancelled.required_fields, vec!["timer_id".to_string()]);
3569    }
3570
3571    #[test]
3572    fn browser_trace_schema_obligation_required_fields_match_payload_shape() {
3573        let schema = browser_trace_schema_v1();
3574        let reserve = schema
3575            .event_specs
3576            .iter()
3577            .find(|entry| entry.event_kind == "obligation_reserve")
3578            .expect("obligation_reserve entry should exist");
3579        let commit = schema
3580            .event_specs
3581            .iter()
3582            .find(|entry| entry.event_kind == "obligation_commit")
3583            .expect("obligation_commit entry should exist");
3584        let abort = schema
3585            .event_specs
3586            .iter()
3587            .find(|entry| entry.event_kind == "obligation_abort")
3588            .expect("obligation_abort entry should exist");
3589        let leak = schema
3590            .event_specs
3591            .iter()
3592            .find(|entry| entry.event_kind == "obligation_leak")
3593            .expect("obligation_leak entry should exist");
3594
3595        assert_eq!(
3596            reserve.required_fields,
3597            vec![
3598                "kind".to_string(),
3599                "obligation".to_string(),
3600                "region".to_string(),
3601                "state".to_string(),
3602                "task".to_string(),
3603            ]
3604        );
3605        assert_eq!(
3606            commit.required_fields,
3607            vec![
3608                "duration_ns".to_string(),
3609                "kind".to_string(),
3610                "obligation".to_string(),
3611                "region".to_string(),
3612                "state".to_string(),
3613                "task".to_string(),
3614            ]
3615        );
3616        assert_eq!(
3617            abort.required_fields,
3618            vec![
3619                "abort_reason".to_string(),
3620                "duration_ns".to_string(),
3621                "kind".to_string(),
3622                "obligation".to_string(),
3623                "region".to_string(),
3624                "state".to_string(),
3625                "task".to_string(),
3626            ]
3627        );
3628        assert_eq!(
3629            leak.required_fields,
3630            vec![
3631                "duration_ns".to_string(),
3632                "kind".to_string(),
3633                "obligation".to_string(),
3634                "region".to_string(),
3635                "state".to_string(),
3636                "task".to_string(),
3637            ]
3638        );
3639    }
3640
3641    #[test]
3642    fn browser_trace_schema_worker_required_fields_match_payload_shape() {
3643        let schema = browser_trace_schema_v1();
3644        for event_kind in [
3645            "worker_cancel_requested",
3646            "worker_cancel_acknowledged",
3647            "worker_drain_started",
3648            "worker_drain_completed",
3649            "worker_finalize_completed",
3650        ] {
3651            let entry = schema
3652                .event_specs
3653                .iter()
3654                .find(|entry| entry.event_kind == event_kind)
3655                .unwrap_or_else(|| panic!("{event_kind} entry should exist"));
3656            assert_eq!(entry.category, BrowserTraceCategory::CancellationTransition);
3657            assert_eq!(
3658                entry.required_fields,
3659                vec![
3660                    "decision_seq".to_string(),
3661                    "job_id".to_string(),
3662                    "obligation".to_string(),
3663                    "region".to_string(),
3664                    "replay_hash".to_string(),
3665                    "task".to_string(),
3666                    "worker_id".to_string(),
3667                ]
3668            );
3669        }
3670    }
3671
3672    #[test]
3673    fn browser_trace_schema_decode_v0_migrates() {
3674        let legacy = serde_json::json!({
3675            "schema_version": "browser-trace-schema-v0",
3676            "required_envelope_fields": [
3677                "event_kind",
3678                "schema_version",
3679                "seq",
3680                "time_ns",
3681                "trace_id"
3682            ],
3683            "ordering_semantics": [
3684                "events must be strictly ordered by seq ascending",
3685                "logical_time must be monotonic for comparable causal domains",
3686                "trace streams must be deterministic for identical seed/config/replay inputs"
3687            ],
3688            "event_specs": browser_trace_schema_v1().event_specs
3689        });
3690        let payload = serde_json::to_string(&legacy).expect("serialize legacy schema");
3691        let decoded = decode_browser_trace_schema(&payload).expect("decode legacy schema");
3692        assert_eq!(
3693            decoded.schema_version,
3694            BROWSER_TRACE_SCHEMA_VERSION.to_string()
3695        );
3696        assert!(
3697            decoded
3698                .compatibility
3699                .backward_decode_aliases
3700                .iter()
3701                .any(|alias| alias == "browser-trace-schema-v0")
3702        );
3703    }
3704
3705    #[test]
3706    fn browser_trace_schema_decode_v0_sparse_event_specs_use_defaults() {
3707        let event_specs = TraceEventKind::ALL
3708            .iter()
3709            .map(|kind| serde_json::json!({ "event_kind": kind.stable_name() }))
3710            .collect::<Vec<_>>();
3711        let legacy = serde_json::json!({
3712            "schema_version": "browser-trace-schema-v0",
3713            "required_envelope_fields": [
3714                "event_kind",
3715                "schema_version",
3716                "seq",
3717                "time_ns",
3718                "trace_id"
3719            ],
3720            "ordering_semantics": [
3721                "events must be strictly ordered by seq ascending",
3722                "logical_time must be monotonic for comparable causal domains",
3723                "trace streams must be deterministic for identical seed/config/replay inputs"
3724            ],
3725            "event_specs": event_specs
3726        });
3727        let payload = serde_json::to_string(&legacy).expect("serialize sparse legacy schema");
3728        let decoded = decode_browser_trace_schema(&payload).expect("decode sparse legacy schema");
3729
3730        let user_trace = decoded
3731            .event_specs
3732            .iter()
3733            .find(|entry| entry.event_kind == "user_trace")
3734            .expect("user_trace entry should exist");
3735        assert_eq!(user_trace.category, BrowserTraceCategory::HostCallback);
3736        assert_eq!(user_trace.required_fields, vec!["message".to_string()]);
3737        assert_eq!(user_trace.redacted_fields, vec!["message".to_string()]);
3738    }
3739
3740    #[test]
3741    fn browser_trace_schema_decode_v0_unknown_event_kind_fails_closed() {
3742        let legacy = serde_json::json!({
3743            "schema_version": "browser-trace-schema-v0",
3744            "required_envelope_fields": [
3745                "event_kind",
3746                "schema_version",
3747                "seq",
3748                "time_ns",
3749                "trace_id"
3750            ],
3751            "ordering_semantics": [
3752                "events must be strictly ordered by seq ascending",
3753                "logical_time must be monotonic for comparable causal domains",
3754                "trace streams must be deterministic for identical seed/config/replay inputs"
3755            ],
3756            "event_specs": [{ "event_kind": "not_a_real_event_kind" }]
3757        });
3758        let payload = serde_json::to_string(&legacy).expect("serialize invalid legacy schema");
3759        let error = decode_browser_trace_schema(&payload)
3760            .expect_err("unknown legacy event kinds must fail decode");
3761        assert!(error.contains("unknown legacy event kind"));
3762    }
3763
3764    #[test]
3765    fn browser_trace_redaction_masks_message_payloads() {
3766        let event = TraceEvent::user_trace(4, Time::ZERO, "secret-token");
3767        let redacted = redact_browser_trace_event(&event);
3768        assert_eq!(
3769            redacted,
3770            TraceEvent::new(
3771                4,
3772                Time::ZERO,
3773                TraceEventKind::UserTrace,
3774                TraceData::Message("<redacted>".to_string())
3775            )
3776        );
3777    }
3778
3779    // br-asupersync-92qzak: redact_browser_trace_event must scrub
3780    // free-form String payloads inside Cancel.reason.message,
3781    // RegionCancel.reason.message, Worker.worker_id, Down.reason
3782    // (Error / Cancelled / Panicked variants), and Exit.reason.
3783    // Pre-fix only UserTrace and ChaosInjection were redacted; this
3784    // test pins the new total coverage.
3785    #[test]
3786    fn redact_scrubs_cancel_reason_message_92qzak() {
3787        let task = TaskId::new_for_test(0, 1);
3788        let region = RegionId::new_for_test(0, 1);
3789        let reason = crate::types::CancelReason::user("invalid token sk_live_ABC123");
3790        let mut event = TraceEvent::new(
3791            1,
3792            Time::ZERO,
3793            TraceEventKind::CancelRequest,
3794            TraceData::Cancel {
3795                task,
3796                region,
3797                reason,
3798            },
3799        );
3800        // Touch the field so the warning about "_ unused" doesn't bite.
3801        event.seq = 1;
3802        let redacted = redact_browser_trace_event(&event);
3803        match &redacted.data {
3804            TraceData::Cancel { reason, .. } => {
3805                assert_eq!(reason.message.as_deref(), Some("<redacted>"));
3806            }
3807            other => panic!("expected redacted Cancel, got {other:?}"),
3808        }
3809    }
3810
3811    #[test]
3812    fn redact_scrubs_region_cancel_reason_message_92qzak() {
3813        let region = RegionId::new_for_test(0, 1);
3814        let reason = crate::types::CancelReason::user("internal-secret");
3815        let event = TraceEvent::new(
3816            1,
3817            Time::ZERO,
3818            TraceEventKind::RegionCancelled,
3819            TraceData::RegionCancel { region, reason },
3820        );
3821        let redacted = redact_browser_trace_event(&event);
3822        match &redacted.data {
3823            TraceData::RegionCancel { reason, .. } => {
3824                assert_eq!(reason.message.as_deref(), Some("<redacted>"));
3825            }
3826            other => panic!("expected redacted RegionCancel, got {other:?}"),
3827        }
3828    }
3829
3830    #[test]
3831    fn redact_scrubs_worker_id_92qzak() {
3832        let task = TaskId::new_for_test(0, 1);
3833        let region = RegionId::new_for_test(0, 1);
3834        let obligation = ObligationId::new_for_test(0, 1);
3835        let event = TraceEvent::new(
3836            1,
3837            Time::ZERO,
3838            TraceEventKind::ChaosInjection,
3839            TraceData::Worker {
3840                worker_id: "worker-with-secret-suffix-token-abc".to_string(),
3841                job_id: 42,
3842                decision_seq: 7,
3843                replay_hash: 0xdeadbeef,
3844                task,
3845                region,
3846                obligation,
3847            },
3848        );
3849        let redacted = redact_browser_trace_event(&event);
3850        match &redacted.data {
3851            TraceData::Worker { worker_id, .. } => assert_eq!(worker_id, "<redacted>"),
3852            other => panic!("expected redacted Worker, got {other:?}"),
3853        }
3854    }
3855
3856    #[test]
3857    fn redact_scrubs_down_reason_error_string_92qzak() {
3858        let watcher = TaskId::new_for_test(0, 1);
3859        let monitored = TaskId::new_for_test(0, 2);
3860        let event = TraceEvent::new(
3861            1,
3862            Time::ZERO,
3863            TraceEventKind::ChaosInjection,
3864            TraceData::Down {
3865                monitor_ref: 1,
3866                watcher,
3867                monitored,
3868                completion_vt: Time::ZERO,
3869                reason: crate::monitor::DownReason::Error(
3870                    "/etc/secret_path:42 panicked with bearer eyJ...".to_string(),
3871                ),
3872            },
3873        );
3874        let redacted = redact_browser_trace_event(&event);
3875        match &redacted.data {
3876            TraceData::Down { reason, .. } => match reason {
3877                crate::monitor::DownReason::Error(msg) => assert_eq!(msg, "<redacted>"),
3878                other => panic!("expected Error variant, got {other:?}"),
3879            },
3880            other => panic!("expected redacted Down, got {other:?}"),
3881        }
3882    }
3883
3884    #[test]
3885    fn redact_preserves_structural_identifiers_92qzak() {
3886        // Structural fields (TaskId, RegionId, sequence, kinds) MUST
3887        // be preserved so causality reconstruction is still possible
3888        // browser-side after redaction.
3889        let task = TaskId::new_for_test(0, 1);
3890        let region = RegionId::new_for_test(0, 1);
3891        let event = TraceEvent::new(
3892            42,
3893            Time::from_nanos(1234),
3894            TraceEventKind::Spawn,
3895            TraceData::Task { task, region },
3896        );
3897        let redacted = redact_browser_trace_event(&event);
3898        assert_eq!(redacted.seq, 42);
3899        assert_eq!(redacted.time, Time::from_nanos(1234));
3900        match &redacted.data {
3901            TraceData::Task { task: t, region: r } => {
3902                assert_eq!(*t, task);
3903                assert_eq!(*r, region);
3904            }
3905            other => panic!("expected Task data, got {other:?}"),
3906        }
3907    }
3908
3909    #[test]
3910    fn browser_trace_log_fields_include_required_metadata() {
3911        let event = TraceEvent::timer_fired(9, Time::from_nanos(42), 10);
3912        let fields = browser_trace_log_fields(&event, "trace-browser-1", None);
3913
3914        assert_eq!(
3915            fields.get("schema_version"),
3916            Some(&BROWSER_TRACE_SCHEMA_VERSION.to_string())
3917        );
3918        assert_eq!(fields.get("trace_id"), Some(&"trace-browser-1".to_string()));
3919        assert_eq!(fields.get("event_kind"), Some(&"timer_fired".to_string()));
3920        assert_eq!(fields.get("seq"), Some(&"9".to_string()));
3921        assert_eq!(fields.get("capture_source"), Some(&"runtime".to_string()));
3922        assert_eq!(fields.get("capture_host_turn_seq"), Some(&"9".to_string()));
3923        assert_eq!(fields.get("capture_source_seq"), Some(&"9".to_string()));
3924        assert_eq!(fields.get("capture_host_time_ns"), Some(&"42".to_string()));
3925        assert_eq!(
3926            fields.get("capture_replay_key"),
3927            Some(&"runtime:9:9:42".to_string())
3928        );
3929        assert_eq!(fields.get("validation_status"), Some(&"valid".to_string()));
3930        assert_eq!(
3931            fields.get("validation_failure_category"),
3932            Some(&"none".to_string())
3933        );
3934        assert_eq!(fields.get("sequence_group"), Some(&"timer:10".to_string()));
3935        assert_eq!(fields.get("timer_id"), Some(&"10".to_string()));
3936    }
3937
3938    #[test]
3939    fn browser_trace_log_fields_with_capture_include_host_metadata() {
3940        let event = TraceEvent::timer_fired(17, Time::from_nanos(200), 11);
3941        let capture = BrowserCaptureMetadata {
3942            host_turn_seq: 71,
3943            source: BrowserCaptureSource::HostInput,
3944            source_seq: 4,
3945            host_time_ns: 9_001,
3946        };
3947        let fields =
3948            browser_trace_log_fields_with_capture(&event, "trace-browser-2", None, Some(&capture));
3949        assert_eq!(
3950            fields.get("capture_source"),
3951            Some(&"host_input".to_string())
3952        );
3953        assert_eq!(fields.get("capture_host_turn_seq"), Some(&"71".to_string()));
3954        assert_eq!(fields.get("capture_source_seq"), Some(&"4".to_string()));
3955        assert_eq!(
3956            fields.get("capture_host_time_ns"),
3957            Some(&"9001".to_string())
3958        );
3959        assert_eq!(
3960            fields.get("capture_replay_key"),
3961            Some(&"host_input:71:4:9001".to_string())
3962        );
3963    }
3964
3965    #[test]
3966    fn browser_trace_log_fields_sequence_group_tracks_causal_domain() {
3967        let first = TraceEvent::timer_fired(7, Time::from_nanos(10), 41);
3968        let second = TraceEvent::timer_cancelled(8, Time::from_nanos(11), 41);
3969        let unrelated = TraceEvent::timer_fired(9, Time::from_nanos(12), 99);
3970
3971        let first_fields = browser_trace_log_fields(&first, "trace-browser-group-1", None);
3972        let second_fields = browser_trace_log_fields(&second, "trace-browser-group-2", None);
3973        let unrelated_fields = browser_trace_log_fields(&unrelated, "trace-browser-group-3", None);
3974
3975        assert_eq!(
3976            first_fields.get("sequence_group"),
3977            Some(&"timer:41".to_string())
3978        );
3979        assert_eq!(
3980            first_fields.get("sequence_group"),
3981            second_fields.get("sequence_group")
3982        );
3983        assert_ne!(
3984            first_fields.get("sequence_group"),
3985            unrelated_fields.get("sequence_group")
3986        );
3987    }
3988
3989    #[test]
3990    fn browser_trace_log_fields_sequence_group_preserves_link_relationships() {
3991        let created = TraceEvent::link_created(
3992            20,
3993            Time::from_nanos(100),
3994            77,
3995            task(1),
3996            region(2),
3997            task(3),
3998            region(4),
3999        );
4000        let exited = TraceEvent::exit_delivered(
4001            21,
4002            Time::from_nanos(101),
4003            77,
4004            task(1),
4005            task(3),
4006            Time::from_nanos(55),
4007            DownReason::Normal,
4008        );
4009        let other = TraceEvent::link_dropped(
4010            22,
4011            Time::from_nanos(102),
4012            88,
4013            task(1),
4014            region(2),
4015            task(3),
4016            region(4),
4017        );
4018
4019        let created_fields = browser_trace_log_fields(&created, "trace-browser-link-1", None);
4020        let exited_fields = browser_trace_log_fields(&exited, "trace-browser-link-2", None);
4021        let other_fields = browser_trace_log_fields(&other, "trace-browser-link-3", None);
4022
4023        assert_eq!(
4024            created_fields.get("sequence_group"),
4025            Some(&"link:77".to_string())
4026        );
4027        assert_eq!(
4028            created_fields.get("sequence_group"),
4029            exited_fields.get("sequence_group")
4030        );
4031        assert_ne!(
4032            created_fields.get("sequence_group"),
4033            other_fields.get("sequence_group")
4034        );
4035    }
4036
4037    #[test]
4038    fn browser_trace_log_fields_mark_invalid_when_failure_category_is_set() {
4039        let event = TraceEvent::timer_fired(9, Time::from_nanos(42), 10);
4040        let fields =
4041            browser_trace_log_fields(&event, "trace-browser-1", Some("schema_version_mismatch"));
4042        assert_eq!(
4043            fields.get("validation_status"),
4044            Some(&"invalid".to_string())
4045        );
4046        assert_eq!(
4047            fields.get("validation_failure_category"),
4048            Some(&"schema_version_mismatch".to_string())
4049        );
4050    }
4051
4052    #[test]
4053    fn browser_trace_log_fields_redact_worker_identity_while_preserving_replay_linkage() {
4054        let raw_worker_id = "worker-a";
4055        let event = TraceEvent::worker_cancel_requested(
4056            21,
4057            Time::from_nanos(55),
4058            raw_worker_id,
4059            77,
4060            91,
4061            0x00C0_FFEE,
4062            task(9),
4063            region(10),
4064            obligation(11),
4065        );
4066        let fields = browser_trace_log_fields(&event, "trace-browser-worker-1", None);
4067        assert_eq!(fields.get("decision_seq"), Some(&"91".to_string()));
4068        assert_eq!(fields.get("job_id"), Some(&"77".to_string()));
4069        assert_eq!(fields.get("obligation"), Some(&obligation(11).to_string()));
4070        assert_eq!(fields.get("region"), Some(&region(10).to_string()));
4071        assert_eq!(fields.get("replay_hash"), Some(&"12648430".to_string()));
4072        assert_eq!(fields.get("task"), Some(&task(9).to_string()));
4073        assert_eq!(fields.get("worker_id"), Some(&"<redacted>".to_string()));
4074        assert_eq!(
4075            fields.get("sequence_group"),
4076            Some(&"worker_job:77:<redacted>".to_string())
4077        );
4078        assert!(
4079            fields.values().all(|value| !value.contains(raw_worker_id)),
4080            "browser trace log fields must not leak raw worker identity: {fields:?}"
4081        );
4082    }
4083
4084    #[test]
4085    fn browser_trace_log_fields_snapshot_scrubs_ids_and_timestamps() {
4086        let event = TraceEvent::worker_cancel_requested(
4087            41,
4088            Time::from_nanos(123_456_789),
4089            "worker-browser-snapshot",
4090            88,
4091            17,
4092            0x00C0_FFEE,
4093            task(9),
4094            region(10),
4095            obligation(11),
4096        );
4097        let capture = BrowserCaptureMetadata {
4098            host_turn_seq: 7,
4099            source: BrowserCaptureSource::HostInput,
4100            source_seq: 19,
4101            host_time_ns: 1_726_133_456_789_000_000,
4102        };
4103
4104        let fields = browser_trace_log_fields_with_capture(
4105            &event,
4106            "trace-browser-snapshot-1",
4107            None,
4108            Some(&capture),
4109        );
4110
4111        insta::assert_json_snapshot!(
4112            "browser_trace_log_fields_worker_scrubbed",
4113            scrub_browser_trace_fields(&fields)
4114        );
4115    }
4116
4117    #[test]
4118    fn browser_trace_log_fields_timer_snapshot_scrubs_ids_and_timestamps() {
4119        let event =
4120            TraceEvent::timer_scheduled(14, Time::from_nanos(333), 42, Time::from_nanos(999));
4121        let fields = browser_trace_log_fields(&event, "trace-browser-timer-1", None);
4122
4123        insta::assert_json_snapshot!(
4124            "browser_trace_log_fields_timer_scrubbed",
4125            scrub_browser_trace_fields(&fields)
4126        );
4127    }
4128
4129    #[test]
4130    fn browser_trace_log_fields_obligation_abort_snapshot_scrubs_ids_and_timestamps() {
4131        let event = TraceEvent::obligation_abort(
4132            52,
4133            Time::from_nanos(7_777),
4134            obligation(4),
4135            task(8),
4136            region(9),
4137            ObligationKind::Lease,
4138            5_000,
4139            ObligationAbortReason::Error,
4140        );
4141        let fields = browser_trace_log_fields(&event, "trace-browser-obligation-1", None);
4142
4143        insta::assert_json_snapshot!(
4144            "browser_trace_log_fields_obligation_abort_scrubbed",
4145            scrub_browser_trace_fields(&fields)
4146        );
4147    }
4148
4149    #[test]
4150    fn browser_trace_log_fields_exit_snapshot_scrubs_ids_and_timestamps() {
4151        let event = TraceEvent::exit_delivered(
4152            61,
4153            Time::from_nanos(8_001),
4154            77,
4155            task(2),
4156            task(3),
4157            Time::from_nanos(4_444),
4158            DownReason::Normal,
4159        );
4160        let fields = browser_trace_log_fields(&event, "trace-browser-exit-1", None);
4161
4162        insta::assert_json_snapshot!(
4163            "browser_trace_log_fields_exit_scrubbed",
4164            scrub_browser_trace_fields(&fields)
4165        );
4166    }
4167
4168    #[test]
4169    fn browser_trace_log_fields_redact_large_worker_attributes() {
4170        let raw_worker_id = format!("worker-{}", "e\u{0301}".repeat(200));
4171        let event = TraceEvent::worker_cancel_requested(
4172            30,
4173            Time::from_nanos(60),
4174            raw_worker_id.clone(),
4175            123,
4176            456,
4177            0xDEAD_BEEF,
4178            task(5),
4179            region(6),
4180            obligation(7),
4181        );
4182        let fields = browser_trace_log_fields(&event, "trace-browser-worker-2", None);
4183
4184        let worker_id = fields
4185            .get("worker_id")
4186            .expect("worker_id field should be present");
4187        let sequence_group = fields
4188            .get("sequence_group")
4189            .expect("sequence_group field should be present");
4190
4191        assert_eq!(worker_id, "<redacted>");
4192        assert_eq!(sequence_group, "worker_job:123:<redacted>");
4193        assert!(
4194            fields.values().all(|value| !value.contains(&raw_worker_id)),
4195            "browser trace log fields must not leak large raw worker identity: {fields:?}"
4196        );
4197    }
4198
4199    #[test]
4200    fn browser_trace_attribute_cap_preserves_utf8_boundary() {
4201        let raw = format!("group:{}", "e\u{0301}".repeat(200));
4202        let capped = cap_browser_trace_attribute(&raw);
4203
4204        assert!(capped.len() <= MAX_BROWSER_TRACE_ATTRIBUTE_BYTES);
4205        assert!(capped.starts_with("group:"));
4206        assert!(capped.contains('#'));
4207        assert!(capped.is_char_boundary(capped.len()));
4208    }
4209
4210    // ── canonical serialization golden ─────────────────────────────
4211    //
4212    // Locks the on-the-wire JSON shape of `TraceEvent` across every
4213    // `TraceData` variant. Producers and consumers ride this wire
4214    // (recorder, replayer, browser bridge, distributed sheaf), so any
4215    // unintentional schema drift here is a cross-component breakage —
4216    // the snapshot is the firewall.
4217
4218    #[test]
4219    fn trace_event_canonical_serialization_golden() {
4220        let events = vec![
4221            TraceEvent::new(
4222                1,
4223                Time::from_nanos(0),
4224                TraceEventKind::UserTrace,
4225                TraceData::None,
4226            ),
4227            TraceEvent::spawn(2, Time::from_nanos(100), task(1), region(1)),
4228            TraceEvent::region_created(3, Time::from_nanos(200), region(2), Some(region(1))),
4229            TraceEvent::obligation_commit(
4230                4,
4231                Time::from_nanos(300),
4232                obligation(5),
4233                task(1),
4234                region(1),
4235                ObligationKind::Lease,
4236                1_500,
4237            ),
4238            TraceEvent::obligation_abort(
4239                5,
4240                Time::from_nanos(310),
4241                obligation(6),
4242                task(1),
4243                region(1),
4244                ObligationKind::SendPermit,
4245                2_500,
4246                ObligationAbortReason::Error,
4247            ),
4248            TraceEvent::cancel_request(
4249                6,
4250                Time::from_nanos(400),
4251                task(1),
4252                region(1),
4253                CancelReason::timeout(),
4254            ),
4255            TraceEvent::worker_cancel_requested(
4256                7,
4257                Time::from_nanos(500),
4258                "worker-canonical",
4259                42,
4260                7,
4261                0xDEAD_BEEF,
4262                task(1),
4263                region(1),
4264                obligation(2),
4265            ),
4266            TraceEvent::region_cancelled(
4267                8,
4268                Time::from_nanos(600),
4269                region(1),
4270                CancelReason::shutdown(),
4271            ),
4272            TraceEvent::time_advance(
4273                9,
4274                Time::from_nanos(700),
4275                Time::from_nanos(700),
4276                Time::from_nanos(800),
4277            ),
4278            TraceEvent::timer_scheduled(10, Time::from_nanos(800), 100, Time::from_nanos(900)),
4279            TraceEvent::timer_fired(11, Time::from_nanos(810), 100),
4280            TraceEvent::io_requested(12, Time::from_nanos(900), 7, 3),
4281            TraceEvent::io_ready(13, Time::from_nanos(1_000), 7, 1),
4282            TraceEvent::io_result(14, Time::from_nanos(1_100), 7, 4_096),
4283            TraceEvent::io_error(15, Time::from_nanos(1_200), 7, 5),
4284            TraceEvent::rng_seed(16, Time::from_nanos(1_300), 0x00C0_FFEE),
4285            TraceEvent::rng_value(17, Time::from_nanos(1_400), 42),
4286            TraceEvent::checkpoint(18, Time::from_nanos(1_500), 1_000, 5, 2),
4287            TraceEvent::new(
4288                19,
4289                Time::from_nanos(1_600),
4290                TraceEventKind::FuturelockDetected,
4291                TraceData::Futurelock {
4292                    task: task(2),
4293                    region: region(1),
4294                    idle_steps: 100,
4295                    held: vec![(obligation(3), ObligationKind::SendPermit)],
4296                },
4297            ),
4298            TraceEvent::monitor_created(
4299                20,
4300                Time::from_nanos(1_700),
4301                50,
4302                task(1),
4303                region(1),
4304                task(2),
4305            ),
4306            TraceEvent::down_delivered(
4307                21,
4308                Time::from_nanos(1_800),
4309                50,
4310                task(1),
4311                task(2),
4312                Time::from_nanos(1_750),
4313                DownReason::Normal,
4314            ),
4315            TraceEvent::link_created(
4316                22,
4317                Time::from_nanos(1_900),
4318                60,
4319                task(1),
4320                region(1),
4321                task(2),
4322                region(2),
4323            ),
4324            TraceEvent::exit_delivered(
4325                23,
4326                Time::from_nanos(2_000),
4327                60,
4328                task(1),
4329                task(2),
4330                Time::from_nanos(1_950),
4331                DownReason::Normal,
4332            ),
4333            TraceEvent::user_trace(24, Time::from_nanos(2_100), "canonical-trace-marker"),
4334            TraceEvent::new(
4335                25,
4336                Time::from_nanos(2_200),
4337                TraceEventKind::ChaosInjection,
4338                TraceData::Chaos {
4339                    kind: "delay".to_string(),
4340                    task: Some(task(1)),
4341                    detail: "injected 1ms delay".to_string(),
4342                },
4343            ),
4344            TraceEvent::poll(26, Time::from_nanos(2_300), task(1), region(1))
4345                .with_logical_time(LogicalTime::Lamport(LamportTime::from_raw(7))),
4346        ];
4347
4348        // Round-trip every event back through Deserialize so the
4349        // golden also locks `TraceData` discriminant tags and field
4350        // names against accidental rename — a renamed field that
4351        // keeps the same JSON layout would still fail Deserialize.
4352        for event in &events {
4353            let json = serde_json::to_value(event).expect("serialize trace event");
4354            let decoded: TraceEvent =
4355                serde_json::from_value(json).expect("deserialize trace event");
4356            assert_eq!(*event, decoded, "round-trip mismatch for {event:?}");
4357        }
4358
4359        insta::assert_json_snapshot!("trace_event_canonical_serialization", events);
4360    }
4361
4362    /// Golden test for OpenTelemetry span-focused 5-event trace serialization.
4363    ///
4364    /// This test pins the canonical JSON serialization of a minimal 5-event
4365    /// trace representing a typical span lifecycle: spawn → schedule → poll →
4366    /// user trace → complete. The golden snapshot ensures that OpenTelemetry
4367    /// bridge exporters receive consistent trace event structure across runtime
4368    /// versions.
4369    #[test]
4370    fn otel_span_golden_tests() {
4371        let span_events = vec![
4372            // Span start: task spawned in a region
4373            TraceEvent::spawn(1, Time::from_nanos(1000), task(10), region(5)),
4374            // Task gets scheduled for execution
4375            TraceEvent::schedule(2, Time::from_nanos(1100), task(10), region(5)),
4376            // Task execution begins
4377            TraceEvent::poll(3, Time::from_nanos(1200), task(10), region(5)),
4378            // User trace event within the span
4379            TraceEvent::user_trace(4, Time::from_nanos(1250), "otel-span-processing"),
4380            // Span end: task completion
4381            TraceEvent::complete(5, Time::from_nanos(1300), task(10), region(5)),
4382        ];
4383
4384        // Verify round-trip serialization integrity for OTel exports
4385        for event in &span_events {
4386            let json = serde_json::to_value(event).expect("serialize otel span event");
4387            let decoded: TraceEvent =
4388                serde_json::from_value(json).expect("deserialize otel span event");
4389            assert_eq!(
4390                *event, decoded,
4391                "otel span round-trip mismatch for {event:?}"
4392            );
4393        }
4394
4395        insta::assert_json_snapshot!("otel_span_golden_tests", span_events);
4396    }
4397
4398    /// Serialize trace events to canonical NDJSON format.
4399    ///
4400    /// Each event becomes a single JSON object on its own line, suitable for
4401    /// streaming trace ingestion and line-by-line processing pipelines.
4402    fn trace_events_to_ndjson(events: &[TraceEvent]) -> Result<String, serde_json::Error> {
4403        let mut ndjson = String::new();
4404        for event in events {
4405            let json_line = serde_json::to_string(event)?;
4406            ndjson.push_str(&json_line);
4407            ndjson.push('\n');
4408        }
4409        Ok(ndjson)
4410    }
4411
4412    /// Golden test for canonical NDJSON trace event serialization.
4413    ///
4414    /// This test pins the NDJSON (Newline Delimited JSON) serialization format
4415    /// for trace events. Each event becomes a single JSON object per line,
4416    /// ensuring compatibility with streaming ingestion pipelines and
4417    /// line-oriented processing tools.
4418    #[test]
4419    fn trace_event_canonical_ndjson_serialization() {
4420        let ndjson_events = vec![
4421            // Simple task lifecycle for NDJSON streaming
4422            TraceEvent::spawn(1, Time::from_nanos(5000), task(20), region(10)),
4423            TraceEvent::schedule(2, Time::from_nanos(5100), task(20), region(10)),
4424            TraceEvent::poll(3, Time::from_nanos(5200), task(20), region(10)),
4425            TraceEvent::user_trace(4, Time::from_nanos(5250), "ndjson-stream-marker"),
4426            TraceEvent::complete(5, Time::from_nanos(5300), task(20), region(10)),
4427        ];
4428
4429        // Generate canonical NDJSON format
4430        let ndjson_output =
4431            trace_events_to_ndjson(&ndjson_events).expect("NDJSON serialization should succeed");
4432
4433        // Verify each line is valid JSON
4434        for (i, line) in ndjson_output.lines().enumerate() {
4435            if !line.is_empty() {
4436                let parsed: serde_json::Value = serde_json::from_str(line)
4437                    .unwrap_or_else(|e| panic!("Line {i} is not valid JSON: {e}"));
4438                assert!(parsed.is_object(), "Line {i} should be a JSON object");
4439            }
4440        }
4441
4442        // Verify round-trip through NDJSON parsing
4443        let parsed_events: Result<Vec<TraceEvent>, _> = ndjson_output
4444            .lines()
4445            .filter(|line| !line.is_empty())
4446            .map(serde_json::from_str)
4447            .collect();
4448        let decoded_events = parsed_events.expect("NDJSON round-trip should succeed");
4449        assert_eq!(ndjson_events, decoded_events, "NDJSON round-trip mismatch");
4450
4451        insta::assert_snapshot!("trace_event_canonical_ndjson_serialization", ndjson_output);
4452    }
4453}