cellstate_core/
event_payloads.rs

1//! Typed event payloads for compile-time safety of event data.
2//!
3//! Each `EventPayload` struct corresponds to a specific `EventKind` variant.
4//! Payloads are stored as `serde_json::Value` in the DAG; these types provide
5//! compile-time decode/encode aids without changing the storage format.
6//!
7//! # Usage
8//!
9//! ```ignore
10//! // Decode: Event<serde_json::Value> → typed payload
11//! if let Some(created) = event.decode_payload::<TrajectoryCreated>() {
12//!     println!("trajectory: {}", created.trajectory_id);
13//! }
14//!
15//! // Encode: typed payload → Event<serde_json::Value>
16//! let typed_event = Event::new(header, TrajectoryCreated { ... });
17//! let stored = typed_event.erase(); // Event<serde_json::Value>
18//! ```
19
20use crate::event::{Event, EventKind};
21use serde::de::DeserializeOwned;
22use serde::{Deserialize, Serialize};
23
24// ============================================================================
25// TRAIT
26// ============================================================================
27
28/// Trait for typed event payloads that correspond to specific `EventKind` variants.
29///
30/// Implementors declare which `EventKind` they represent via `const KIND`.
31/// Used with `Event::decode::<P>()` for type-safe payload extraction.
32pub trait EventPayload: Serialize + DeserializeOwned + Send + Sync + 'static {
33    /// The `EventKind` this payload type corresponds to.
34    const KIND: EventKind;
35}
36
37// ============================================================================
38// DECODE / ERASE ON EVENT
39// ============================================================================
40
41impl Event<serde_json::Value> {
42    /// Decode the JSON payload into a typed `EventPayload`.
43    ///
44    /// Returns `None` if the event kind doesn't match `P::KIND` or deserialization fails.
45    pub fn decode<P: EventPayload>(&self) -> Option<Event<P>> {
46        if self.header.event_kind != P::KIND {
47            return None;
48        }
49        let payload: P = serde_json::from_value(self.payload.clone()).ok()?;
50        Some(Event {
51            header: self.header.clone(),
52            payload,
53            hash_chain: self.hash_chain,
54        })
55    }
56
57    /// Decode just the payload without reconstructing the full `Event`.
58    ///
59    /// Returns `None` if the event kind doesn't match or deserialization fails.
60    pub fn decode_payload<P: EventPayload>(&self) -> Option<P> {
61        if self.header.event_kind != P::KIND {
62            return None;
63        }
64        serde_json::from_value(self.payload.clone()).ok()
65    }
66}
67
68impl<P: Serialize> Event<P> {
69    /// Erase the typed payload to `serde_json::Value` for storage.
70    ///
71    /// This is the inverse of `decode()`. The storage format is unchanged —
72    /// `serde_json::Value` is what the DAG persists.
73    pub fn erase(self) -> Event<serde_json::Value> {
74        Event {
75            header: self.header,
76            payload: serde_json::to_value(self.payload).unwrap_or(serde_json::Value::Null),
77            hash_chain: self.hash_chain,
78        }
79    }
80}
81
82// ============================================================================
83// TRAJECTORY EVENT PAYLOADS (0x1xxx)
84// ============================================================================
85
86/// Payload for `TRAJECTORY_CREATED` events.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct TrajectoryCreated {
89    pub trajectory_id: crate::TrajectoryId,
90    #[serde(default)]
91    pub name: String,
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub description: Option<String>,
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub parent_trajectory_id: Option<crate::TrajectoryId>,
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub root_trajectory_id: Option<crate::TrajectoryId>,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub agent_id: Option<crate::AgentId>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub metadata: Option<serde_json::Value>,
102}
103
104impl EventPayload for TrajectoryCreated {
105    const KIND: EventKind = EventKind::TRAJECTORY_CREATED;
106}
107
108/// Payload for `TRAJECTORY_UPDATED` events (JSON merge-patch semantics).
109///
110/// All fields are optional: present means "change", absent means "keep".
111/// Note: `null` vs absent for `description` is collapsed to `None` by serde.
112/// Use raw `serde_json::Value` access when null-vs-absent matters.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct TrajectoryUpdated {
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub name: Option<String>,
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub description: Option<String>,
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub status: Option<crate::TrajectoryStatus>,
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub metadata: Option<serde_json::Value>,
123}
124
125impl EventPayload for TrajectoryUpdated {
126    const KIND: EventKind = EventKind::TRAJECTORY_UPDATED;
127}
128
129/// Payload for `TRAJECTORY_COMPLETED` events.
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct TrajectoryCompleted {
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub outcome: Option<crate::TrajectoryOutcome>,
134}
135
136impl EventPayload for TrajectoryCompleted {
137    const KIND: EventKind = EventKind::TRAJECTORY_COMPLETED;
138}
139
140/// Payload for `TRAJECTORY_FAILED` events.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct TrajectoryFailed {
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub error: Option<String>,
145}
146
147impl EventPayload for TrajectoryFailed {
148    const KIND: EventKind = EventKind::TRAJECTORY_FAILED;
149}
150
151/// Payload for `TRAJECTORY_SUSPENDED` events.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct TrajectorySuspended {}
154
155impl EventPayload for TrajectorySuspended {
156    const KIND: EventKind = EventKind::TRAJECTORY_SUSPENDED;
157}
158
159/// Payload for `TRAJECTORY_RESUMED` events.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct TrajectoryResumed {}
162
163impl EventPayload for TrajectoryResumed {
164    const KIND: EventKind = EventKind::TRAJECTORY_RESUMED;
165}
166
167/// Payload for `TRAJECTORY_DELETED` events.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct TrajectoryDeleted {}
170
171impl EventPayload for TrajectoryDeleted {
172    const KIND: EventKind = EventKind::TRAJECTORY_DELETED;
173}
174
175// ============================================================================
176// SCOPE EVENT PAYLOADS (0x2xxx)
177// ============================================================================
178
179/// Payload for `SCOPE_CREATED` events.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ScopeCreated {
182    pub scope_id: crate::ScopeId,
183    pub trajectory_id: crate::TrajectoryId,
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub parent_scope_id: Option<crate::ScopeId>,
186    #[serde(default)]
187    pub name: String,
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub purpose: Option<String>,
190    #[serde(default = "default_token_budget")]
191    pub token_budget: i32,
192    #[serde(default)]
193    pub tokens_used: i32,
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub metadata: Option<serde_json::Value>,
196}
197
198fn default_token_budget() -> i32 {
199    8000
200}
201
202impl EventPayload for ScopeCreated {
203    const KIND: EventKind = EventKind::SCOPE_CREATED;
204}
205
206/// Payload for `SCOPE_UPDATED` events (JSON merge-patch semantics).
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ScopeUpdated {
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub name: Option<String>,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub purpose: Option<String>,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub token_budget: Option<i32>,
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub tokens_used: Option<i32>,
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub metadata: Option<serde_json::Value>,
219}
220
221impl EventPayload for ScopeUpdated {
222    const KIND: EventKind = EventKind::SCOPE_UPDATED;
223}
224
225/// Payload for `SCOPE_CLOSED` events.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ScopeClosed {}
228
229impl EventPayload for ScopeClosed {
230    const KIND: EventKind = EventKind::SCOPE_CLOSED;
231}
232
233/// Payload for `SCOPE_CHECKPOINTED` events.
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct ScopeCheckpointed {
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub checkpoint: Option<serde_json::Value>,
238}
239
240impl EventPayload for ScopeCheckpointed {
241    const KIND: EventKind = EventKind::SCOPE_CHECKPOINTED;
242}
243
244// ============================================================================
245// ARTIFACT EVENT PAYLOADS (0x3xxx)
246// ============================================================================
247
248/// Payload for `ARTIFACT_CREATED` events.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ArtifactCreated {
251    pub artifact_id: crate::ArtifactId,
252    pub trajectory_id: crate::TrajectoryId,
253    pub scope_id: crate::ScopeId,
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub artifact_type: Option<crate::ArtifactType>,
256    #[serde(default)]
257    pub name: String,
258    #[serde(default)]
259    pub content: String,
260    #[serde(default, skip_serializing_if = "Option::is_none")]
261    pub content_hash: Option<crate::ContentHash>,
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub provenance: Option<crate::Provenance>,
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub ttl: Option<crate::TTL>,
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub metadata: Option<serde_json::Value>,
268}
269
270impl EventPayload for ArtifactCreated {
271    const KIND: EventKind = EventKind::ARTIFACT_CREATED;
272}
273
274/// Payload for `ARTIFACT_UPDATED` events (JSON merge-patch semantics).
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct ArtifactUpdated {
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub name: Option<String>,
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub content: Option<String>,
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub content_hash: Option<crate::ContentHash>,
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub metadata: Option<serde_json::Value>,
285}
286
287impl EventPayload for ArtifactUpdated {
288    const KIND: EventKind = EventKind::ARTIFACT_UPDATED;
289}
290
291/// Payload for `ARTIFACT_SUPERSEDED` events.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct ArtifactSuperseded {
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub superseded_by: Option<crate::ArtifactId>,
296}
297
298impl EventPayload for ArtifactSuperseded {
299    const KIND: EventKind = EventKind::ARTIFACT_SUPERSEDED;
300}
301
302/// Payload for `ARTIFACT_DELETED` events.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct ArtifactDeleted {}
305
306impl EventPayload for ArtifactDeleted {
307    const KIND: EventKind = EventKind::ARTIFACT_DELETED;
308}
309
310// ============================================================================
311// NOTE EVENT PAYLOADS (0x4xxx)
312// ============================================================================
313
314/// Payload for `NOTE_CREATED` events.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct NoteCreated {
317    pub note_id: crate::NoteId,
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub note_type: Option<crate::NoteType>,
320    #[serde(default)]
321    pub title: String,
322    #[serde(default)]
323    pub content: String,
324    #[serde(default, skip_serializing_if = "Option::is_none")]
325    pub content_hash: Option<crate::ContentHash>,
326    #[serde(default, skip_serializing_if = "Option::is_none")]
327    pub ttl: Option<crate::TTL>,
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub abstraction_level: Option<crate::AbstractionLevel>,
330    #[serde(default)]
331    pub source_trajectory_ids: Vec<crate::TrajectoryId>,
332    #[serde(default)]
333    pub source_artifact_ids: Vec<crate::ArtifactId>,
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub metadata: Option<serde_json::Value>,
336}
337
338impl EventPayload for NoteCreated {
339    const KIND: EventKind = EventKind::NOTE_CREATED;
340}
341
342/// Payload for `NOTE_UPDATED` events (JSON merge-patch semantics).
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct NoteUpdated {
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub title: Option<String>,
347    #[serde(default, skip_serializing_if = "Option::is_none")]
348    pub content: Option<String>,
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub content_hash: Option<crate::ContentHash>,
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    pub metadata: Option<serde_json::Value>,
353}
354
355impl EventPayload for NoteUpdated {
356    const KIND: EventKind = EventKind::NOTE_UPDATED;
357}
358
359/// Payload for `NOTE_SUPERSEDED` events.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct NoteSuperseded {
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    pub superseded_by: Option<crate::NoteId>,
364}
365
366impl EventPayload for NoteSuperseded {
367    const KIND: EventKind = EventKind::NOTE_SUPERSEDED;
368}
369
370/// Payload for `NOTE_DELETED` events.
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct NoteDeleted {}
373
374impl EventPayload for NoteDeleted {
375    const KIND: EventKind = EventKind::NOTE_DELETED;
376}
377
378/// Payload for `NOTE_ACCESSED` events.
379#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct NoteAccessed {}
381
382impl EventPayload for NoteAccessed {
383    const KIND: EventKind = EventKind::NOTE_ACCESSED;
384}
385
386// ============================================================================
387// AGENT / BDI EVENT PAYLOADS (0x6xxx)
388// ============================================================================
389
390/// Payload for `GOAL_CREATED` events.
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct GoalCreated {
393    pub agent_id: uuid::Uuid,
394    pub goal_id: crate::GoalId,
395    #[serde(default)]
396    pub description: String,
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    pub goal_type: Option<String>,
399    #[serde(default)]
400    pub priority: i32,
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub parent_id: Option<crate::GoalId>,
403    #[serde(default, skip_serializing_if = "Option::is_none")]
404    pub deadline: Option<chrono::DateTime<chrono::Utc>>,
405}
406
407impl EventPayload for GoalCreated {
408    const KIND: EventKind = EventKind::GOAL_CREATED;
409}
410
411/// Payload for `GOAL_ACTIVATED` events.
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct GoalActivated {
414    pub agent_id: uuid::Uuid,
415    pub goal_id: crate::GoalId,
416}
417
418impl EventPayload for GoalActivated {
419    const KIND: EventKind = EventKind::GOAL_ACTIVATED;
420}
421
422/// Payload for `GOAL_ACHIEVED` events.
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct GoalAchieved {
425    pub agent_id: uuid::Uuid,
426    pub goal_id: crate::GoalId,
427}
428
429impl EventPayload for GoalAchieved {
430    const KIND: EventKind = EventKind::GOAL_ACHIEVED;
431}
432
433/// Payload for `GOAL_FAILED` events.
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct GoalFailed {
436    pub agent_id: uuid::Uuid,
437    pub goal_id: crate::GoalId,
438    #[serde(default, skip_serializing_if = "Option::is_none")]
439    pub reason: Option<String>,
440}
441
442impl EventPayload for GoalFailed {
443    const KIND: EventKind = EventKind::GOAL_FAILED;
444}
445
446/// Payload for `PLAN_CREATED` events.
447#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct PlanCreated {
449    pub agent_id: uuid::Uuid,
450    pub plan_id: crate::PlanId,
451    pub goal_id: crate::GoalId,
452    #[serde(default)]
453    pub step_count: usize,
454}
455
456impl EventPayload for PlanCreated {
457    const KIND: EventKind = EventKind::PLAN_CREATED;
458}
459
460/// Payload for `PLAN_COMPLETED` events.
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct PlanCompleted {
463    pub agent_id: uuid::Uuid,
464    pub plan_id: crate::PlanId,
465    pub goal_id: crate::GoalId,
466}
467
468impl EventPayload for PlanCompleted {
469    const KIND: EventKind = EventKind::PLAN_COMPLETED;
470}
471
472/// Payload for `PLAN_FAILED` events.
473#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct PlanFailed {
475    pub agent_id: uuid::Uuid,
476    pub plan_id: crate::PlanId,
477    pub goal_id: crate::GoalId,
478}
479
480impl EventPayload for PlanFailed {
481    const KIND: EventKind = EventKind::PLAN_FAILED;
482}
483
484/// Payload for `STEP_COMPLETED` events.
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct StepCompleted {
487    pub agent_id: uuid::Uuid,
488    pub plan_id: crate::PlanId,
489    pub step_id: crate::StepId,
490}
491
492impl EventPayload for StepCompleted {
493    const KIND: EventKind = EventKind::STEP_COMPLETED;
494}
495
496/// Payload for `STEP_FAILED` events.
497#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct StepFailed {
499    pub agent_id: uuid::Uuid,
500    pub plan_id: crate::PlanId,
501    pub step_id: crate::StepId,
502    #[serde(default, skip_serializing_if = "Option::is_none")]
503    pub error: Option<String>,
504}
505
506impl EventPayload for StepFailed {
507    const KIND: EventKind = EventKind::STEP_FAILED;
508}
509
510/// Payload for `BELIEF_CREATED` events.
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct BeliefCreated {
513    pub agent_id: uuid::Uuid,
514    pub belief_id: crate::BeliefId,
515    #[serde(default)]
516    pub content: String,
517    #[serde(default)]
518    pub confidence: f32,
519    #[serde(default, skip_serializing_if = "Option::is_none")]
520    pub source: Option<String>,
521}
522
523impl EventPayload for BeliefCreated {
524    const KIND: EventKind = EventKind::BELIEF_CREATED;
525}
526
527/// Payload for `BELIEF_SUPERSEDED` events.
528#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct BeliefSuperseded {
530    pub agent_id: uuid::Uuid,
531    pub old_belief_id: crate::BeliefId,
532    pub new_belief_id: crate::BeliefId,
533    #[serde(default)]
534    pub new_content: String,
535}
536
537impl EventPayload for BeliefSuperseded {
538    const KIND: EventKind = EventKind::BELIEF_SUPERSEDED;
539}
540
541/// Payload for `ENGINE_STATE_PERSISTED` events.
542///
543/// Contains a full BDI engine snapshot for fast reconstruction.
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct EngineStatePersisted {
546    pub agent_id: uuid::Uuid,
547    /// Full snapshot of beliefs, goals, and plans.
548    pub snapshot: serde_json::Value,
549}
550
551impl EventPayload for EngineStatePersisted {
552    const KIND: EventKind = EventKind::ENGINE_STATE_PERSISTED;
553}
554
555// ============================================================================
556// PCP MEMORY EVENT PAYLOADS (0xE0xx)
557// ============================================================================
558
559/// Payload for `MEMORY_COMMIT_CREATED` events.
560///
561/// Records a PCP MemoryCommit to the DAG for durable decision audit trail.
562#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct MemoryCommitCreated {
564    pub commit_id: uuid::Uuid,
565    pub trajectory_id: crate::TrajectoryId,
566    pub scope_id: crate::ScopeId,
567    #[serde(default)]
568    pub query: String,
569    #[serde(default)]
570    pub mode: String,
571    #[serde(default)]
572    pub tokens_used: i32,
573    #[serde(default)]
574    pub cost: f64,
575}
576
577impl EventPayload for MemoryCommitCreated {
578    const KIND: EventKind = EventKind::MEMORY_COMMIT_CREATED;
579}
580
581// ============================================================================
582// PCP CONTEXT COMMIT PAYLOAD
583// ============================================================================
584
585/// Full context commit for PCP recall.
586///
587/// Unlike `MemoryCommitCreated` (lightweight audit stub), this payload carries
588/// the full content needed for SQL-based recall via `pcp_recall()`.
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct ContextCommit {
591    pub tenant_id: crate::TenantId,
592    pub trajectory_id: crate::TrajectoryId,
593    pub scope_id: crate::ScopeId,
594    #[serde(default, skip_serializing_if = "Option::is_none")]
595    pub agent_id: Option<crate::AgentId>,
596    /// The response/context being committed.
597    pub content: String,
598    /// Interaction mode (e.g., "standard", "deep_work", "super_think").
599    #[serde(default)]
600    pub mode: String,
601    /// The input query that produced this context.
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub query: Option<String>,
604    #[serde(default, skip_serializing_if = "Option::is_none")]
605    pub reasoning_trace: Option<serde_json::Value>,
606    #[serde(default, skip_serializing_if = "Vec::is_empty")]
607    pub artifacts_referenced: Vec<crate::ArtifactId>,
608    #[serde(default, skip_serializing_if = "Vec::is_empty")]
609    pub notes_referenced: Vec<crate::NoteId>,
610    #[serde(default, skip_serializing_if = "Vec::is_empty")]
611    pub tools_invoked: Vec<String>,
612    #[serde(default)]
613    pub tokens_input: i64,
614    #[serde(default)]
615    pub tokens_output: i64,
616    #[serde(default, skip_serializing_if = "Option::is_none")]
617    pub metadata: Option<serde_json::Value>,
618}
619
620impl EventPayload for ContextCommit {
621    const KIND: EventKind = EventKind::CONTEXT_COMMIT;
622}
623
624// ============================================================================
625// TESTS
626// ============================================================================
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631    use crate::event::{DagPosition, EventHeader};
632    use crate::EntityIdType;
633
634    fn make_event(kind: EventKind, payload: serde_json::Value) -> Event<serde_json::Value> {
635        let event_id = crate::EventId::now_v7();
636        let header = EventHeader::new(
637            event_id,
638            event_id,
639            chrono::Utc::now().timestamp_micros(),
640            DagPosition::root(),
641            0,
642            kind,
643        );
644        Event::new(header, payload)
645    }
646
647    #[test]
648    fn test_decode_trajectory_created() {
649        let tid = crate::TrajectoryId::now_v7();
650        let payload = serde_json::json!({
651            "trajectory_id": tid.as_uuid().to_string(),
652            "name": "test-trajectory",
653            "description": "A test trajectory",
654        });
655        let event = make_event(EventKind::TRAJECTORY_CREATED, payload);
656
657        let decoded = event.decode_payload::<TrajectoryCreated>().unwrap();
658        assert_eq!(decoded.trajectory_id, tid);
659        assert_eq!(decoded.name, "test-trajectory");
660        assert_eq!(decoded.description, Some("A test trajectory".to_string()));
661        assert!(decoded.parent_trajectory_id.is_none());
662    }
663
664    #[test]
665    fn test_decode_wrong_kind_returns_none() {
666        let payload = serde_json::json!({"name": "test"});
667        let event = make_event(EventKind::SCOPE_CREATED, payload);
668
669        assert!(event.decode_payload::<TrajectoryCreated>().is_none());
670    }
671
672    #[test]
673    fn test_decode_invalid_payload_returns_none() {
674        // TRAJECTORY_CREATED requires trajectory_id
675        let payload = serde_json::json!({"invalid": true});
676        let event = make_event(EventKind::TRAJECTORY_CREATED, payload);
677
678        assert!(event.decode_payload::<TrajectoryCreated>().is_none());
679    }
680
681    #[test]
682    fn test_decode_empty_event_succeeds() {
683        // Empty events like TrajectorySuspended should decode from any object
684        let payload = serde_json::json!({"extra_field": "ignored"});
685        let event = make_event(EventKind::TRAJECTORY_SUSPENDED, payload);
686
687        assert!(event.decode_payload::<TrajectorySuspended>().is_some());
688    }
689
690    #[test]
691    fn test_erase_roundtrip() {
692        let tid = crate::TrajectoryId::now_v7();
693        let created = TrajectoryCreated {
694            trajectory_id: tid,
695            name: "roundtrip-test".to_string(),
696            description: Some("desc".to_string()),
697            parent_trajectory_id: None,
698            root_trajectory_id: None,
699            agent_id: None,
700            metadata: None,
701        };
702
703        let event_id = crate::EventId::now_v7();
704        let header = EventHeader::new(
705            event_id,
706            event_id,
707            chrono::Utc::now().timestamp_micros(),
708            DagPosition::root(),
709            0,
710            EventKind::TRAJECTORY_CREATED,
711        );
712
713        // Typed event → erase → decode → compare
714        let typed_event = Event::new(header, created);
715        let erased = typed_event.erase();
716        let decoded = erased.decode_payload::<TrajectoryCreated>().unwrap();
717
718        assert_eq!(decoded.trajectory_id, tid);
719        assert_eq!(decoded.name, "roundtrip-test");
720        assert_eq!(decoded.description, Some("desc".to_string()));
721    }
722
723    #[test]
724    fn test_decode_full_event() {
725        let tid = crate::TrajectoryId::now_v7();
726        let payload = serde_json::json!({
727            "trajectory_id": tid.as_uuid().to_string(),
728            "name": "full-event",
729        });
730        let event = make_event(EventKind::TRAJECTORY_CREATED, payload);
731
732        let decoded = event.decode::<TrajectoryCreated>().unwrap();
733        assert_eq!(decoded.payload.trajectory_id, tid);
734        assert_eq!(decoded.payload.name, "full-event");
735        assert_eq!(decoded.header.event_kind, EventKind::TRAJECTORY_CREATED);
736    }
737
738    #[test]
739    fn test_scope_created_defaults() {
740        let sid = crate::ScopeId::now_v7();
741        let tid = crate::TrajectoryId::now_v7();
742        let payload = serde_json::json!({
743            "scope_id": sid.as_uuid().to_string(),
744            "trajectory_id": tid.as_uuid().to_string(),
745        });
746        let event = make_event(EventKind::SCOPE_CREATED, payload);
747
748        let decoded = event.decode_payload::<ScopeCreated>().unwrap();
749        assert_eq!(decoded.scope_id, sid);
750        assert_eq!(decoded.trajectory_id, tid);
751        assert_eq!(decoded.token_budget, 8000); // default
752        assert_eq!(decoded.tokens_used, 0); // default
753        assert!(decoded.name.is_empty());
754    }
755}