1pub fn payload_type(suffix: &str) -> String {
11 format!("application/vnd.treeship.{}.v1+json", suffix)
12}
13
14pub const TYPE_ACTION: &str = "treeship/action/v1";
15pub const TYPE_APPROVAL: &str = "treeship/approval/v1";
16pub const TYPE_HANDOFF: &str = "treeship/handoff/v1";
17pub const TYPE_ENDORSEMENT: &str = "treeship/endorsement/v1";
18pub const TYPE_RECEIPT: &str = "treeship/receipt/v1";
19pub const TYPE_BUNDLE: &str = "treeship/bundle/v1";
20pub const TYPE_DECISION: &str = "treeship/decision/v1";
21
22mod approval_use;
26pub use approval_use::{
27 ApprovalRevocation, ApprovalUse, CheckpointKind, HubCheckpointVerification,
28 JournalCheckpoint, ReplayCheck, ReplayCheckLevel,
29 TYPE_APPROVAL_REVOCATION, TYPE_APPROVAL_USE, TYPE_JOURNAL_CHECKPOINT,
30 approval_revocation_record_digest, approval_use_record_digest,
31 journal_checkpoint_record_digest, nonce_digest, verify_hub_checkpoint_signature,
32};
33
34use serde::{Deserialize, Serialize};
35
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct SubjectRef {
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub digest: Option<String>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub uri: Option<String>,
47
48 #[serde(rename = "artifactId", skip_serializing_if = "Option::is_none")]
50 pub artifact_id: Option<String>,
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct ApprovalScope {
69 #[serde(rename = "maxActions", skip_serializing_if = "Option::is_none")]
73 pub max_actions: Option<u32>,
74
75 #[serde(rename = "validUntil", skip_serializing_if = "Option::is_none")]
80 pub valid_until: Option<String>,
81
82 #[serde(rename = "allowedActors", skip_serializing_if = "Vec::is_empty", default)]
85 pub allowed_actors: Vec<String>,
86
87 #[serde(rename = "allowedActions", skip_serializing_if = "Vec::is_empty", default)]
90 pub allowed_actions: Vec<String>,
91
92 #[serde(rename = "allowedSubjects", skip_serializing_if = "Vec::is_empty", default)]
97 pub allowed_subjects: Vec<String>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub extra: Option<serde_json::Value>,
102}
103
104impl ApprovalScope {
105 pub fn is_unscoped(&self) -> bool {
110 self.max_actions.is_none()
111 && self.valid_until.is_none()
112 && self.allowed_actors.is_empty()
113 && self.allowed_actions.is_empty()
114 && self.allowed_subjects.is_empty()
115 && self.extra.is_none()
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ActionStatement {
125 #[serde(rename = "type")]
127 pub type_: String,
128
129 pub timestamp: String,
131
132 pub actor: String,
134
135 pub action: String,
137
138 #[serde(default, skip_serializing_if = "is_empty_subject")]
139 pub subject: SubjectRef,
140
141 #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
143 pub parent_id: Option<String>,
144
145 #[serde(rename = "approvalNonce", skip_serializing_if = "Option::is_none")]
149 pub approval_nonce: Option<String>,
150
151 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
152 pub policy_ref: Option<String>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub meta: Option<serde_json::Value>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ApprovalStatement {
166 #[serde(rename = "type")]
167 pub type_: String,
168 pub timestamp: String,
169
170 pub approver: String,
172
173 #[serde(default, skip_serializing_if = "is_empty_subject")]
174 pub subject: SubjectRef,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub description: Option<String>,
178
179 #[serde(rename = "expiresAt", skip_serializing_if = "Option::is_none")]
181 pub expires_at: Option<String>,
182
183 pub delegatable: bool,
185
186 pub nonce: String,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub scope: Option<ApprovalScope>,
193
194 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
195 pub policy_ref: Option<String>,
196
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub meta: Option<serde_json::Value>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct HandoffStatement {
207 #[serde(rename = "type")]
208 pub type_: String,
209 pub timestamp: String,
210
211 pub from: String,
213 pub to: String,
215
216 pub artifacts: Vec<String>,
218
219 #[serde(rename = "approvalIds", default, skip_serializing_if = "Vec::is_empty")]
221 pub approval_ids: Vec<String>,
222
223 #[serde(default, skip_serializing_if = "Vec::is_empty")]
225 pub obligations: Vec<String>,
226
227 pub delegatable: bool,
228
229 #[serde(rename = "taskRef", skip_serializing_if = "Option::is_none")]
230 pub task_ref: Option<String>,
231
232 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
233 pub policy_ref: Option<String>,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub meta: Option<serde_json::Value>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct EndorsementStatement {
244 #[serde(rename = "type")]
245 pub type_: String,
246 pub timestamp: String,
247
248 pub endorser: String,
250 pub subject: SubjectRef,
251
252 pub kind: String,
255
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub rationale: Option<String>,
258
259 #[serde(rename = "expiresAt", skip_serializing_if = "Option::is_none")]
260 pub expires_at: Option<String>,
261
262 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
263 pub policy_ref: Option<String>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub meta: Option<serde_json::Value>,
267}
268
269impl EndorsementStatement {
270 pub fn new(endorser: impl Into<String>, kind: impl Into<String>) -> Self {
271 Self {
272 type_: TYPE_ENDORSEMENT.into(),
273 timestamp: now_rfc3339(),
274 endorser: endorser.into(),
275 subject: SubjectRef::default(),
276 kind: kind.into(),
277 rationale: None,
278 expires_at: None,
279 policy_ref: None,
280 meta: None,
281 }
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct ReceiptStatement {
290 #[serde(rename = "type")]
291 pub type_: String,
292 pub timestamp: String,
293
294 pub system: String,
297
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub subject: Option<SubjectRef>,
300
301 pub kind: String,
303
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub payload: Option<serde_json::Value>,
306
307 #[serde(rename = "payloadDigest", skip_serializing_if = "Option::is_none")]
308 pub payload_digest: Option<String>,
309
310 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
311 pub policy_ref: Option<String>,
312
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub meta: Option<serde_json::Value>,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ArtifactRef {
320 pub id: String,
321 pub digest: String,
322 #[serde(rename = "type")]
323 pub type_: String,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct BundleStatement {
329 #[serde(rename = "type")]
330 pub type_: String,
331 pub timestamp: String,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
334 pub tag: Option<String>,
335
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub description: Option<String>,
338
339 pub artifacts: Vec<ArtifactRef>,
340
341 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
342 pub policy_ref: Option<String>,
343
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub meta: Option<serde_json::Value>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct DecisionStatement {
354 #[serde(rename = "type")]
356 pub type_: String,
357
358 pub timestamp: String,
360
361 pub actor: String,
363
364 #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
366 pub parent_id: Option<String>,
367
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub model: Option<String>,
371
372 #[serde(rename = "modelVersion", skip_serializing_if = "Option::is_none")]
374 pub model_version: Option<String>,
375
376 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub provider: Option<String>,
391
392 #[serde(rename = "tokensIn", skip_serializing_if = "Option::is_none")]
394 pub tokens_in: Option<u64>,
395
396 #[serde(rename = "tokensOut", skip_serializing_if = "Option::is_none")]
398 pub tokens_out: Option<u64>,
399
400 #[serde(rename = "promptDigest", skip_serializing_if = "Option::is_none")]
402 pub prompt_digest: Option<String>,
403
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub summary: Option<String>,
407
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub confidence: Option<f64>,
411
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub alternatives: Option<Vec<String>>,
415
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub meta: Option<serde_json::Value>,
419}
420
421fn is_empty_subject(s: &SubjectRef) -> bool {
423 s.digest.is_none() && s.uri.is_none() && s.artifact_id.is_none()
424}
425
426impl ActionStatement {
429 pub fn new(actor: impl Into<String>, action: impl Into<String>) -> Self {
430 Self {
431 type_: TYPE_ACTION.into(),
432 timestamp: now_rfc3339(),
433 actor: actor.into(),
434 action: action.into(),
435 subject: SubjectRef::default(),
436 parent_id: None,
437 approval_nonce: None,
438 policy_ref: None,
439 meta: None,
440 }
441 }
442}
443
444impl ApprovalStatement {
445 pub fn new(approver: impl Into<String>, nonce: impl Into<String>) -> Self {
446 Self {
447 type_: TYPE_APPROVAL.into(),
448 timestamp: now_rfc3339(),
449 approver: approver.into(),
450 subject: SubjectRef::default(),
451 description: None,
452 expires_at: None,
453 delegatable: false,
454 nonce: nonce.into(),
455 scope: None,
456 policy_ref: None,
457 meta: None,
458 }
459 }
460}
461
462impl HandoffStatement {
463 pub fn new(
464 from: impl Into<String>,
465 to: impl Into<String>,
466 artifacts: Vec<String>,
467 ) -> Self {
468 Self {
469 type_: TYPE_HANDOFF.into(),
470 timestamp: now_rfc3339(),
471 from: from.into(),
472 to: to.into(),
473 artifacts,
474 approval_ids: vec![],
475 obligations: vec![],
476 delegatable: false,
477 task_ref: None,
478 policy_ref: None,
479 meta: None,
480 }
481 }
482}
483
484impl ReceiptStatement {
485 pub fn new(system: impl Into<String>, kind: impl Into<String>) -> Self {
486 Self {
487 type_: TYPE_RECEIPT.into(),
488 timestamp: now_rfc3339(),
489 system: system.into(),
490 subject: None,
491 kind: kind.into(),
492 payload: None,
493 payload_digest: None,
494 policy_ref: None,
495 meta: None,
496 }
497 }
498}
499
500impl DecisionStatement {
501 pub fn new(actor: impl Into<String>) -> Self {
502 Self {
503 type_: TYPE_DECISION.into(),
504 timestamp: now_rfc3339(),
505 actor: actor.into(),
506 parent_id: None,
507 model: None,
508 model_version: None,
509 provider: None,
510 tokens_in: None,
511 tokens_out: None,
512 prompt_digest: None,
513 summary: None,
514 confidence: None,
515 alternatives: None,
516 meta: None,
517 }
518 }
519}
520
521fn now_rfc3339() -> String {
522 use std::time::{SystemTime, UNIX_EPOCH};
525 let secs = SystemTime::now()
526 .duration_since(UNIX_EPOCH)
527 .unwrap_or_default()
528 .as_secs();
529 unix_to_rfc3339(secs)
530}
531
532pub fn unix_to_rfc3339(secs: u64) -> String {
533 let s = secs;
536 let (y, mo, d, h, mi, sec) = seconds_to_ymd_hms(s);
537 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, mo, d, h, mi, sec)
538}
539
540fn seconds_to_ymd_hms(s: u64) -> (u64, u64, u64, u64, u64, u64) {
541 let sec = s % 60;
542 let mins = s / 60;
543 let min = mins % 60;
544 let hrs = mins / 60;
545 let hour = hrs % 24;
546 let days = hrs / 24;
547
548 let (y, m, d) = days_to_ymd(days);
550 (y, m, d, hour, min, sec)
551}
552
553fn days_to_ymd(days: u64) -> (u64, u64, u64) {
554 let mut d = days;
556 let mut year = 1970u64;
557 loop {
558 let dy = if is_leap(year) { 366 } else { 365 };
559 if d < dy { break; }
560 d -= dy;
561 year += 1;
562 }
563 let months = if is_leap(year) {
564 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
565 } else {
566 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
567 };
568 let mut month = 1u64;
569 for dm in months {
570 if d < dm { break; }
571 d -= dm;
572 month += 1;
573 }
574 (year, month, d + 1)
575}
576
577fn is_leap(y: u64) -> bool {
578 (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use crate::attestation::{sign, Ed25519Signer, Verifier};
585
586 #[test]
587 fn payload_type_format() {
588 assert_eq!(
589 payload_type("action"),
590 "application/vnd.treeship.action.v1+json"
591 );
592 assert_eq!(
593 payload_type("approval"),
594 "application/vnd.treeship.approval.v1+json"
595 );
596 }
597
598 #[test]
599 fn action_statement_sign_verify() {
600 let signer = Ed25519Signer::generate("key_test").unwrap();
601 let verifier = Verifier::from_signer(&signer);
602
603 let mut stmt = ActionStatement::new("agent://researcher", "tool.call");
604 stmt.parent_id = Some("art_aabbccdd11223344aabbccdd11223344".into());
605
606 let pt = payload_type("action");
607 let result = sign(&pt, &stmt, &signer).unwrap();
608
609 assert!(result.artifact_id.starts_with("art_"));
610
611 let vr = verifier.verify(&result.envelope).unwrap();
612 assert_eq!(vr.artifact_id, result.artifact_id);
613
614 let decoded: ActionStatement = result.envelope.unmarshal_statement().unwrap();
616 assert_eq!(decoded.actor, "agent://researcher");
617 assert_eq!(decoded.action, "tool.call");
618 assert_eq!(decoded.type_, TYPE_ACTION);
619 }
620
621 #[test]
622 fn approval_statement_with_nonce() {
623 let signer = Ed25519Signer::generate("key_human").unwrap();
624
625 let mut approval = ApprovalStatement::new("human://alice", "nonce_abc123");
626 approval.description = Some("approve laptop purchase < $1500".into());
627 approval.scope = Some(ApprovalScope {
628 max_actions: Some(1),
629 allowed_actions: vec!["stripe.payment_intent.create".into()],
630 ..Default::default()
631 });
632
633 let pt = payload_type("approval");
634 let result = sign(&pt, &approval, &signer).unwrap();
635 assert!(result.artifact_id.starts_with("art_"));
636
637 let decoded: ApprovalStatement = result.envelope.unmarshal_statement().unwrap();
638 assert_eq!(decoded.nonce, "nonce_abc123");
639 assert_eq!(decoded.scope.unwrap().max_actions, Some(1));
640 }
641
642 #[test]
643 fn approval_scope_full_grant_roundtrips() {
644 let signer = Ed25519Signer::generate("key_piyush").unwrap();
648
649 let mut approval = ApprovalStatement::new("human://piyush", "nonce_deadbeef");
650 approval.description = Some("Deploy production after final review".into());
651 approval.scope = Some(ApprovalScope {
652 max_actions: Some(1),
653 valid_until: None,
654 allowed_actors: vec!["agent://deployer".into()],
655 allowed_actions: vec!["deploy.production".into()],
656 allowed_subjects: vec!["env://production".into()],
657 extra: None,
658 });
659
660 let pt = payload_type("approval");
661 let result = sign(&pt, &approval, &signer).unwrap();
662 let decoded: ApprovalStatement = result.envelope.unmarshal_statement().unwrap();
663 let scope = decoded.scope.expect("scope must round-trip");
664
665 assert_eq!(scope.allowed_actors, vec!["agent://deployer".to_string()]);
666 assert_eq!(scope.allowed_actions, vec!["deploy.production".to_string()]);
667 assert_eq!(scope.allowed_subjects, vec!["env://production".to_string()]);
668 assert_eq!(scope.max_actions, Some(1));
669 }
670
671 #[test]
672 fn approval_scope_is_unscoped_predicate() {
673 assert!(ApprovalScope::default().is_unscoped());
675
676 assert!(!ApprovalScope { max_actions: Some(1), ..Default::default() }.is_unscoped());
678 assert!(!ApprovalScope { valid_until: Some("2030-01-01T00:00:00Z".into()), ..Default::default() }.is_unscoped());
679 assert!(!ApprovalScope { allowed_actors: vec!["agent://x".into()], ..Default::default() }.is_unscoped());
680 assert!(!ApprovalScope { allowed_actions: vec!["doit".into()], ..Default::default() }.is_unscoped());
681 assert!(!ApprovalScope { allowed_subjects: vec!["env://prod".into()], ..Default::default() }.is_unscoped());
682 }
683
684 #[test]
685 fn approval_scope_legacy_payloads_decode_with_empty_new_fields() {
686 let legacy = serde_json::json!({
690 "maxActions": 1,
691 "allowedActions": ["stripe.payment_intent.create"]
692 });
693 let scope: ApprovalScope = serde_json::from_value(legacy).unwrap();
694 assert_eq!(scope.max_actions, Some(1));
695 assert_eq!(scope.allowed_actions, vec!["stripe.payment_intent.create".to_string()]);
696 assert!(scope.allowed_actors.is_empty());
698 assert!(scope.allowed_subjects.is_empty());
699 assert!(!scope.is_unscoped()); }
701
702 #[test]
703 fn handoff_statement() {
704 let signer = Ed25519Signer::generate("key_agent").unwrap();
705
706 let handoff = HandoffStatement::new(
707 "agent://researcher",
708 "agent://checkout",
709 vec!["art_aabbccdd11223344aabbccdd11223344".into()],
710 );
711
712 let pt = payload_type("handoff");
713 let result = sign(&pt, &handoff, &signer).unwrap();
714 let decoded: HandoffStatement = result.envelope.unmarshal_statement().unwrap();
715
716 assert_eq!(decoded.from, "agent://researcher");
717 assert_eq!(decoded.to, "agent://checkout");
718 assert_eq!(decoded.artifacts.len(), 1);
719 }
720
721 #[test]
722 fn receipt_statement() {
723 let signer = Ed25519Signer::generate("key_system").unwrap();
724
725 let mut receipt = ReceiptStatement::new("system://stripe-webhook", "confirmation");
726 receipt.payload = Some(serde_json::json!({
727 "eventId": "evt_abc123",
728 "status": "succeeded"
729 }));
730
731 let pt = payload_type("receipt");
732 let result = sign(&pt, &receipt, &signer).unwrap();
733 let decoded: ReceiptStatement = result.envelope.unmarshal_statement().unwrap();
734
735 assert_eq!(decoded.system, "system://stripe-webhook");
736 assert_eq!(decoded.kind, "confirmation");
737 }
738
739 #[test]
740 fn nonce_binding_survives_serialization() {
741 let signer = Ed25519Signer::generate("key_test").unwrap();
742
743 let approval = ApprovalStatement::new("human://alice", "secure_nonce_xyz");
746 let pt = payload_type("approval");
747 let signed = sign(&pt, &approval, &signer).unwrap();
748
749 let decoded: ApprovalStatement = signed.envelope.unmarshal_statement().unwrap();
750 assert_eq!(decoded.nonce, "secure_nonce_xyz", "nonce must survive serialization");
751 }
752
753 #[test]
754 fn decision_statement_sign_verify() {
755 let signer = Ed25519Signer::generate("key_test").unwrap();
756 let verifier = Verifier::from_signer(&signer);
757
758 let mut stmt = DecisionStatement::new("agent://analyst");
759 stmt.model = Some("claude-opus-4".into());
760 stmt.tokens_in = Some(8432);
761 stmt.tokens_out = Some(1247);
762 stmt.summary = Some("Contract looks standard.".into());
763 stmt.confidence = Some(0.91);
764
765 let pt = payload_type("decision");
766 let result = sign(&pt, &stmt, &signer).unwrap();
767
768 assert!(result.artifact_id.starts_with("art_"));
769
770 let vr = verifier.verify(&result.envelope).unwrap();
771 assert_eq!(vr.artifact_id, result.artifact_id);
772
773 let decoded: DecisionStatement = result.envelope.unmarshal_statement().unwrap();
775 assert_eq!(decoded.actor, "agent://analyst");
776 assert_eq!(decoded.model, Some("claude-opus-4".into()));
777 assert_eq!(decoded.tokens_in, Some(8432));
778 assert_eq!(decoded.tokens_out, Some(1247));
779 assert_eq!(decoded.summary, Some("Contract looks standard.".into()));
780 assert_eq!(decoded.confidence, Some(0.91));
781 assert_eq!(decoded.type_, TYPE_DECISION);
782 }
783
784 #[test]
785 fn decision_statement_provider_roundtrips() {
786 let signer = Ed25519Signer::generate("key_test").unwrap();
790 let verifier = Verifier::from_signer(&signer);
791
792 let mut stmt = DecisionStatement::new("agent://researcher");
793 stmt.model = Some("kimi-k2".into());
794 stmt.provider = Some("moonshot".into());
795
796 let pt = payload_type("decision");
797 let result = sign(&pt, &stmt, &signer).unwrap();
798 verifier.verify(&result.envelope).unwrap();
799
800 let decoded: DecisionStatement = result.envelope.unmarshal_statement().unwrap();
801 assert_eq!(decoded.model, Some("kimi-k2".into()));
802 assert_eq!(decoded.provider, Some("moonshot".into()));
803 }
804
805 #[test]
806 fn decision_statement_legacy_payload_without_provider_decodes() {
807 let raw = serde_json::json!({
813 "type": TYPE_DECISION,
814 "timestamp": "2026-04-30T12:00:00Z",
815 "actor": "agent://legacy",
816 "model": "claude-opus-4",
817 });
818 let parsed: DecisionStatement = serde_json::from_value(raw).unwrap();
819 assert_eq!(parsed.model, Some("claude-opus-4".into()));
820 assert_eq!(parsed.provider, None);
821 }
822
823 #[test]
824 fn different_statement_types_different_ids() {
825 let signer = Ed25519Signer::generate("key_test").unwrap();
828
829 let action = ActionStatement::new("agent://test", "do.thing");
830 let approval = ApprovalStatement::new("human://test", "nonce_123");
831
832 let r_action = sign(&payload_type("action"), &action, &signer).unwrap();
833 let r_approval = sign(&payload_type("approval"), &approval, &signer).unwrap();
834
835 assert_ne!(r_action.artifact_id, r_approval.artifact_id);
836 }
837
838 #[test]
839 fn timestamp_format() {
840 let ts = unix_to_rfc3339(0);
841 assert_eq!(ts, "1970-01-01T00:00:00Z");
842
843 let ts2 = unix_to_rfc3339(1_000_000_000);
844 assert_eq!(ts2, "2001-09-09T01:46:40Z");
845 }
846}