1use crate::{
4 ActorId, AgentId, ContextPack, Message, ModelName, PolicyDecision, RunId, ToolCall, ToolCallId,
5 ToolProposal, ToolResult, TurnId,
6};
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct ModelUsage {
13 pub input_tokens: u32,
15 pub output_tokens: u32,
17}
18
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct RecordedEvent {
23 pub seq: u64,
25 pub occurred_at_ms: u64,
27 pub event: HarnessEvent,
29}
30
31#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "event")]
36pub enum HarnessEvent {
37 RunStarted {
39 run_id: RunId,
41 agent_id: AgentId,
43 },
44 ContextBuilt {
46 run_id: RunId,
48 turn_id: TurnId,
50 context: ContextPack,
52 },
53 ContextCompacted {
55 run_id: RunId,
57 turn_id: TurnId,
59 estimated_tokens_before: u32,
61 estimated_tokens_after: u32,
63 dropped_turn_start: u64,
65 dropped_turn_end_exclusive: u64,
67 },
68 ModelRequested {
70 run_id: RunId,
72 turn_id: TurnId,
74 step: u32,
76 model: ModelName,
78 },
79 ModelFailed {
81 run_id: RunId,
83 turn_id: TurnId,
85 step: u32,
87 reason: String,
89 },
90 ModelResponded {
92 run_id: RunId,
94 turn_id: TurnId,
96 step: u32,
98 output: Message,
100 proposed_calls: Vec<ToolProposal>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
109 served_model: Option<ModelName>,
110 usage: Option<ModelUsage>,
114 },
115 ToolProposalsRejected {
117 run_id: RunId,
119 turn_id: TurnId,
121 reason: String,
123 },
124 ToolCallProposed {
126 run_id: RunId,
128 turn_id: TurnId,
130 call: ToolCall,
132 },
133 PolicyEvaluated {
135 run_id: RunId,
137 call_id: ToolCallId,
139 decision: PolicyDecision,
141 },
142 ApprovalGranted {
144 run_id: RunId,
146 call_id: ToolCallId,
148 actor_id: ActorId,
150 },
151 ApprovalDenied {
153 run_id: RunId,
155 call_id: ToolCallId,
157 actor_id: ActorId,
159 reason: String,
161 },
162 ToolStarted {
164 run_id: RunId,
166 call_id: ToolCallId,
168 },
169 ToolFinished {
171 run_id: RunId,
173 result: ToolResult,
175 },
176 ToolFailed {
178 run_id: RunId,
180 call_id: ToolCallId,
182 reason: String,
184 },
185 RunFinished {
187 run_id: RunId,
189 },
190 RunFailed {
192 run_id: RunId,
194 reason: String,
196 },
197}
198
199impl HarnessEvent {
200 pub fn run_id(&self) -> &RunId {
202 match self {
203 Self::RunStarted { run_id, .. }
204 | Self::ContextBuilt { run_id, .. }
205 | Self::ContextCompacted { run_id, .. }
206 | Self::ModelRequested { run_id, .. }
207 | Self::ModelFailed { run_id, .. }
208 | Self::ModelResponded { run_id, .. }
209 | Self::ToolProposalsRejected { run_id, .. }
210 | Self::ToolCallProposed { run_id, .. }
211 | Self::PolicyEvaluated { run_id, .. }
212 | Self::ApprovalGranted { run_id, .. }
213 | Self::ApprovalDenied { run_id, .. }
214 | Self::ToolStarted { run_id, .. }
215 | Self::ToolFinished { run_id, .. }
216 | Self::ToolFailed { run_id, .. }
217 | Self::RunFinished { run_id }
218 | Self::RunFailed { run_id, .. } => run_id,
219 }
220 }
221
222 pub fn name(&self) -> &'static str {
224 match self {
225 Self::RunStarted { .. } => "run_started",
226 Self::ContextBuilt { .. } => "context_built",
227 Self::ContextCompacted { .. } => "context_compacted",
228 Self::ModelRequested { .. } => "model_requested",
229 Self::ModelFailed { .. } => "model_failed",
230 Self::ModelResponded { .. } => "model_responded",
231 Self::ToolProposalsRejected { .. } => "tool_proposals_rejected",
232 Self::ToolCallProposed { .. } => "tool_call_proposed",
233 Self::PolicyEvaluated { .. } => "policy_evaluated",
234 Self::ApprovalGranted { .. } => "approval_granted",
235 Self::ApprovalDenied { .. } => "approval_denied",
236 Self::ToolStarted { .. } => "tool_started",
237 Self::ToolFinished { .. } => "tool_finished",
238 Self::ToolFailed { .. } => "tool_failed",
239 Self::RunFinished { .. } => "run_finished",
240 Self::RunFailed { .. } => "run_failed",
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::{HarnessEvent, ModelUsage, RecordedEvent};
248 use crate::{
249 ActorId, AgentId, ArtifactId, ContextFragment, ContextLane, ContextPack, EffectClass,
250 Message, MessageRole, ModelName, PolicyDecision, ResultVisibility, RunId, ToolCall,
251 ToolCallId, ToolName, ToolProposal, ToolResult, TurnId,
252 };
253 use serde_json::json;
254
255 const ACCEPTED_V0_2_0_HARNESS_EVENT_JSON: &str =
256 r#"{"event":"run_started","run_id":"run_1","agent_id":"agent_1"}"#;
257 const REJECTED_UNKNOWN_FIELD_HARNESS_EVENT_JSON: &str =
258 r#"{"event":"run_started","run_id":"run_1","agent_id":"agent_1","future_field":true}"#;
259 const PRE_FIELD_V0_3_0_MODEL_RESPONDED_JSON: &str = r#"{"seq":7,"occurred_at_ms":1700000000000,"event":{"event":"model_responded","run_id":"run_1","turn_id":"turn_1","step":1,"output":{"role":"assistant","content":"Done."},"proposed_calls":[],"usage":{"input_tokens":12,"output_tokens":4}}}"#;
260
261 #[test]
262 fn harness_event_json_schema_is_fail_closed_and_v0_2_0_compatible() {
263 let accepted: HarnessEvent =
264 serde_json::from_str(ACCEPTED_V0_2_0_HARNESS_EVENT_JSON).unwrap();
265 assert_eq!(
266 accepted,
267 HarnessEvent::RunStarted {
268 run_id: RunId::new("run_1").unwrap(),
269 agent_id: AgentId::new("agent_1").unwrap(),
270 }
271 );
272
273 let error = serde_json::from_str::<HarnessEvent>(REJECTED_UNKNOWN_FIELD_HARNESS_EVENT_JSON)
274 .unwrap_err();
275 assert!(error.to_string().contains("unknown field `future_field`"));
276 }
277
278 #[test]
279 fn recorded_event_json_fixtures_are_bidirectional() {
280 let run_id = RunId::new("run_1").unwrap();
281 let turn_id = TurnId::new("turn_1").unwrap();
282 let call_id = ToolCallId::new("call_1").unwrap();
283 let tool = ToolName::new("file.read").unwrap();
284
285 let events = [
286 HarnessEvent::RunStarted {
287 run_id: run_id.clone(),
288 agent_id: AgentId::new("agent_1").unwrap(),
289 },
290 HarnessEvent::ContextBuilt {
291 run_id: run_id.clone(),
292 turn_id: turn_id.clone(),
293 context: ContextPack {
294 token_budget: 2_048,
295 fragments: vec![
296 ContextFragment {
297 lane: ContextLane::CurrentTask,
298 source: "user".into(),
299 content: "Summarize README.md".into(),
300 estimated_tokens: 5,
301 },
302 ContextFragment {
303 lane: ContextLane::RetrievedContext,
304 source: "README.md".into(),
305 content: "Platonic core".into(),
306 estimated_tokens: 3,
307 },
308 ],
309 },
310 },
311 HarnessEvent::ContextCompacted {
312 run_id: run_id.clone(),
313 turn_id: turn_id.clone(),
314 estimated_tokens_before: 4_096,
315 estimated_tokens_after: 2_048,
316 dropped_turn_start: 0,
317 dropped_turn_end_exclusive: 3,
318 },
319 HarnessEvent::ModelRequested {
320 run_id: run_id.clone(),
321 turn_id: turn_id.clone(),
322 step: 1,
323 model: ModelName::new("model_1").unwrap(),
324 },
325 HarnessEvent::ModelFailed {
326 run_id: run_id.clone(),
327 turn_id: turn_id.clone(),
328 step: 1,
329 reason: "provider unavailable".into(),
330 },
331 HarnessEvent::ModelResponded {
332 run_id: run_id.clone(),
333 turn_id: turn_id.clone(),
334 step: 1,
335 output: Message {
336 role: MessageRole::Assistant,
337 content: "Reading the file.".into(),
338 },
339 proposed_calls: vec![ToolProposal {
340 tool: tool.clone(),
341 input: json!({ "path": "README.md" }),
342 }],
343 served_model: Some(ModelName::new("provider/model_1-2026-07-31").unwrap()),
344 usage: Some(ModelUsage {
345 input_tokens: 12,
346 output_tokens: 4,
347 }),
348 },
349 HarnessEvent::ToolProposalsRejected {
350 run_id: run_id.clone(),
351 turn_id: turn_id.clone(),
352 reason: "proposal schema invalid".into(),
353 },
354 HarnessEvent::ToolCallProposed {
355 run_id: run_id.clone(),
356 turn_id: turn_id.clone(),
357 call: ToolCall {
358 id: call_id.clone(),
359 tool: tool.clone(),
360 effect: EffectClass::ReadOnly,
361 input: json!({ "path": "README.md" }),
362 },
363 },
364 HarnessEvent::PolicyEvaluated {
365 run_id: run_id.clone(),
366 call_id: call_id.clone(),
367 decision: PolicyDecision::RequireApproval {
368 reason: "workspace write requires approval".into(),
369 },
370 },
371 HarnessEvent::ApprovalGranted {
372 run_id: run_id.clone(),
373 call_id: call_id.clone(),
374 actor_id: ActorId::new("actor_1").unwrap(),
375 },
376 HarnessEvent::ApprovalDenied {
377 run_id: run_id.clone(),
378 call_id: call_id.clone(),
379 actor_id: ActorId::new("actor_1").unwrap(),
380 reason: "not approved".into(),
381 },
382 HarnessEvent::ToolStarted {
383 run_id: run_id.clone(),
384 call_id: call_id.clone(),
385 },
386 HarnessEvent::ToolFinished {
387 run_id: run_id.clone(),
388 result: ToolResult {
389 call_id: call_id.clone(),
390 summary: "read 13 bytes".into(),
391 data: json!({ "contents": "Platonic core" }),
392 artifacts: vec![ArtifactId::new("artifact_1").unwrap()],
393 visibility: ResultVisibility::Both,
394 },
395 },
396 HarnessEvent::ToolFailed {
397 run_id: run_id.clone(),
398 call_id,
399 reason: "file not found".into(),
400 },
401 HarnessEvent::RunFinished {
402 run_id: run_id.clone(),
403 },
404 HarnessEvent::RunFailed {
405 run_id,
406 reason: "model unavailable".into(),
407 },
408 ];
409
410 for event in events {
411 let name = event.name();
412 let fixture = match &event {
413 HarnessEvent::RunStarted { .. } => json!({
414 "seq": 7,
415 "occurred_at_ms": 1_700_000_000_000_u64,
416 "event": {
417 "event": "run_started",
418 "run_id": "run_1",
419 "agent_id": "agent_1"
420 }
421 }),
422 HarnessEvent::ContextBuilt { .. } => json!({
423 "seq": 7,
424 "occurred_at_ms": 1_700_000_000_000_u64,
425 "event": {
426 "event": "context_built",
427 "run_id": "run_1",
428 "turn_id": "turn_1",
429 "context": {
430 "token_budget": 2_048,
431 "fragments": [
432 {
433 "lane": "current_task",
434 "source": "user",
435 "content": "Summarize README.md",
436 "estimated_tokens": 5
437 },
438 {
439 "lane": "retrieved_context",
440 "source": "README.md",
441 "content": "Platonic core",
442 "estimated_tokens": 3
443 }
444 ]
445 }
446 }
447 }),
448 HarnessEvent::ContextCompacted { .. } => json!({
449 "seq": 7,
450 "occurred_at_ms": 1_700_000_000_000_u64,
451 "event": {
452 "event": "context_compacted",
453 "run_id": "run_1",
454 "turn_id": "turn_1",
455 "estimated_tokens_before": 4_096,
456 "estimated_tokens_after": 2_048,
457 "dropped_turn_start": 0,
458 "dropped_turn_end_exclusive": 3
459 }
460 }),
461 HarnessEvent::ModelRequested { .. } => json!({
462 "seq": 7,
463 "occurred_at_ms": 1_700_000_000_000_u64,
464 "event": {
465 "event": "model_requested",
466 "run_id": "run_1",
467 "turn_id": "turn_1",
468 "step": 1,
469 "model": "model_1"
470 }
471 }),
472 HarnessEvent::ModelFailed { .. } => json!({
473 "seq": 7,
474 "occurred_at_ms": 1_700_000_000_000_u64,
475 "event": {
476 "event": "model_failed",
477 "run_id": "run_1",
478 "turn_id": "turn_1",
479 "step": 1,
480 "reason": "provider unavailable"
481 }
482 }),
483 HarnessEvent::ModelResponded { .. } => json!({
484 "seq": 7,
485 "occurred_at_ms": 1_700_000_000_000_u64,
486 "event": {
487 "event": "model_responded",
488 "run_id": "run_1",
489 "turn_id": "turn_1",
490 "step": 1,
491 "output": {
492 "role": "assistant",
493 "content": "Reading the file."
494 },
495 "proposed_calls": [
496 {
497 "tool": "file.read",
498 "input": { "path": "README.md" }
499 }
500 ],
501 "served_model": "provider/model_1-2026-07-31",
502 "usage": {
503 "input_tokens": 12,
504 "output_tokens": 4
505 }
506 }
507 }),
508 HarnessEvent::ToolProposalsRejected { .. } => json!({
509 "seq": 7,
510 "occurred_at_ms": 1_700_000_000_000_u64,
511 "event": {
512 "event": "tool_proposals_rejected",
513 "run_id": "run_1",
514 "turn_id": "turn_1",
515 "reason": "proposal schema invalid"
516 }
517 }),
518 HarnessEvent::ToolCallProposed { .. } => json!({
519 "seq": 7,
520 "occurred_at_ms": 1_700_000_000_000_u64,
521 "event": {
522 "event": "tool_call_proposed",
523 "run_id": "run_1",
524 "turn_id": "turn_1",
525 "call": {
526 "id": "call_1",
527 "tool": "file.read",
528 "effect": "read_only",
529 "input": { "path": "README.md" }
530 }
531 }
532 }),
533 HarnessEvent::PolicyEvaluated { .. } => json!({
534 "seq": 7,
535 "occurred_at_ms": 1_700_000_000_000_u64,
536 "event": {
537 "event": "policy_evaluated",
538 "run_id": "run_1",
539 "call_id": "call_1",
540 "decision": {
541 "decision": "require_approval",
542 "reason": "workspace write requires approval"
543 }
544 }
545 }),
546 HarnessEvent::ApprovalGranted { .. } => json!({
547 "seq": 7,
548 "occurred_at_ms": 1_700_000_000_000_u64,
549 "event": {
550 "event": "approval_granted",
551 "run_id": "run_1",
552 "call_id": "call_1",
553 "actor_id": "actor_1"
554 }
555 }),
556 HarnessEvent::ApprovalDenied { .. } => json!({
557 "seq": 7,
558 "occurred_at_ms": 1_700_000_000_000_u64,
559 "event": {
560 "event": "approval_denied",
561 "run_id": "run_1",
562 "call_id": "call_1",
563 "actor_id": "actor_1",
564 "reason": "not approved"
565 }
566 }),
567 HarnessEvent::ToolStarted { .. } => json!({
568 "seq": 7,
569 "occurred_at_ms": 1_700_000_000_000_u64,
570 "event": {
571 "event": "tool_started",
572 "run_id": "run_1",
573 "call_id": "call_1"
574 }
575 }),
576 HarnessEvent::ToolFinished { .. } => json!({
577 "seq": 7,
578 "occurred_at_ms": 1_700_000_000_000_u64,
579 "event": {
580 "event": "tool_finished",
581 "run_id": "run_1",
582 "result": {
583 "call_id": "call_1",
584 "summary": "read 13 bytes",
585 "data": { "contents": "Platonic core" },
586 "artifacts": ["artifact_1"],
587 "visibility": "both"
588 }
589 }
590 }),
591 HarnessEvent::ToolFailed { .. } => json!({
592 "seq": 7,
593 "occurred_at_ms": 1_700_000_000_000_u64,
594 "event": {
595 "event": "tool_failed",
596 "run_id": "run_1",
597 "call_id": "call_1",
598 "reason": "file not found"
599 }
600 }),
601 HarnessEvent::RunFinished { .. } => json!({
602 "seq": 7,
603 "occurred_at_ms": 1_700_000_000_000_u64,
604 "event": {
605 "event": "run_finished",
606 "run_id": "run_1"
607 }
608 }),
609 HarnessEvent::RunFailed { .. } => json!({
610 "seq": 7,
611 "occurred_at_ms": 1_700_000_000_000_u64,
612 "event": {
613 "event": "run_failed",
614 "run_id": "run_1",
615 "reason": "model unavailable"
616 }
617 }),
618 };
619
620 let expected = RecordedEvent {
621 seq: 7,
622 occurred_at_ms: 1_700_000_000_000,
623 event,
624 };
625 let decoded: RecordedEvent = serde_json::from_value(fixture.clone()).unwrap();
626
627 assert_eq!(decoded, expected, "failed to decode {name} fixture");
628 assert_eq!(
629 serde_json::to_value(&expected).unwrap(),
630 fixture,
631 "failed to encode {name} fixture"
632 );
633 }
634 }
635
636 #[test]
637 fn model_response_usage_json_fixtures_are_bidirectional() {
638 let fixtures = [
639 (
640 "known",
641 json!({
642 "seq": 7,
643 "occurred_at_ms": 1_700_000_000_000_u64,
644 "event": {
645 "event": "model_responded",
646 "run_id": "run_1",
647 "turn_id": "turn_1",
648 "step": 1,
649 "output": {
650 "role": "assistant",
651 "content": "Done."
652 },
653 "proposed_calls": [],
654 "usage": {
655 "input_tokens": 12,
656 "output_tokens": 4
657 }
658 }
659 }),
660 Some(ModelUsage {
661 input_tokens: 12,
662 output_tokens: 4,
663 }),
664 ),
665 (
666 "zero",
667 json!({
668 "seq": 7,
669 "occurred_at_ms": 1_700_000_000_000_u64,
670 "event": {
671 "event": "model_responded",
672 "run_id": "run_1",
673 "turn_id": "turn_1",
674 "step": 1,
675 "output": {
676 "role": "assistant",
677 "content": "Done."
678 },
679 "proposed_calls": [],
680 "usage": {
681 "input_tokens": 0,
682 "output_tokens": 0
683 }
684 }
685 }),
686 Some(ModelUsage {
687 input_tokens: 0,
688 output_tokens: 0,
689 }),
690 ),
691 (
692 "unknown",
693 json!({
694 "seq": 7,
695 "occurred_at_ms": 1_700_000_000_000_u64,
696 "event": {
697 "event": "model_responded",
698 "run_id": "run_1",
699 "turn_id": "turn_1",
700 "step": 1,
701 "output": {
702 "role": "assistant",
703 "content": "Done."
704 },
705 "proposed_calls": [],
706 "usage": null
707 }
708 }),
709 None,
710 ),
711 ];
712
713 for (name, fixture, usage) in fixtures {
714 let expected = RecordedEvent {
715 seq: 7,
716 occurred_at_ms: 1_700_000_000_000,
717 event: HarnessEvent::ModelResponded {
718 run_id: RunId::new("run_1").unwrap(),
719 turn_id: TurnId::new("turn_1").unwrap(),
720 step: 1,
721 output: Message {
722 role: MessageRole::Assistant,
723 content: "Done.".into(),
724 },
725 proposed_calls: vec![],
726 served_model: None,
727 usage,
728 },
729 };
730 let decoded: RecordedEvent = serde_json::from_value(fixture.clone()).unwrap();
731
732 assert_eq!(decoded, expected, "failed to decode {name} usage fixture");
733 assert_eq!(
734 serde_json::to_value(&expected).unwrap(),
735 fixture,
736 "failed to encode {name} usage fixture"
737 );
738 }
739 }
740
741 #[test]
742 fn served_model_json_fixtures_are_bidirectional_and_v0_3_0_compatible() {
743 let fixtures = [
744 (
745 "known",
746 json!({
747 "seq": 7,
748 "occurred_at_ms": 1_700_000_000_000_u64,
749 "event": {
750 "event": "model_responded",
751 "run_id": "run_1",
752 "turn_id": "turn_1",
753 "step": 1,
754 "output": {
755 "role": "assistant",
756 "content": "Done."
757 },
758 "proposed_calls": [],
759 "served_model": "openai/gpt-5.2-2026-07-31",
760 "usage": null
761 }
762 }),
763 Some(ModelName::new("openai/gpt-5.2-2026-07-31").unwrap()),
764 ),
765 (
766 "unknown",
767 json!({
768 "seq": 7,
769 "occurred_at_ms": 1_700_000_000_000_u64,
770 "event": {
771 "event": "model_responded",
772 "run_id": "run_1",
773 "turn_id": "turn_1",
774 "step": 1,
775 "output": {
776 "role": "assistant",
777 "content": "Done."
778 },
779 "proposed_calls": [],
780 "usage": null
781 }
782 }),
783 None,
784 ),
785 ];
786
787 for (name, fixture, served_model) in fixtures {
788 let expected = RecordedEvent {
789 seq: 7,
790 occurred_at_ms: 1_700_000_000_000,
791 event: HarnessEvent::ModelResponded {
792 run_id: RunId::new("run_1").unwrap(),
793 turn_id: TurnId::new("turn_1").unwrap(),
794 step: 1,
795 output: Message {
796 role: MessageRole::Assistant,
797 content: "Done.".into(),
798 },
799 proposed_calls: vec![],
800 served_model,
801 usage: None,
802 },
803 };
804 let decoded: RecordedEvent = serde_json::from_value(fixture.clone()).unwrap();
805
806 assert_eq!(decoded, expected, "failed to decode {name} fixture");
807 assert_eq!(
808 serde_json::to_value(&expected).unwrap(),
809 fixture,
810 "failed to encode {name} fixture"
811 );
812 }
813
814 let pre_field_fixture =
815 serde_json::from_str::<serde_json::Value>(PRE_FIELD_V0_3_0_MODEL_RESPONDED_JSON)
816 .unwrap();
817 let decoded: RecordedEvent = serde_json::from_value(pre_field_fixture.clone()).unwrap();
818 let expected = RecordedEvent {
819 seq: 7,
820 occurred_at_ms: 1_700_000_000_000,
821 event: HarnessEvent::ModelResponded {
822 run_id: RunId::new("run_1").unwrap(),
823 turn_id: TurnId::new("turn_1").unwrap(),
824 step: 1,
825 output: Message {
826 role: MessageRole::Assistant,
827 content: "Done.".into(),
828 },
829 proposed_calls: vec![],
830 served_model: None,
831 usage: Some(ModelUsage {
832 input_tokens: 12,
833 output_tokens: 4,
834 }),
835 },
836 };
837
838 assert_eq!(decoded, expected);
839 assert_eq!(serde_json::to_value(expected).unwrap(), pre_field_fixture);
840 }
841
842 #[test]
843 fn served_model_reuses_model_name_validation() {
844 let mut fixture = json!({
845 "seq": 7,
846 "occurred_at_ms": 1_700_000_000_000_u64,
847 "event": {
848 "event": "model_responded",
849 "run_id": "run_1",
850 "turn_id": "turn_1",
851 "step": 1,
852 "output": {
853 "role": "assistant",
854 "content": "Done."
855 },
856 "proposed_calls": [],
857 "served_model": " ",
858 "usage": null
859 }
860 });
861 let error = serde_json::from_value::<RecordedEvent>(fixture.clone()).unwrap_err();
862
863 assert!(error.to_string().contains("ModelName cannot be empty"));
864
865 fixture["event"]["served_model"] = json!(7);
866 assert!(serde_json::from_value::<RecordedEvent>(fixture).is_err());
867 }
868}