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