pointlock_ir/record.rs
1//! Durable execution records and the checkpoint materialization view.
2//!
3//! Definition home adjudicated to `pointlock-ir` (type truth source, R12) so
4//! `pointlock-store` and `pointlock-runner` share one shape without violating
5//! the dependency direction; pending spine batch incorporation.
6//!
7//! Shapes follow spine §6.6 (`CheckpointView`/`StepRecord`), §6.7-A
8//! (`AlignmentReport`), and 07 §3.2 (`CallFrame` refinement,
9//! `humanPending.purpose`).
10
11use std::collections::BTreeMap;
12
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17use crate::primitives::{ActionName, AssertId, FlowId, Hash, StepId};
18use crate::run_path::RunPath;
19use crate::runtime::{EvidenceRef, StepVerdict, Viewport};
20use crate::vocab::{
21 ActChannel, AlignmentClass, Channel, CoordinateFallbackReason, ErrorClass, ExecutionMode,
22 HumanMode, HumanPurpose, ScreenshotOmissionReason, StepState, UiSnapshotOmissionReason,
23 VerdictStatus,
24};
25
26/// Discriminant-only projection of an `ActionOutcome` for durable attempt
27/// records (the full outcome lives in the RunLog `actionSettled` payload).
28/// Pending incorporation.
29#[derive(
30 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
31)]
32#[serde(rename_all = "camelCase")]
33pub enum ActionOutcomeKind {
34 /// Completed with a result.
35 Succeeded,
36 /// Failed with a structured error.
37 Failed,
38 /// Cooperatively cancelled.
39 Cancelled,
40 /// Action budget elapsed.
41 TimedOut,
42}
43
44/// One attempt of an action step (spine §6.6): every dispatch, successful
45/// or not, leaves one record keyed by its WAL callId.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
47#[serde(rename_all = "camelCase", deny_unknown_fields)]
48pub struct AttemptRecord {
49 /// The caller-generated action id (matches the `actionIntent` WAL entry).
50 pub call_id: String,
51 /// Four-way terminal discriminant.
52 pub outcome: ActionOutcomeKind,
53 /// Runner-side error classification, for non-succeeded outcomes.
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub error_class: Option<ErrorClass>,
56 /// The execution mode the provider reported.
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub execution_mode: Option<ExecutionMode>,
59 /// Fallback reason when `executionMode` is `coordinateFallback`.
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub fallback_reason: Option<CoordinateFallbackReason>,
62 /// 1-based `binding.attempts` position of the dispatch (2026-07-18
63 /// incorporation, item ② — carried from the `actionIntent` event;
64 /// absent on pre-incorporation ledgers).
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub chain_index: Option<u32>,
67 /// The dispatched attempt's locating channel.
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub channel: Option<ActChannel>,
70 /// The dispatched attempt's provider-native action name.
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub action_name: Option<ActionName>,
73}
74
75/// The provider-side session profile captured at a failure or suspension
76/// instant (07 §2.2, incorporated 2026-07-18): pure forensic material for
77/// the dossier/overview — resume reconcile, the fold's binding provenance
78/// and alignment never consume it. Rides `stepExited` (fail/unknown
79/// verdict in force at exit) and `runSuspended` as an additive optional
80/// payload field; absent on ledgers recorded before incorporation.
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
82#[serde(rename_all = "camelCase", deny_unknown_fields)]
83pub struct ProviderStateSummary {
84 /// Known session generations, best-effort: the checkpoint-known
85 /// lineage plus the live session observed at capture.
86 pub session_lineage: Vec<String>,
87 /// The capture-instant cursor (`currentCursor()` RPC). Absent when
88 /// the call failed — never a stale bind-time value (principle 4).
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub event_cursor: Option<EventCursor>,
91 /// The attested capability baseline at capture.
92 pub attestation: AttestationSnapshot,
93 /// `ProviderSession::health()` at capture; a failed call records
94 /// `{ ok: false, degraded: "<errorClass>" }`.
95 pub health: SessionHealthSnapshot,
96 /// The bound device.
97 pub device_id: String,
98 /// `lockfile.device.platform`; absent when the assembly layer did
99 /// not supply it (the SPI attestation does not carry it).
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub platform: Option<String>,
102}
103
104/// The two attestation facts 07 §2.2 pins into the summary (deliberately
105/// not the full `CapabilityAttestation`).
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
107#[serde(rename_all = "camelCase", deny_unknown_fields)]
108pub struct AttestationSnapshot {
109 /// Digest of the attested lockfile.
110 pub lockfile_digest: Hash,
111 /// Attestation timestamp, verbatim.
112 pub attested_at: String,
113}
114
115/// Wire twin of the provider-kit `SessionHealth` (defined here because
116/// provider-kit depends on ir, not vice versa).
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
118#[serde(rename_all = "camelCase", deny_unknown_fields)]
119pub struct SessionHealthSnapshot {
120 /// Whether the session answered healthy.
121 pub ok: bool,
122 /// Degradation note (an error class when the health call failed).
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub degraded: Option<String>,
125}
126
127/// A localized observation record (spine §6.6; omissions are typed data).
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
129#[serde(rename_all = "camelCase", deny_unknown_fields)]
130pub struct ObservationRecord {
131 /// Provider-scoped observation id.
132 pub observation_id: String,
133 /// Capture timestamp (ms since epoch).
134 pub captured_at_ms: u64,
135 /// The capture-time viewport, verbatim from the provider observation
136 /// (M3a additive close of the registered W1 gap; absent on ledgers
137 /// recorded before the field existed, or when the provider reported
138 /// a non-finite scale factor — the runner refuses to persist a value
139 /// serde_json would write as `null` and never read back).
140 ///
141 /// Compat note: the additive claim is one-directional — readers
142 /// built before this field existed reject ledgers containing it
143 /// (`deny_unknown_fields`, fail-closed per R12); binary downgrade
144 /// against a store written by a newer runner is unsupported.
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub viewport: Option<Viewport>,
147 /// Localized screenshot evidence, when captured.
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub screenshot: Option<EvidenceRef>,
150 /// Why the screenshot was legitimately omitted.
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub screenshot_omission: Option<ScreenshotOmissionReason>,
153 /// Localized UI-tree evidence, when captured.
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub ui_snapshot: Option<EvidenceRef>,
156 /// Why the UI snapshot was legitimately omitted.
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub ui_snapshot_omission: Option<UiSnapshotOmissionReason>,
159}
160
161/// Identity and self-reported basis of the vision judge that completed a
162/// vision-channel evaluation. Evidence honesty for a pluggable judge:
163/// once the model behind the `vision` channel is swappable, a verdict
164/// that does not say *which* model answered — and on *what* facts it says
165/// it based the answer — cannot be audited, compared across deployments,
166/// or meaningfully re-judged offline.
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
168#[serde(rename_all = "camelCase", deny_unknown_fields)]
169pub struct VisionJudgeRecord {
170 /// The verifier implementation family, e.g. `anthropic` /
171 /// `openai-compat`.
172 pub provider: String,
173 /// The model id the verifier requested, when it has one.
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub model: Option<String>,
176 /// The judge's self-reported observations: the on-screen facts it
177 /// listed before answering (the look-then-judge answer protocol).
178 /// Self-reported — checkable against the archived screenshot, not
179 /// independently attested. Absent when the model listed none.
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub observations: Option<Vec<String>>,
182}
183
184/// Outcome of one assertion evaluation along its verify chain (spine §6.6).
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
186#[serde(rename_all = "camelCase", deny_unknown_fields)]
187pub struct AssertionOutcomeRecord {
188 /// The assertion's id.
189 pub assert_id: AssertId,
190 /// Three-valued result (a chain that ran dry is `unknown`, never `fail`).
191 pub result: VerdictStatus,
192 /// The channel that completed the evaluation, when one did.
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub channel: Option<Channel>,
195 /// Human-readable evaluation reason.
196 pub reason: String,
197 /// The vision judge that completed this evaluation, when the
198 /// completing channel was `vision`. Absent on ledgers recorded before
199 /// the field existed and on evaluations no vision model completed.
200 ///
201 /// Compat note: the additive claim is one-directional, as for
202 /// [`ObservationRecord::viewport`] — readers built before this field
203 /// existed reject ledgers containing it (`deny_unknown_fields`,
204 /// fail-closed per R12).
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub vision_judge: Option<VisionJudgeRecord>,
207}
208
209/// The durable record of one completed step (spine §6.6).
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
211#[serde(rename_all = "camelCase", deny_unknown_fields)]
212pub struct StepRecord {
213 /// Full run path of this step instance.
214 pub run_path: RunPath,
215 /// The step's stable id.
216 pub step_id: StepId,
217 /// Effect-domain hash at execution time (alignment input).
218 pub effect_hash: Hash,
219 /// Judge-domain hash at execution time (alignment input).
220 pub judge_hash: Hash,
221 /// Every dispatch attempt, in order.
222 pub attempts: Vec<AttemptRecord>,
223 /// Input expressions evaluated once at `ready` and snapshotted;
224 /// never re-evaluated on resume.
225 pub resolved_inputs: Value,
226 /// Projected step output, when the step declares outputs.
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub output: Option<Value>,
229 /// Localized observations.
230 pub observations: Vec<ObservationRecord>,
231 /// Localized evidence (content-addressed).
232 pub evidence: Vec<EvidenceRef>,
233 /// Assertion outcomes, in declaration order.
234 pub assertion_outcomes: Vec<AssertionOutcomeRecord>,
235 /// The folded verdict projection, absent for unasserted mutations (R4).
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub verdict: Option<StepVerdict>,
238}
239
240/// One live `foreach` iteration inside a call frame (07 §3.2).
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
242#[serde(rename_all = "camelCase", deny_unknown_fields)]
243pub struct IterState {
244 /// The iteration variable name (`foreach ... as`).
245 #[serde(rename = "as")]
246 pub var: String,
247 /// Zero-based index of the current item.
248 pub index: u64,
249 /// Optional stable item key (keyed foreach is reserved, v0.2).
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub key: Option<String>,
252}
253
254/// One frame of the live call stack (07 §3.2 refinement; `frames[0]` is the
255/// root flow frame). Pending spine incorporation of the refined fields.
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
257#[serde(rename_all = "camelCase", deny_unknown_fields)]
258pub struct CallFrame {
259 /// The flow executing in this frame.
260 pub flow_id: FlowId,
261 /// Content hash of that flow's IR.
262 pub ir_hash: Hash,
263 /// The host call step's id; absent for the root frame and for
264 /// handler-repair launched frames (spine §9).
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub call_step_id: Option<StepId>,
267 /// Call-by-value input snapshot taken when the frame was pushed;
268 /// never updated from a newer IR (07 §5.2).
269 pub inputs_snapshot: Value,
270 /// `let` bindings materialized in this frame (SSA, single assignment).
271 pub vars: BTreeMap<String, Value>,
272 /// Live `foreach` iterations, innermost last.
273 pub iter_stack: Vec<IterState>,
274 /// Index of the next body step to schedule in this frame.
275 pub next_index: u64,
276}
277
278/// The WAL'd intent of a dispatched-but-unsettled action (spine §6.6).
279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
280#[serde(rename_all = "camelCase", deny_unknown_fields)]
281pub struct PendingIntent {
282 /// The caller-generated action id to reconcile against.
283 pub call_id: String,
284 /// The evaluated arguments as dispatched.
285 pub args_snapshot: Value,
286}
287
288/// The execution frontier: the single in-flight step (spine §6.6).
289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
290#[serde(rename_all = "camelCase", deny_unknown_fields)]
291pub struct Frontier {
292 /// Run path of the in-flight step.
293 pub run_path: RunPath,
294 /// Its lifecycle state at materialization time.
295 pub state: StepState,
296 /// The hanging action intent, when crash-vulnerable work was in flight.
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub pending_intent: Option<PendingIntent>,
299}
300
301/// A pending human interaction (spine §6.6 + R13 purpose discriminator;
302/// 06 §8-Q4 adopted — `mode`/`deadlineAtMs` are carried so lazy timeout
303/// settlement needs no event-stream re-read).
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
305#[serde(rename_all = "camelCase", deny_unknown_fields)]
306pub struct HumanPending {
307 /// Run path of the awaiting step (or gated step for supervision).
308 pub run_path: RunPath,
309 /// The request id the response must pair with.
310 pub request_id: String,
311 /// Why the interaction exists (step vs supervision gate).
312 pub purpose: HumanPurpose,
313 /// Interaction mode of a `purpose="step"` request; absent for
314 /// supervision gates (06 §2.1).
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub mode: Option<HumanMode>,
317 /// The prompt shown to the human (auto-generated gate description for
318 /// supervision requests).
319 pub prompt: String,
320 /// Absolute response deadline (ms since epoch); absent for supervision
321 /// requests, which have no deadline (spine §6.9).
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub deadline_at_ms: Option<u64>,
324}
325
326/// Event-log cursor of the bound provider session (spine §6.6; advanced
327/// ack-after-persist, never across session generations).
328#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
329#[serde(rename_all = "camelCase", deny_unknown_fields)]
330pub struct EventCursor {
331 /// The provider session that issued the recorded events.
332 pub session_id: String,
333 /// Last sequence durably consumed from that session.
334 pub last_sequence: u64,
335}
336
337/// The run's provider binding state (spine §6.6).
338#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
339#[serde(rename_all = "camelCase", deny_unknown_fields)]
340pub struct BindingState {
341 /// The bound device.
342 pub device_id: String,
343 /// Every provider session generation of this run, oldest first.
344 pub session_lineage: Vec<String>,
345 /// Cursor into the issuing session's event log.
346 pub event_cursor: EventCursor,
347}
348
349/// The deterministic materialized view of a RunLog at a safe point
350/// (spine §6.6); always reconstructible by folding the log.
351#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
352#[serde(rename_all = "camelCase", deny_unknown_fields)]
353pub struct CheckpointView {
354 /// The run this view materializes.
355 pub run_id: String,
356 /// Content hash of the executing IR.
357 pub ir_hash: Hash,
358 /// Digest of the capability lockfile the IR was bound against.
359 pub lockfile_digest: Hash,
360 /// The run's input parameters, snapshotted at start.
361 pub params_snapshot: Value,
362 /// Provider binding state.
363 pub binding: BindingState,
364 /// Records of all completed steps, in completion order.
365 pub completed: Vec<StepRecord>,
366 /// The live call stack; `frames[0]` is the root flow frame.
367 pub frames: Vec<CallFrame>,
368 /// The single in-flight step.
369 pub frontier: Frontier,
370 /// The pending human interaction, when suspended on one.
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub human_pending: Option<HumanPending>,
373}
374
375/// One aligned step in a resume alignment report (spine §6.7-A).
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
377#[serde(rename_all = "camelCase", deny_unknown_fields)]
378pub struct AlignmentEntry {
379 /// Which instance was classified (07 §5.2: the report entry is
380 /// path-addressed).
381 ///
382 /// A step id alone is ambiguous the moment a run nests: the same id
383 /// recurs once per `foreach` round and once per call of a callee, so
384 /// two entries could name the same step and mean different instances.
385 /// The path says which one — and it is the same address
386 /// `requiresConfirmation` already uses, so the two halves of the
387 /// report finally speak one vocabulary.
388 pub run_path: RunPath,
389 /// The step being classified. Kept alongside the path: it is what a
390 /// reader recognizes, and what `--allow-mutating-reexec` names.
391 pub step_id: StepId,
392 /// Its alignment class.
393 pub class: AlignmentClass,
394 /// Sub-domain annotation, e.g. `preflightChanged` (02 §12.3 ruling 6).
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub reason: Option<String>,
397}
398
399/// A step whose re-execution needs explicit human authorization
400/// (07 §5.2/§5.4 unified gate).
401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
402#[serde(rename_all = "camelCase", deny_unknown_fields)]
403pub struct RequiresConfirmation {
404 /// Run path of the gated step.
405 pub run_path: RunPath,
406 /// The step id `--allow-mutating-reexec` takes for this gate (07 §5.4
407 /// step 2: authorization names steps one by one, never a wildcard).
408 ///
409 /// Without it a reader has to re-derive the id from the path — which is
410 /// exactly what the CLI did, and what every other consumer would have
411 /// had to reimplement. Additive optional field (spine §6.1 R12): absent
412 /// on pre-incorporation ledgers, where consumers fall back to the path.
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub step_id: Option<StepId>,
415 /// Gate cause. Known values: `mutatingReexec`, `positionalReplay`,
416 /// `orderInvalidated`, `frontierUnknown` (07 §5.4); closed-set
417 /// incorporation pending.
418 pub cause: String,
419 /// Human-readable explanation.
420 pub reason: String,
421}
422
423/// The alignment report a resume records in `runResumed` (spine §6.7-A).
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
425#[serde(rename_all = "camelCase", deny_unknown_fields)]
426pub struct AlignmentReport {
427 /// Per-step classifications, in new-IR order.
428 pub entries: Vec<AlignmentEntry>,
429 /// The computed resume point, absent when the run cannot resume.
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub resume_point: Option<RunPath>,
432 /// Steps requiring explicit re-execution authorization.
433 pub requires_confirmation: Vec<RequiresConfirmation>,
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use serde_json::json;
440
441 #[test]
442 fn iter_state_uses_the_as_keyword_on_the_wire() {
443 let iter = IterState {
444 var: "item".to_owned(),
445 index: 2,
446 key: None,
447 };
448 let wire = serde_json::to_value(&iter).expect("serialize");
449 assert_eq!(wire, json!({"as": "item", "index": 2}));
450 }
451
452 #[test]
453 fn checkpoint_view_round_trips_with_absent_optionals() {
454 let view = CheckpointView {
455 run_id: "run-1".to_owned(),
456 ir_hash: serde_json::from_value(json!(format!("sha256:{}", "a".repeat(64))))
457 .expect("hash"),
458 lockfile_digest: serde_json::from_value(json!(format!("sha256:{}", "b".repeat(64))))
459 .expect("hash"),
460 params_snapshot: json!({"user": "alice"}),
461 binding: BindingState {
462 device_id: "dev-1".to_owned(),
463 session_lineage: vec!["s-1".to_owned()],
464 event_cursor: EventCursor {
465 session_id: "s-1".to_owned(),
466 last_sequence: 42,
467 },
468 },
469 completed: vec![],
470 frames: vec![CallFrame {
471 flow_id: serde_json::from_value(json!("checkout")).expect("flow id"),
472 ir_hash: serde_json::from_value(json!(format!("sha256:{}", "a".repeat(64))))
473 .expect("hash"),
474 call_step_id: None,
475 inputs_snapshot: json!({}),
476 vars: BTreeMap::new(),
477 iter_stack: vec![],
478 next_index: 3,
479 }],
480 frontier: Frontier {
481 run_path: vec![],
482 state: StepState::Acting,
483 pending_intent: Some(PendingIntent {
484 call_id: "c-1".to_owned(),
485 args_snapshot: json!({"x": 1}),
486 }),
487 },
488 human_pending: None,
489 };
490 let wire = serde_json::to_value(&view).expect("serialize");
491 assert_eq!(wire["frontier"]["state"], "acting");
492 assert_eq!(wire["binding"]["eventCursor"]["lastSequence"], 42);
493 assert!(wire.get("humanPending").is_none());
494 assert!(wire["frames"][0].get("callStepId").is_none());
495 let back: CheckpointView = serde_json::from_value(wire).expect("deserialize");
496 assert_eq!(back, view);
497 }
498
499 #[test]
500 fn human_pending_purpose_discriminates_supervision() {
501 // Supervision requests carry no mode and no deadline (spine §6.9):
502 // both optionals are absent on the wire, never null.
503 let pending = HumanPending {
504 run_path: vec![],
505 request_id: "req-1".to_owned(),
506 purpose: HumanPurpose::Supervision,
507 mode: None,
508 prompt: "Approve mutating dispatch of tapPay".to_owned(),
509 deadline_at_ms: None,
510 };
511 let wire = serde_json::to_value(&pending).expect("serialize");
512 assert_eq!(wire["purpose"], "supervision");
513 let object = wire.as_object().expect("object");
514 assert!(!object.contains_key("mode"));
515 assert!(!object.contains_key("deadlineAtMs"));
516 let back: HumanPending = serde_json::from_value(wire).expect("deserialize");
517 assert_eq!(back, pending);
518 }
519
520 #[test]
521 fn human_pending_step_purpose_carries_mode_and_deadline() {
522 // 06 §8-Q4 adopted: a step-purpose pending request carries its
523 // mode and absolute deadline for lazy settlement.
524 let pending = HumanPending {
525 run_path: vec![],
526 request_id: "req-2".to_owned(),
527 purpose: HumanPurpose::Step,
528 mode: Some(crate::vocab::HumanMode::Confirm),
529 prompt: "Confirm the transfer".to_owned(),
530 deadline_at_ms: Some(1_700_000_600_000),
531 };
532 let wire = serde_json::to_value(&pending).expect("serialize");
533 assert_eq!(wire["mode"], "confirm");
534 assert_eq!(
535 wire["deadlineAtMs"],
536 serde_json::json!(1_700_000_600_000_u64)
537 );
538 let back: HumanPending = serde_json::from_value(wire).expect("deserialize");
539 assert_eq!(back, pending);
540 }
541}