Skip to main content

rite_model/
transcript.rs

1//! Persisted transcript schema.
2//!
3//! These types are the **durable** audit surface: what gets written to
4//! `transcript.jsonl`, what `rite verify` reads back, what report and
5//! audit tooling consume. They are deliberately kept independent of the
6//! executor and the channel plumbing, a third-party verifier can parse
7//! a transcript with only `rite-model` on its dependency list.
8//!
9//! The live runtime↔frontend channel protocol (`ExecEvent`, `UiCommand`,
10//! `Response`, `Icon`, …) lives in `rite-runtime` next to the executor
11//! that owns the channels. The boundary is **persisted vs in-flight**.
12
13use std::path::PathBuf;
14
15use serde::{Deserialize, Serialize};
16
17use crate::ir::{ActId, RoleId, StepId};
18
19/// Validator applied by the runtime to a free-form text or literal response
20/// before it is accepted.
21#[non_exhaustive]
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
24pub enum ValidatorSpec {
25    /// Reject empty or whitespace-only input.
26    NonEmpty,
27    /// Input must match this regular expression.
28    Regex(String),
29    /// Named, runtime-defined predicate (e.g. `serial_number`).
30    Predefined(String),
31}
32
33/// Request for user input, recorded into the transcript as part of
34/// [`StepFact::PromptAnswered`].
35#[non_exhaustive]
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(tag = "type", rename_all = "snake_case")]
38pub enum Prompt {
39    /// Yes / no question with an optional default.
40    Confirm {
41        /// Question shown to the user.
42        question: String,
43        /// Default selection if the user presses Enter without choosing.
44        default: Option<bool>,
45    },
46    /// Free-form text input, validated against a [`ValidatorSpec`].
47    Text {
48        /// Label shown to the user.
49        label: String,
50        /// Validator applied before the response is accepted.
51        validator: ValidatorSpec,
52    },
53    /// Sensitive input (PIN, password). Echo is suppressed; plaintext is
54    /// never serialized to the transcript.
55    Secret {
56        /// Label shown to the user.
57        label: String,
58    },
59    /// User must type a specific literal string exactly. Validation is
60    /// performed by the runtime against `expected`.
61    Literal {
62        /// Label shown to the user.
63        label: String,
64        /// Exact string the user must type.
65        expected: String,
66    },
67    /// Wait for the user to acknowledge before proceeding. Used for pacing.
68    Continue {
69        /// Optional hint such as "Press Enter when ready".
70        hint: Option<String>,
71    },
72}
73
74/// Serializable, redacted form of a user response.
75///
76/// Used inside [`StepFact::PromptAnswered`] so that the transcript records
77/// what was answered without ever persisting plaintext secrets. The
78/// in-flight `Response` type lives next to the channel protocol in
79/// `rite-runtime`; conversion happens at the moment the prompt is accepted.
80#[non_exhaustive]
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(tag = "type", rename_all = "snake_case")]
83pub enum ResponseRecord {
84    /// Yes / no answer.
85    Bool {
86        /// The answer.
87        value: bool,
88    },
89    /// Free-form text answer.
90    Text {
91        /// The answer.
92        value: String,
93    },
94    /// Secret answer. The plaintext is never stored, and no digest of it is
95    /// kept either: a hash of a low-entropy secret (such as a 6-8 digit PIV
96    /// PIN) is brute-forceable from a shared transcript. The position of the
97    /// enclosing `PromptAnswered` fact in the chain already records that a
98    /// secret was entered at this point.
99    // Note: a salted, per-run HMAC could later attest that two prompts received
100    // the same secret without reintroducing the low-entropy guessing oracle.
101    // Deferred until a concrete use case needs it.
102    SecretRedacted {},
103    /// Acknowledgement of a [`Prompt::Continue`].
104    Acknowledged,
105}
106
107/// Audit classification of a bad outcome, recorded so an auditor can tell the
108/// nature of a failure apart without parsing the free-form `message`.
109///
110/// This is the *audit* taxonomy (what an auditor sees), distinct from the
111/// runtime's `Retriability` (whether a step may re-run). For a backend error
112/// the two align: a retriable error is `Environmental`. They are kept separate
113/// because some classes never map cleanly onto retriability (an `Abort` is a
114/// decision, not an error).
115#[non_exhaustive]
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum ErrorClass {
119    /// The world wasn't ready; the step's work did not happen (token absent,
120    /// loose cable, PIN required).
121    Environmental,
122    /// The ceremony's own logic concluded badly (a verification mismatch, a
123    /// refused attestation). A result, not a recoverable condition.
124    Procedural,
125    /// The run itself is compromised or the definition is broken (transcript
126    /// write failed, channel lost, unknown action, invalid params).
127    Integrity,
128    /// The operator chose to stop. Not an error at all, but recorded on the
129    /// terminal fact so abort is distinguishable from failure.
130    Abort,
131}
132
133/// Structured error record for transcript serialization.
134#[non_exhaustive]
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ErrorRecord {
137    /// Audit classification of this error.
138    pub class: ErrorClass,
139    /// Stable kind label (e.g. `aborted`, `step_failed`, `material_load_failed`).
140    pub kind: String,
141    /// Human-readable message.
142    pub message: String,
143}
144
145impl ErrorRecord {
146    /// Construct an error record.
147    pub fn new(class: ErrorClass, kind: impl Into<String>, message: impl Into<String>) -> Self {
148        Self {
149            class,
150            kind: kind.into(),
151            message: message.into(),
152        }
153    }
154}
155
156/// Outcome of a single step, carried by [`StepFact::StepCompleted`].
157#[non_exhaustive]
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(tag = "status", rename_all = "snake_case")]
160pub enum StepOutcome {
161    /// Step executed successfully.
162    Completed {
163        /// Human-readable completion message.
164        message: String,
165    },
166}
167
168/// Durable, transcript-worthy fact.
169///
170/// Every variant is recorded by the runtime's transcript sink synchronously
171/// before being forwarded to the UI. Action handlers emit
172/// [`BackendOperation`] and [`AttestationRecorded`]; all other variants are
173/// emitted by the executor at the corresponding lifecycle boundary.
174///
175/// [`BackendOperation`]: StepFact::BackendOperation
176/// [`AttestationRecorded`]: StepFact::AttestationRecorded
177#[non_exhaustive]
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(tag = "type", rename_all = "snake_case")]
180pub enum StepFact {
181    /// Ceremony has started running.
182    CeremonyStarted {
183        /// Ceremony name from the DSL.
184        name: String,
185    },
186    /// Beginning of an act (named subdivision of a ceremony).
187    ActStarted {
188        /// Act identifier.
189        id: ActId,
190        /// Act label as authored in the DSL.
191        label: String,
192    },
193    /// Beginning of a step.
194    StepStarted {
195        /// Step identifier.
196        id: StepId,
197        /// Step label as authored in the DSL.
198        label: String,
199        /// Role responsible for this step (stable id).
200        role: RoleId,
201        /// Human-readable role name, from the ceremony's role definition.
202        /// Carried alongside the id so transcripts stay self-contained for
203        /// reports and verifiers without re-reading the ceremony YAML.
204        role_name: String,
205    },
206    /// A prompt has been answered and validated.
207    PromptAnswered {
208        /// Step that issued the prompt, if any. `None` for ceremony-level
209        /// prompts emitted before the first step (e.g. the ceremony-start
210        /// confirmation) or after the last step.
211        #[serde(skip_serializing_if = "Option::is_none")]
212        step: Option<StepId>,
213        /// The prompt as issued.
214        prompt: Prompt,
215        /// Redacted response record.
216        response: ResponseRecord,
217    },
218    /// A backend operation completed and produced structured evidence.
219    BackendOperation {
220        /// Step under which the operation ran.
221        step: StepId,
222        /// Stable operation kind (e.g. `generate_key`, `sign_data`).
223        kind: String,
224        /// Structured inputs to the operation (parameters, references).
225        inputs: serde_json::Value,
226        /// Structured outputs from the operation (artifact ids, hashes).
227        outputs: serde_json::Value,
228        /// Optional fingerprint of the produced material.
229        fingerprint: Option<String>,
230    },
231    /// A key held outside the ceremony was used as the recipient of a wrap.
232    ///
233    /// Its own fact rather than a field on the wrap operation: the method of a
234    /// wrap is publishable and the identity of a custodian may not be, and a
235    /// fact carries one confidentiality level.
236    ///
237    /// The fingerprint records the bytes that were present. Nothing in the
238    /// ceremony corroborates that they belong to the intended recipient, and
239    /// nothing shows the recipient can open the result. The fact states an
240    /// assumption the ceremony rests on, not something Rite proved.
241    WrapRecipientRecorded {
242        /// Step that wrapped to this recipient.
243        step: StepId,
244        /// The artifact or material the recipient key came from, as named in
245        /// the ceremony.
246        source: String,
247        /// `sha256:<hex>` over the recipient's SPKI DER.
248        fingerprint: String,
249        /// Whether the ceremony declared this fingerprint in advance, so the
250        /// runtime checked the key it received against the definition rather
251        /// than recording whatever arrived.
252        declared: bool,
253    },
254    /// A human attestation was recorded.
255    AttestationRecorded {
256        /// Step under which the attestation was recorded.
257        step: StepId,
258        /// Role that issued the attestation.
259        role: RoleId,
260        /// Verbatim attestation statement.
261        statement: String,
262    },
263    /// An artifact was written to disk.
264    ArtifactWritten {
265        /// Step that produced the artifact.
266        step: StepId,
267        /// Artifact name as declared in the DSL.
268        name: String,
269        /// Path on disk.
270        path: PathBuf,
271        /// Lowercase hex SHA-256 of the artifact bytes.
272        sha256: String,
273    },
274    /// A deviation was logged by the operator.
275    DeviationRecorded {
276        /// Step in which the deviation was logged, if any. `None` for
277        /// deviations logged outside of a step (before the first step or
278        /// while a ceremony-level prompt is pending).
279        #[serde(skip_serializing_if = "Option::is_none")]
280        step: Option<StepId>,
281        /// Verbatim deviation text.
282        text: String,
283    },
284    /// A step attempt failed. Recorded per attempt, so a retried step shows
285    /// `StepAttemptFailed{attempt: 1}` followed by the operator's retry
286    /// decision and, on success, `StepCompleted`. The final attempt of a step
287    /// that the run gives up on is followed by the terminal `CeremonyFailed`.
288    StepAttemptFailed {
289        /// Step whose attempt failed.
290        step: StepId,
291        /// 1-based attempt number within this step.
292        attempt: u32,
293        /// Structured error record for the failed attempt.
294        error: ErrorRecord,
295    },
296    /// Step finished executing.
297    StepCompleted {
298        /// Step identifier.
299        id: StepId,
300        /// Outcome (completed or skipped).
301        outcome: StepOutcome,
302    },
303    /// Ceremony finished successfully.
304    ///
305    /// The transcript's cryptographic identity is `SHA-256(line_bytes)`
306    /// for this line, recoverable by any reader, so the fact itself
307    /// carries no fingerprint field. The runtime forwards the value to
308    /// frontends through an out-of-band channel event.
309    CeremonyCompleted {},
310    /// Ceremony failed or was aborted.
311    CeremonyFailed {
312        /// Structured error record.
313        error: ErrorRecord,
314    },
315    /// The ceremony entropy source was seeded with machine randomness.
316    ///
317    /// Emitted once by the runner at ceremony start (run-metadata). Records
318    /// the machine contribution `m` and the frozen derivation scheme so any
319    /// value the ceremony later draws is re-derivable from the transcript
320    /// alone. Part of the [entropy source](StepFact::EntropyDrawn) family.
321    ///
322    /// Like every fact, it is timed by the `at` on its chain envelope.
323    EntropySeeded {
324        /// Lowercase hex of the gathered machine entropy `m`.
325        m: String,
326        /// Provenance of `m` (e.g. `os`). A single label today; comma-separated
327        /// if more than one source is ever mixed.
328        source: String,
329        /// Frozen derivation-scheme tag (e.g. `rite-kdf/v1`) that pins the
330        /// entire construction. A verifier rejects an unrecognised value.
331        derivation: String,
332    },
333    /// A human folded additional entropy into the seed, advancing the ratchet.
334    ///
335    /// Emitted by the authored `gather_entropy` step. The verbatim operator
336    /// contribution is recorded so the epoch chain re-folds identically; it is
337    /// public, witnessed entropy, not a secret. Timed by its enclosing step
338    /// boundaries (and by the `PromptAnswered` that captured the input).
339    EntropyContributed {
340        /// Step under which the contribution was gathered.
341        step: StepId,
342        /// Epoch index produced by this fold (1 for the first contribution).
343        epoch: u32,
344        /// Verbatim operator contribution, fed as UTF-8 into the ratchet.
345        contribution: String,
346    },
347    /// A value was drawn from the entropy source (a nonce, certificate serial,
348    /// or challenge).
349    ///
350    /// Emitted whenever an action draws bytes from the entropy source. The
351    /// derivation `path` plus the recorded seed let `rite verify` re-derive
352    /// the value and confirm the right value reached the right consumer. Like
353    /// other action-emitted evidence, it is timed by its enclosing step.
354    EntropyDrawn {
355        /// Step that drew the value.
356        step: StepId,
357        /// Derivation path `<epoch>/<step>/<purpose>`.
358        path: String,
359        /// Lowercase hex of the derived bytes. Its length fixes the byte count,
360        /// so `rite verify` re-derives exactly this many bytes from the seed.
361        value: String,
362    },
363}
364
365impl StepFact {
366    /// Whether this fact evidences work performed on the world: a backend
367    /// operation, a written artifact, a captured attestation, or consumed
368    /// entropy. Interaction records (an answered prompt, an operator
369    /// deviation note) and lifecycle markers are not side effects: repeating
370    /// the interaction is safe and simply produces a fresh record.
371    ///
372    /// The runtime's retry gate refuses to re-execute a step attempt that
373    /// already emitted a side-effect fact.
374    #[must_use]
375    pub fn is_side_effect(&self) -> bool {
376        match self {
377            StepFact::BackendOperation { .. }
378            | StepFact::AttestationRecorded { .. }
379            | StepFact::ArtifactWritten { .. }
380            | StepFact::EntropyContributed { .. }
381            | StepFact::EntropyDrawn { .. } => true,
382            StepFact::CeremonyStarted { .. }
383            | StepFact::ActStarted { .. }
384            | StepFact::StepStarted { .. }
385            | StepFact::PromptAnswered { .. }
386            // Records an input the step read, not work it performed.
387            | StepFact::WrapRecipientRecorded { .. }
388            | StepFact::DeviationRecorded { .. }
389            | StepFact::StepAttemptFailed { .. }
390            | StepFact::StepCompleted { .. }
391            | StepFact::CeremonyCompleted {}
392            | StepFact::CeremonyFailed { .. }
393            | StepFact::EntropySeeded { .. } => false,
394        }
395    }
396}
397
398/// JSON-shape snapshot tests, the tripwire for accidental wire-format breaks.
399///
400/// Every variant of [`StepFact`], [`Prompt`], [`ResponseRecord`], [`StepOutcome`],
401/// and [`ValidatorSpec`] is serialized once with fixed payloads and compared
402/// against an inline JSON literal. The on-disk transcript schema is what
403/// `rite verify`, `rite report`, and any third-party verifier consume; a
404/// rename, a `serde(tag)` change, a `rename_all` flip, or a timestamp format
405/// swap must surface here, not in the field.
406///
407/// Breaking the format is allowed in early beta, **deliberately**, update
408/// the fixture in the same commit so the diff documents the wire change.
409#[cfg(test)]
410mod schema_snapshot_tests {
411    use super::*;
412    use serde_json::json;
413
414    fn assert_json(fact: &StepFact, expected: &serde_json::Value) {
415        let actual = serde_json::to_value(fact).expect("serialize StepFact");
416        assert_eq!(&actual, expected, "wire-format drift for {fact:?}");
417    }
418
419    #[test]
420    fn ceremony_started() {
421        assert_json(
422            &StepFact::CeremonyStarted {
423                name: "Root CA".to_string(),
424            },
425            &json!({
426                "type": "ceremony_started",
427                "name": "Root CA",
428            }),
429        );
430    }
431
432    #[test]
433    fn act_started() {
434        assert_json(
435            &StepFact::ActStarted {
436                id: ActId::new("setup"),
437                label: "Setup".to_string(),
438            },
439            &json!({
440                "type": "act_started",
441                "id": "setup",
442                "label": "Setup",
443            }),
444        );
445    }
446
447    #[test]
448    fn step_started() {
449        assert_json(
450            &StepFact::StepStarted {
451                id: StepId::new("s1"),
452                label: "2.1".to_string(),
453                role: RoleId::new("crypto_officer"),
454                role_name: "Crypto Officer".to_string(),
455            },
456            &json!({
457                "type": "step_started",
458                "id": "s1",
459                "label": "2.1",
460                "role": "crypto_officer",
461                "role_name": "Crypto Officer",
462            }),
463        );
464    }
465
466    #[test]
467    fn prompt_answered_confirm_bool() {
468        assert_json(
469            &StepFact::PromptAnswered {
470                step: Some(StepId::new("s1")),
471                prompt: Prompt::Confirm {
472                    question: "Proceed?".to_string(),
473                    default: Some(true),
474                },
475                response: ResponseRecord::Bool { value: true },
476            },
477            &json!({
478                "type": "prompt_answered",
479                "step": "s1",
480                "prompt": { "type": "confirm", "question": "Proceed?", "default": true },
481                "response": { "type": "bool", "value": true },
482            }),
483        );
484    }
485
486    #[test]
487    fn prompt_answered_text_nonempty() {
488        assert_json(
489            &StepFact::PromptAnswered {
490                step: Some(StepId::new("s1")),
491                prompt: Prompt::Text {
492                    label: "Name".to_string(),
493                    validator: ValidatorSpec::NonEmpty,
494                },
495                response: ResponseRecord::Text {
496                    value: "Alice".to_string(),
497                },
498            },
499            &json!({
500                "type": "prompt_answered",
501                "step": "s1",
502                "prompt": {
503                    "type": "text",
504                    "label": "Name",
505                    "validator": { "kind": "non_empty" },
506                },
507                "response": { "type": "text", "value": "Alice" },
508            }),
509        );
510    }
511
512    #[test]
513    fn prompt_answered_text_regex() {
514        assert_json(
515            &StepFact::PromptAnswered {
516                step: Some(StepId::new("s1")),
517                prompt: Prompt::Text {
518                    label: "SN".to_string(),
519                    validator: ValidatorSpec::Regex(r"^[A-Z0-9]+$".to_string()),
520                },
521                response: ResponseRecord::Text {
522                    value: "AB12".to_string(),
523                },
524            },
525            &json!({
526                "type": "prompt_answered",
527                "step": "s1",
528                "prompt": {
529                    "type": "text",
530                    "label": "SN",
531                    "validator": { "kind": "regex", "value": "^[A-Z0-9]+$" },
532                },
533                "response": { "type": "text", "value": "AB12" },
534            }),
535        );
536    }
537
538    #[test]
539    fn prompt_answered_text_predefined() {
540        assert_json(
541            &StepFact::PromptAnswered {
542                step: Some(StepId::new("s1")),
543                prompt: Prompt::Text {
544                    label: "SN".to_string(),
545                    validator: ValidatorSpec::Predefined("serial_number".to_string()),
546                },
547                response: ResponseRecord::Text {
548                    value: "ABCD".to_string(),
549                },
550            },
551            &json!({
552                "type": "prompt_answered",
553                "step": "s1",
554                "prompt": {
555                    "type": "text",
556                    "label": "SN",
557                    "validator": { "kind": "predefined", "value": "serial_number" },
558                },
559                "response": { "type": "text", "value": "ABCD" },
560            }),
561        );
562    }
563
564    #[test]
565    fn prompt_answered_secret_redacted() {
566        assert_json(
567            &StepFact::PromptAnswered {
568                step: Some(StepId::new("s1")),
569                prompt: Prompt::Secret {
570                    label: "PIN".to_string(),
571                },
572                response: ResponseRecord::SecretRedacted {},
573            },
574            &json!({
575                "type": "prompt_answered",
576                "step": "s1",
577                "prompt": { "type": "secret", "label": "PIN" },
578                "response": { "type": "secret_redacted" },
579            }),
580        );
581    }
582
583    #[test]
584    fn prompt_answered_literal_text() {
585        assert_json(
586            &StepFact::PromptAnswered {
587                step: Some(StepId::new("s1")),
588                prompt: Prompt::Literal {
589                    label: "Type 'attest'".to_string(),
590                    expected: "attest".to_string(),
591                },
592                response: ResponseRecord::Text {
593                    value: "attest".to_string(),
594                },
595            },
596            &json!({
597                "type": "prompt_answered",
598                "step": "s1",
599                "prompt": { "type": "literal", "label": "Type 'attest'", "expected": "attest" },
600                "response": { "type": "text", "value": "attest" },
601            }),
602        );
603    }
604
605    #[test]
606    fn prompt_answered_continue_acknowledged() {
607        assert_json(
608            &StepFact::PromptAnswered {
609                step: Some(StepId::new("s1")),
610                prompt: Prompt::Continue {
611                    hint: Some("Press Enter".to_string()),
612                },
613                response: ResponseRecord::Acknowledged,
614            },
615            &json!({
616                "type": "prompt_answered",
617                "step": "s1",
618                "prompt": { "type": "continue", "hint": "Press Enter" },
619                "response": { "type": "acknowledged" },
620            }),
621        );
622    }
623
624    #[test]
625    fn backend_operation() {
626        assert_json(
627            &StepFact::BackendOperation {
628                step: StepId::new("s1"),
629                kind: "generate_key".to_string(),
630                inputs: json!({ "algorithm": "rsa", "bits": 4096 }),
631                outputs: json!({ "key_id": "k1" }),
632                fingerprint: Some("sha256:deadbeef".to_string()),
633            },
634            &json!({
635                "type": "backend_operation",
636                "step": "s1",
637                "kind": "generate_key",
638                "inputs": { "algorithm": "rsa", "bits": 4096 },
639                "outputs": { "key_id": "k1" },
640                "fingerprint": "sha256:deadbeef",
641            }),
642        );
643    }
644
645    #[test]
646    fn wrap_recipient_recorded() {
647        assert_json(
648            &StepFact::WrapRecipientRecorded {
649                step: StepId::new("s1"),
650                source: "escrow_pubkey".to_string(),
651                fingerprint: "sha256:deadbeef".to_string(),
652                declared: true,
653            },
654            &json!({
655                "type": "wrap_recipient_recorded",
656                "step": "s1",
657                "source": "escrow_pubkey",
658                "fingerprint": "sha256:deadbeef",
659                "declared": true,
660            }),
661        );
662    }
663
664    #[test]
665    fn attestation_recorded() {
666        assert_json(
667            &StepFact::AttestationRecorded {
668                step: StepId::new("s1"),
669                role: RoleId::new("crypto_officer"),
670                statement: "I confirm.".to_string(),
671            },
672            &json!({
673                "type": "attestation_recorded",
674                "step": "s1",
675                "role": "crypto_officer",
676                "statement": "I confirm.",
677            }),
678        );
679    }
680
681    #[test]
682    fn artifact_written() {
683        assert_json(
684            &StepFact::ArtifactWritten {
685                step: StepId::new("s1"),
686                name: "root.crt".to_string(),
687                path: "artifacts/root.crt".into(),
688                sha256: "a".repeat(64),
689            },
690            &json!({
691                "type": "artifact_written",
692                "step": "s1",
693                "name": "root.crt",
694                "path": "artifacts/root.crt",
695                "sha256": "a".repeat(64),
696            }),
697        );
698    }
699
700    #[test]
701    fn deviation_recorded() {
702        assert_json(
703            &StepFact::DeviationRecorded {
704                step: Some(StepId::new("s1")),
705                text: "phone rang".to_string(),
706            },
707            &json!({
708                "type": "deviation_recorded",
709                "step": "s1",
710                "text": "phone rang",
711            }),
712        );
713    }
714
715    #[test]
716    fn step_completed_completed() {
717        assert_json(
718            &StepFact::StepCompleted {
719                id: StepId::new("s1"),
720                outcome: StepOutcome::Completed {
721                    message: "done".to_string(),
722                },
723            },
724            &json!({
725                "type": "step_completed",
726                "id": "s1",
727                "outcome": { "status": "completed", "message": "done" },
728            }),
729        );
730    }
731
732    #[test]
733    fn ceremony_completed() {
734        assert_json(
735            &StepFact::CeremonyCompleted {},
736            &json!({
737                "type": "ceremony_completed",
738            }),
739        );
740    }
741
742    #[test]
743    fn ceremony_failed() {
744        assert_json(
745            &StepFact::CeremonyFailed {
746                error: ErrorRecord::new(
747                    ErrorClass::Abort,
748                    "aborted",
749                    "ceremony aborted by operator",
750                ),
751            },
752            &json!({
753                "type": "ceremony_failed",
754                "error": {
755                    "class": "abort",
756                    "kind": "aborted",
757                    "message": "ceremony aborted by operator",
758                },
759            }),
760        );
761    }
762
763    #[test]
764    fn step_attempt_failed() {
765        assert_json(
766            &StepFact::StepAttemptFailed {
767                step: StepId::new("import_key"),
768                attempt: 1,
769                error: ErrorRecord::new(
770                    ErrorClass::Environmental,
771                    "backend_error",
772                    "Token not present",
773                ),
774            },
775            &json!({
776                "type": "step_attempt_failed",
777                "step": "import_key",
778                "attempt": 1,
779                "error": {
780                    "class": "environmental",
781                    "kind": "backend_error",
782                    "message": "Token not present",
783                },
784            }),
785        );
786    }
787
788    #[test]
789    fn entropy_seeded() {
790        assert_json(
791            &StepFact::EntropySeeded {
792                m: "00112233".to_string(),
793                source: "os".to_string(),
794                derivation: "rite-kdf/v1".to_string(),
795            },
796            &json!({
797                "type": "entropy_seeded",
798                "m": "00112233",
799                "source": "os",
800                "derivation": "rite-kdf/v1",
801            }),
802        );
803    }
804
805    #[test]
806    fn entropy_contributed() {
807        assert_json(
808            &StepFact::EntropyContributed {
809                step: StepId::new("roll_dice"),
810                epoch: 1,
811                contribution: "3 1 6 4 2 5".to_string(),
812            },
813            &json!({
814                "type": "entropy_contributed",
815                "step": "roll_dice",
816                "epoch": 1,
817                "contribution": "3 1 6 4 2 5",
818            }),
819        );
820    }
821
822    #[test]
823    fn entropy_drawn() {
824        assert_json(
825            &StepFact::EntropyDrawn {
826                step: StepId::new("issue"),
827                path: "0/issue/cert-serial".to_string(),
828                value: "aabbccddeeff00112233".to_string(),
829            },
830            &json!({
831                "type": "entropy_drawn",
832                "step": "issue",
833                "path": "0/issue/cert-serial",
834                "value": "aabbccddeeff00112233",
835            }),
836        );
837    }
838}