1use vta_sdk::client::VtaClient;
18use vta_sdk::error::VtaError;
19use vta_sdk::protocol::services::{
20 DisableRestRequest, DisableTspRequest, EnableRestRequest, EnableTspRequest,
21 RollbackDidcommRequest, RollbackRestRequest, RollbackTspRequest, UpdateRestRequest,
22 UpdateTspRequest,
23};
24use vta_sdk::protocol::{DisableDidcommRequest, EnableDidcommRequest, UpdateDidcommRequest};
25
26use crate::display::{NAME_HEADER, NameBook, UNNAMED, book_from_acl, inline, shorten_did};
27
28const DIDCOMM_RESTART_WAIT_SECS: u64 = 300;
29
30pub async fn cmd_services_list(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
34 let response = client.list_services().await?;
35
36 if crate::render::is_json_output() {
37 crate::render::print_json(&response)?;
38 return Ok(());
39 }
40
41 println!("Services advertised on this VTA's DID document:");
42 println!();
43 for state in &response.services {
44 match state {
45 vta_sdk::protocol::services::ServiceState::Tsp {
46 enabled,
47 mediator_did,
48 } => {
49 let on = if *enabled { "on" } else { "off" };
50 println!(" TSP: {on}");
51 if let Some(m) = mediator_did {
52 println!(" Mediator: {m}");
53 }
54 }
55 vta_sdk::protocol::services::ServiceState::Didcomm {
56 enabled,
57 mediator_did,
58 routing_keys,
59 } => {
60 let on = if *enabled { "on" } else { "off" };
61 println!(" DIDComm: {on}");
62 if let Some(m) = mediator_did {
63 println!(" Mediator: {m}");
64 }
65 if !routing_keys.is_empty() {
66 println!(" Routing keys: {}", routing_keys.join(", "));
67 }
68 }
69 vta_sdk::protocol::services::ServiceState::Rest { enabled, url } => {
70 let on = if *enabled { "on" } else { "off" };
71 println!(" REST: {on}");
72 if let Some(u) = url {
73 println!(" URL: {u}");
74 }
75 }
76 vta_sdk::protocol::services::ServiceState::Webauthn { enabled, url } => {
77 let on = if *enabled { "on" } else { "off" };
78 println!(" WebAuthn: {on}");
79 if let Some(u) = url {
80 println!(" URL: {u}");
81 }
82 }
83 }
84 }
85 Ok(())
86}
87
88pub async fn cmd_services_rest_enable(
91 client: &VtaClient,
92 url: String,
93) -> Result<(), Box<dyn std::error::Error>> {
94 let req = EnableRestRequest::new(url);
95 let resp = client.enable_rest(req).await?;
96 println!("REST enabled.");
97 println!(" New version ID: {}", resp.log_entry_version_id);
98 println!(" Effective at: {}", resp.effective_at);
99 print_serverless_hint(resp.serverless, &resp.vta_did);
100 Ok(())
101}
102
103pub async fn cmd_services_rest_update(
104 client: &VtaClient,
105 url: String,
106) -> Result<(), Box<dyn std::error::Error>> {
107 let req = UpdateRestRequest::new(url);
108 let resp = client.update_rest(req).await?;
109 println!("REST URL updated.");
110 println!(" New version ID: {}", resp.log_entry_version_id);
111 println!(" Effective at: {}", resp.effective_at);
112 print_serverless_hint(resp.serverless, &resp.vta_did);
113 Ok(())
114}
115
116pub async fn cmd_services_rest_disable(
117 client: &VtaClient,
118) -> Result<(), Box<dyn std::error::Error>> {
119 let resp = client.disable_rest(DisableRestRequest::default()).await?;
120 println!("REST disabled.");
121 println!(" New version ID: {}", resp.log_entry_version_id);
122 println!(" Effective at: {}", resp.effective_at);
123 print_serverless_hint(resp.serverless, &resp.vta_did);
124 Ok(())
125}
126
127pub async fn cmd_services_rest_rollback(
128 client: &VtaClient,
129) -> Result<(), Box<dyn std::error::Error>> {
130 let resp = client.rollback_rest(RollbackRestRequest::default()).await?;
131 print_rollback_result("REST", &resp);
132 Ok(())
133}
134
135pub async fn cmd_services_tsp_enable(
143 client: &VtaClient,
144 mediator_did: String,
145) -> Result<(), Box<dyn std::error::Error>> {
146 let req = EnableTspRequest::new(mediator_did);
147 let resp = client.enable_tsp(req).await?;
148 println!("TSP enabled.");
149 println!(" New version ID: {}", resp.log_entry_version_id);
150 println!(" Effective at: {}", resp.effective_at);
151 print_serverless_hint(resp.serverless, &resp.vta_did);
152 Ok(())
153}
154
155pub async fn cmd_services_tsp_update(
156 client: &VtaClient,
157 mediator_did: String,
158) -> Result<(), Box<dyn std::error::Error>> {
159 let req = UpdateTspRequest::new(mediator_did);
160 let resp = client.update_tsp(req).await?;
161 println!("TSP mediator DID updated.");
162 println!(" New version ID: {}", resp.log_entry_version_id);
163 println!(" Effective at: {}", resp.effective_at);
164 print_serverless_hint(resp.serverless, &resp.vta_did);
165 Ok(())
166}
167
168pub async fn cmd_services_tsp_disable(
169 client: &VtaClient,
170) -> Result<(), Box<dyn std::error::Error>> {
171 let resp = client.disable_tsp(DisableTspRequest::default()).await?;
172 println!("TSP disabled.");
173 println!(" New version ID: {}", resp.log_entry_version_id);
174 println!(" Effective at: {}", resp.effective_at);
175 print_serverless_hint(resp.serverless, &resp.vta_did);
176 Ok(())
177}
178
179pub async fn cmd_services_tsp_rollback(
180 client: &VtaClient,
181) -> Result<(), Box<dyn std::error::Error>> {
182 let resp = client.rollback_tsp(RollbackTspRequest::default()).await?;
183 print_rollback_result("TSP", &resp);
184 Ok(())
185}
186
187pub async fn cmd_services_webauthn_enable(
190 client: &VtaClient,
191 url: String,
192) -> Result<(), Box<dyn std::error::Error>> {
193 let req = vta_sdk::protocol::services::EnableWebauthnRequest::new(url);
194 let resp = client.enable_webauthn(req).await?;
195 println!("WebAuthn enabled.");
196 println!(" New version ID: {}", resp.log_entry_version_id);
197 println!(" Effective at: {}", resp.effective_at);
198 print_serverless_hint(resp.serverless, &resp.vta_did);
199 Ok(())
200}
201
202pub async fn cmd_services_webauthn_update(
203 client: &VtaClient,
204 url: String,
205) -> Result<(), Box<dyn std::error::Error>> {
206 let req = vta_sdk::protocol::services::UpdateWebauthnRequest::new(url);
207 let resp = client.update_webauthn(req).await?;
208 println!("WebAuthn URL updated.");
209 println!(" New version ID: {}", resp.log_entry_version_id);
210 println!(" Effective at: {}", resp.effective_at);
211 print_serverless_hint(resp.serverless, &resp.vta_did);
212 Ok(())
213}
214
215pub async fn cmd_services_webauthn_disable(
216 client: &VtaClient,
217) -> Result<(), Box<dyn std::error::Error>> {
218 eprintln!(
219 "WARNING: disabling WebAuthn will also REMOVE passkey verificationMethods from every DID \
220 this VTA controls. Any operator currently using passkey login will need to re-enrol \
221 after the next `services webauthn enable`."
222 );
223 let resp = client
224 .disable_webauthn(vta_sdk::protocol::services::DisableWebauthnRequest::default())
225 .await?;
226 println!("WebAuthn disabled.");
227 println!(" New version ID: {}", resp.log_entry_version_id);
228 println!(" Effective at: {}", resp.effective_at);
229 print_serverless_hint(resp.serverless, &resp.vta_did);
230 Ok(())
231}
232
233pub async fn cmd_services_webauthn_rollback(
234 client: &VtaClient,
235) -> Result<(), Box<dyn std::error::Error>> {
236 let resp = client
237 .rollback_webauthn(vta_sdk::protocol::services::RollbackWebauthnRequest::default())
238 .await?;
239 print_rollback_result("WebAuthn", &resp);
240 Ok(())
241}
242
243pub async fn cmd_services_didcomm_enable(
246 client: &VtaClient,
247 mediator_did: String,
248 setup_acl: bool,
249 force: bool,
250 handshake_timeout_secs: Option<u64>,
251 wait_secs: Option<u64>,
252 no_wait: bool,
253) -> Result<(), Box<dyn std::error::Error>> {
254 let mut req = EnableDidcommRequest::new(&mediator_did);
255 req.setup_acl = setup_acl;
256 req.force = force;
257 req.handshake_timeout_secs = handshake_timeout_secs;
258 let resp = match client.enable_didcomm(req).await {
259 Ok(resp) => resp,
260 Err(VtaError::Conflict(body)) if body.contains("DIDComm is already enabled") => {
264 println!("DIDComm already enabled.");
265 println!(" To change the mediator: pnm services didcomm update --mediator-did <did>");
266 return Ok(());
267 }
268 Err(e) => return Err(e.into()),
269 };
270 println!("DIDComm enabled.");
271 println!(" Mediator DID: {}", resp.mediator_did);
272 if !resp.mediator_endpoint.is_empty() {
273 println!(" Mediator URL: {}", resp.mediator_endpoint);
274 }
275 println!(" New version ID: {}", resp.new_version_id);
276 if force {
277 println!();
278 println!(" Note: --force was set; mediator handshake steps 2-5 were bypassed.");
279 }
280 print_serverless_hint(resp.serverless, &resp.vta_did);
281 if resp.restarting {
282 if no_wait {
283 println!(" The VTA is restarting; readiness polling was disabled.");
284 } else {
285 wait_for_didcomm_restart(
286 client,
287 &mediator_did,
288 wait_secs.unwrap_or(DIDCOMM_RESTART_WAIT_SECS),
289 )
290 .await?;
291 }
292 }
293 Ok(())
294}
295
296async fn wait_for_didcomm_restart(
297 client: &VtaClient,
298 mediator_did: &str,
299 wait_secs: u64,
300) -> Result<(), Box<dyn std::error::Error>> {
301 use std::time::Duration;
302
303 let session = std::sync::Arc::new(tokio::sync::Mutex::new(None));
311 let probe_session = std::sync::Arc::clone(&session);
312
313 let result = wait_for_didcomm_restart_with_probe(
314 move |probe_window| {
315 let session = std::sync::Arc::clone(&probe_session);
316 async move {
317 let probe = async {
318 let mut session = session.lock().await;
319 if session.is_none() {
320 *session = Some(client.open_vta_ping_session(mediator_did, "pnm").await?);
321 }
322 let (session, vta_did) = session.as_ref().expect("opened above");
323 session.ping(Some(vta_did)).await
324 };
325 tokio::time::timeout(probe_window, probe)
326 .await
327 .unwrap_or_else(|_| Err("trust-ping timed out".into()))
328 }
329 },
330 Duration::from_secs(wait_secs),
331 Duration::from_secs(10),
332 Duration::from_secs(2),
333 )
334 .await;
335
336 if let Some((session, _)) = session.lock().await.take() {
337 session.shutdown().await;
338 }
339 result
340}
341
342async fn wait_for_didcomm_restart_with_probe<Probe, ProbeFuture>(
343 mut probe: Probe,
344 timeout: std::time::Duration,
345 probe_window: std::time::Duration,
346 retry_delay: std::time::Duration,
347) -> Result<(), Box<dyn std::error::Error>>
348where
349 Probe: FnMut(std::time::Duration) -> ProbeFuture,
350 ProbeFuture: std::future::Future<Output = Result<u128, Box<dyn std::error::Error>>>,
351{
352 use std::time::Instant;
353
354 let deadline = Instant::now() + timeout;
355 println!(
356 " Waiting for the restarted DIDComm listener (up to {}s)...",
357 timeout.as_secs()
358 );
359
360 loop {
361 let remaining = deadline.saturating_duration_since(Instant::now());
362 if remaining.is_zero() {
363 break;
364 }
365 if let Ok(latency) = probe(probe_window.min(remaining)).await {
366 println!(" Trust-ping: pong ({latency}ms)");
367 return Ok(());
368 }
369
370 let remaining = deadline.saturating_duration_since(Instant::now());
371 if remaining.is_zero() {
372 break;
373 }
374 tokio::time::sleep(retry_delay.min(remaining)).await;
375 }
376
377 Err(format!(
378 "DIDComm is enabled, but its listener did not answer within {}s; the VTA's mediator self-DID readiness gate may still be waiting for the VTA DID to resolve, or the mediator may be dropping the pong because this client's own account refuses forwarded delivery",
379 timeout.as_secs()
380 )
381 .into())
382}
383
384pub async fn cmd_services_didcomm_update(
385 client: &VtaClient,
386 new_mediator_did: String,
387 drain_ttl_secs: u64,
388 setup_acl: bool,
389 force: bool,
390 handshake_timeout_secs: Option<u64>,
391) -> Result<(), Box<dyn std::error::Error>> {
392 let mut req = UpdateDidcommRequest::new(&new_mediator_did, drain_ttl_secs);
393 req.setup_acl = setup_acl;
394 req.force = force;
395 req.handshake_timeout_secs = handshake_timeout_secs;
396 let resp = client.update_didcomm(req).await?;
397 println!("DIDComm mediator updated.");
398 println!(" Prior mediator: {}", resp.prior_mediator_did);
399 println!(" Active mediator: {}", resp.active_mediator_did);
400 if !resp.active_mediator_endpoint.is_empty() {
401 println!(" Active endpoint: {}", resp.active_mediator_endpoint);
402 }
403 println!(" New version ID: {}", resp.new_version_id);
404 println!(
405 " Drain deadline: {} (prior listener stays up until then)",
406 resp.drains_until
407 );
408 print_serverless_hint(resp.serverless, &resp.vta_did);
409 Ok(())
410}
411
412pub async fn cmd_services_didcomm_disable(
413 client: &VtaClient,
414 drain_ttl_secs: u64,
415) -> Result<(), Box<dyn std::error::Error>> {
416 let req = DisableDidcommRequest::new(drain_ttl_secs);
417 let resp = client.disable_didcomm(req).await?;
418 println!("DIDComm disabled.");
419 println!(" Prior mediator: {}", resp.prior_mediator_did);
420 println!(" New version ID: {}", resp.new_version_id);
421 match resp.drains_until {
422 Some(deadline) => {
423 println!(" Drain deadline: {deadline}");
424 println!();
425 println!(" The listener stays up until the deadline so in-flight messages can drain.");
426 println!(
427 " Cancel early with `pnm services didcomm drain cancel --mediator-did <did>`."
428 );
429 }
430 None => println!(" Listener torn down immediately (drain TTL was 0)."),
431 }
432 print_serverless_hint(resp.serverless, &resp.vta_did);
433 Ok(())
434}
435
436pub async fn cmd_services_didcomm_rollback(
437 client: &VtaClient,
438 drain_ttl_secs: Option<u64>,
439) -> Result<(), Box<dyn std::error::Error>> {
440 let req = RollbackDidcommRequest { drain_ttl_secs };
441 let resp = client.rollback_didcomm(req).await?;
442 print_rollback_result("DIDComm", &resp);
443 Ok(())
444}
445
446async fn mediator_name_book(client: &VtaClient) -> NameBook {
456 let mut book = NameBook::new();
457 if let Ok(acl) = client.list_acl(None).await {
458 book_from_acl(&mut book, &acl.entries);
459 }
460 book
461}
462
463pub async fn cmd_services_didcomm_drain_list(
466 client: &VtaClient,
467) -> Result<(), Box<dyn std::error::Error>> {
468 let resp = client.list_drain().await?;
469 if resp.entries.is_empty() {
470 println!("No mediators currently in drain.");
471 return Ok(());
472 }
473 println!("Drain set ({} mediator(s)):", resp.entries.len());
474 println!();
475
476 let book = mediator_name_book(client).await;
477 let show_names = book.names_any(resp.entries.iter().map(|e| e.mediator_did.as_str()));
478
479 let header_did = "MEDIATOR DID";
480 let header_until = "DRAIN UNTIL";
481 if show_names {
482 println!(" {:<24} {header_did:<46} {header_until}", NAME_HEADER);
483 } else {
484 println!(" {header_did:<46} {header_until}");
485 }
486 for e in &resp.entries {
487 let did = shorten_did(&e.mediator_did);
488 if show_names {
489 let name = book
490 .name_of(&e.mediator_did)
491 .unwrap_or_else(|| UNNAMED.into());
492 println!(" {:<24} {:<46} {}", name, did, e.drains_until);
493 } else {
494 println!(" {:<46} {}", did, e.drains_until);
495 }
496 }
497 Ok(())
498}
499
500pub async fn cmd_services_didcomm_drain_cancel(
501 client: &VtaClient,
502 mediator_did: String,
503) -> Result<(), Box<dyn std::error::Error>> {
504 let req = vta_sdk::protocol::DrainCancelRequest { mediator_did };
505 let resp = client.drain_cancel(req).await?;
506 println!("Drain cancelled for {}.", resp.mediator_did);
507 println!(" Listener was torn down immediately.");
508 Ok(())
509}
510
511pub async fn cmd_services_report(
514 client: &VtaClient,
515 since: Option<String>,
516 until: Option<String>,
517 format: ReportFormat,
518) -> Result<(), Box<dyn std::error::Error>> {
519 let report = client
520 .mediator_report(since.as_deref(), until.as_deref())
521 .await?;
522
523 match format {
524 ReportFormat::Json => {
525 println!("{}", serde_json::to_string_pretty(&report)?);
526 }
527 ReportFormat::Table => {
528 println!("Service-management report");
529 if let Some(ref s) = report.since {
530 println!(" Window: {s} → {}", report.until);
531 } else {
532 println!(" Window: (all time) → {}", report.until);
533 }
534 println!();
535 let book = mediator_name_book(client).await;
539
540 if report.mediators.is_empty() {
541 println!(" No inbound DIDComm messages recorded.");
542 } else {
543 println!(" Per-mediator inbound counts (most recent first):");
544 let show_names =
545 book.names_any(report.mediators.iter().map(|m| m.mediator_did.as_str()));
546 let header_did = "MEDIATOR DID";
547 let header_count = "INBOUND";
548 if show_names {
549 println!(
550 " {:<24} {header_did:<46} {header_count:>10} LAST SEEN",
551 NAME_HEADER
552 );
553 } else {
554 println!(" {header_did:<46} {header_count:>10} LAST SEEN");
555 }
556 for m in &report.mediators {
557 let did = shorten_did(&m.mediator_did);
558 if show_names {
559 let name = book
560 .name_of(&m.mediator_did)
561 .unwrap_or_else(|| UNNAMED.into());
562 println!(
563 " {:<24} {:<46} {:>10} {}",
564 name, did, m.inbound_count, m.last_seen
565 );
566 } else {
567 println!(" {:<46} {:>10} {}", did, m.inbound_count, m.last_seen);
568 }
569 }
570 }
571 if !report.senders.is_empty() {
572 println!();
573 println!(" Senders by last-seen mediator:");
574 for s in &report.senders {
575 println!(
578 " {} → {} (at {})",
579 inline(&book, &s.sender_did),
580 inline(&book, &s.last_seen_mediator),
581 s.last_seen_at
582 );
583 }
584 }
585 }
586 }
587 Ok(())
588}
589
590fn print_rollback_result(kind: &str, resp: &vta_sdk::protocol::services::RollbackResponse) {
593 if resp.kind == "no_op" {
594 println!("{kind} rollback: no change required.");
595 println!(" Snapshot matches current state — nothing to do.");
596 return;
597 }
598 println!("{kind} rolled back.");
599 println!(" Action: {}", resp.kind);
600 if !resp.log_entry_version_id.is_empty() {
601 println!(" New version ID: {}", resp.log_entry_version_id);
602 }
603 println!(" Effective at: {}", resp.effective_at);
604 if let Some(ref drain_until) = resp.drain_until {
605 println!(" Drain deadline: {drain_until}");
606 }
607 if let Some(ref draining) = resp.draining_mediator {
608 println!(" Draining: {draining}");
609 }
610 print_serverless_hint(resp.serverless, &resp.vta_did);
611}
612
613pub fn print_serverless_hint(serverless: bool, vta_did: &str) {
627 if !serverless || vta_did.is_empty() {
628 return;
629 }
630 print_self_hosted_notice(vta_did, "now serves");
631}
632
633pub fn print_serverless_hint_offline(serverless: bool, vta_did: &str) {
636 if !serverless || vta_did.is_empty() {
637 return;
638 }
639 print_self_hosted_notice(vta_did, "will serve, once it is running again,");
640}
641
642fn print_self_hosted_notice(vta_did: &str, serves: &str) {
643 println!();
644 match webvh_log_url(vta_did) {
645 Some(url) => {
646 println!(" This VTA hosts its own DID, and {serves} the updated log at");
647 println!(" {url}");
648 }
649 None => println!(" This VTA hosts its own DID, and {serves} the updated log itself."),
650 }
651 println!(" Nothing to redeploy. Resolvers pick up the new version as their cache");
652 println!(" expires (60 s for this VTA's own responses, up to 5 min in a caching resolver).");
653 println!(" Only a copy you also publish elsewhere needs replacing:");
654 println!(" pnm did-mgmt dids get-log {vta_did} --out did.jsonl");
655}
656
657pub fn print_redeploy_hint(serverless: bool, did: &str) {
662 if !serverless || did.is_empty() {
663 return;
664 }
665 println!();
666 println!(" This DID is self-hosted, and not by this VTA. Fetch the updated log:");
667 println!(" pnm did-mgmt dids get-log {did} --out did.jsonl");
668 match webvh_log_url(did) {
669 Some(url) => println!(" then install it where {url} is served from."),
670 None => println!(" then install it where the DID's log is served from."),
671 }
672 println!(" For a community that self-hosts its DID, that is:");
673 println!(" cnm did-log install --file did.jsonl");
674 println!(" Until you do, resolvers will keep returning the prior version.");
675}
676
677fn webvh_log_url(did: &str) -> Option<String> {
683 let log_did = vta_sdk::did_scid::webvh_did(did);
684 let did = log_did.as_deref().unwrap_or(did);
685 let rest = did.strip_prefix("did:webvh:")?;
686 let mut parts = rest.split(':');
687 let _scid = parts.next().filter(|s| !s.is_empty())?;
688 let host = parts.next().filter(|s| !s.is_empty())?;
689 let host = host.replace("%3A", ":").replace("%3a", ":");
690 let path: Vec<&str> = parts.collect();
691 if path.iter().any(|p| p.is_empty()) {
692 return None;
693 }
694 Some(if path.is_empty() {
695 format!("https://{host}/.well-known/did.jsonl")
696 } else {
697 format!("https://{host}/{}/did.jsonl", path.join("/"))
698 })
699}
700
701#[derive(Debug, Clone, Copy)]
702pub enum ReportFormat {
703 Json,
704 Table,
705}
706
707impl std::str::FromStr for ReportFormat {
708 type Err = String;
709 fn from_str(s: &str) -> Result<Self, Self::Err> {
710 match s {
711 "json" => Ok(Self::Json),
712 "table" => Ok(Self::Table),
713 other => Err(format!("unknown format `{other}` — use `json` or `table`")),
714 }
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 #[test]
722 fn webvh_log_url_follows_the_did_to_https_transform() {
723 use super::webvh_log_url;
724 assert_eq!(
725 webvh_log_url("did:webvh:QmScid:vta.example.com").as_deref(),
726 Some("https://vta.example.com/.well-known/did.jsonl")
727 );
728 assert_eq!(
729 webvh_log_url("did:webvh:QmScid:example.com%3A8100:dids:vta").as_deref(),
730 Some("https://example.com:8100/dids/vta/did.jsonl")
731 );
732 assert_eq!(webvh_log_url("did:key:z6Mk"), None);
733 assert_eq!(webvh_log_url("did:webvh:QmScid"), None);
734 }
735
736 #[tokio::test]
737 async fn readiness_poll_retries_until_a_pong_arrives() {
738 use std::sync::{Arc, Mutex};
739 use std::time::Duration;
740
741 let attempts = Arc::new(Mutex::new(0u32));
742 let attempts_for_probe = Arc::clone(&attempts);
743 let result = super::wait_for_didcomm_restart_with_probe(
744 move |_| {
745 let attempt = {
746 let mut attempts = attempts_for_probe.lock().unwrap();
747 *attempts += 1;
748 *attempts
749 };
750 async move {
751 if attempt < 3 {
752 Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "not ready").into())
753 } else {
754 Ok(12)
755 }
756 }
757 },
758 Duration::from_millis(100),
759 Duration::from_millis(1),
760 Duration::from_millis(1),
761 )
762 .await;
763
764 assert!(result.is_ok());
765 assert_eq!(*attempts.lock().unwrap(), 3);
766 }
767
768 #[tokio::test]
769 async fn readiness_poll_names_the_gate_when_deadline_expires() {
770 use std::time::Duration;
771
772 let result = super::wait_for_didcomm_restart_with_probe(
773 |_| async {
774 Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "not ready").into())
775 },
776 Duration::from_millis(5),
777 Duration::from_millis(1),
778 Duration::from_millis(1),
779 )
780 .await;
781
782 let error = result.expect_err("poll should time out").to_string();
783 assert!(error.contains("self-DID readiness gate"), "{error}");
784 assert!(error.contains("forwarded delivery"), "{error}");
786 }
787
788 #[tokio::test]
791 async fn readiness_poll_stops_after_the_first_pong() {
792 use std::sync::{Arc, Mutex};
793 use std::time::{Duration, Instant};
794
795 let attempts = Arc::new(Mutex::new(0u32));
796 let counter = Arc::clone(&attempts);
797 let started = Instant::now();
798 let result = super::wait_for_didcomm_restart_with_probe(
799 move |_| {
800 *counter.lock().unwrap() += 1;
801 async { Ok::<u128, Box<dyn std::error::Error>>(7) }
802 },
803 Duration::from_secs(300),
804 Duration::from_secs(10),
805 Duration::from_secs(10),
806 )
807 .await;
808
809 assert!(result.is_ok());
810 assert_eq!(*attempts.lock().unwrap(), 1);
811 assert!(
812 started.elapsed() < Duration::from_secs(5),
813 "a pong must return immediately, not after the retry delay: {:?}",
814 started.elapsed()
815 );
816 }
817
818 #[tokio::test]
822 async fn readiness_poll_never_hands_a_probe_more_time_than_remains() {
823 use std::sync::{Arc, Mutex};
824 use std::time::Duration;
825
826 let windows = Arc::new(Mutex::new(Vec::new()));
827 let seen = Arc::clone(&windows);
828 let budget = Duration::from_millis(50);
829
830 let result = super::wait_for_didcomm_restart_with_probe(
831 move |window| {
832 seen.lock().unwrap().push(window);
833 async { Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "not ready").into()) }
834 },
835 budget,
836 Duration::from_secs(10),
837 Duration::from_millis(1),
838 )
839 .await;
840
841 assert!(result.is_err());
842 let windows = windows.lock().unwrap();
843 assert!(!windows.is_empty(), "the poll must probe at least once");
844 assert!(
845 windows.iter().all(|w| *w <= budget),
846 "a probe was given more than the {budget:?} budget: {windows:?}"
847 );
848 }
849
850 #[tokio::test]
853 async fn readiness_poll_clamps_its_retry_delay_to_the_deadline() {
854 use std::time::{Duration, Instant};
855
856 let started = Instant::now();
857 let result = super::wait_for_didcomm_restart_with_probe(
858 |_| async {
859 Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "not ready").into())
860 },
861 Duration::from_millis(50),
862 Duration::from_millis(1),
863 Duration::from_secs(10),
864 )
865 .await;
866
867 assert!(result.is_err());
868 assert!(
869 started.elapsed() < Duration::from_secs(5),
870 "the poll slept past its own deadline: {:?}",
871 started.elapsed()
872 );
873 }
874}