1use std::fmt::{Display, Formatter};
4
5use crate::auth::Redactor;
6use crate::{DocumentQuality, DocumentQualityCategory, IndexDocument, IndexNode};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DiagnosticSeverity {
11 Info,
13 Warning,
15 Error,
17}
18
19impl Display for DiagnosticSeverity {
20 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
21 match self {
22 Self::Info => f.write_str("info"),
23 Self::Warning => f.write_str("warning"),
24 Self::Error => f.write_str("error"),
25 }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum TelemetryPolicy {
32 LocalOnly,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum DiagnosticSource {
39 LocalInput,
41 Network,
43 Parser,
45 Readability,
47 GenericTransformer,
49 Adapter,
51 Headless,
53 Extraction,
55 Renderer,
57 Shelf,
59}
60
61impl DiagnosticSource {
62 #[must_use]
64 pub const fn as_str(self) -> &'static str {
65 match self {
66 Self::LocalInput => "local-input",
67 Self::Network => "network",
68 Self::Parser => "parser",
69 Self::Readability => "readability",
70 Self::GenericTransformer => "generic-transformer",
71 Self::Adapter => "adapter",
72 Self::Headless => "headless",
73 Self::Extraction => "extraction",
74 Self::Renderer => "renderer",
75 Self::Shelf => "shelf",
76 }
77 }
78}
79
80impl Display for DiagnosticSource {
81 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82 f.write_str(self.as_str())
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum DiagnosticConfidence {
89 Failed,
91 Low,
93 Medium,
95}
96
97impl DiagnosticConfidence {
98 #[must_use]
100 pub const fn as_str(self) -> &'static str {
101 match self {
102 Self::Failed => "failed",
103 Self::Low => "low",
104 Self::Medium => "medium",
105 }
106 }
107}
108
109impl Display for DiagnosticConfidence {
110 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
111 f.write_str(self.as_str())
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum DiagnosticAction {
118 Retry,
120 TryHeadless,
122 Extract,
124 Capture,
126 Repair,
128 AddFixture,
130 ShelfSearch,
132}
133
134impl DiagnosticAction {
135 #[must_use]
137 pub const fn label(self) -> &'static str {
138 match self {
139 Self::Retry => "retry the request",
140 Self::TryHeadless => "try headless fallback",
141 Self::Extract => "extract links or JSON for inspection",
142 Self::Capture => "capture a local redacted fixture",
143 Self::Repair => "repair the reader view locally",
144 Self::AddFixture => "add or improve a fixture",
145 Self::ShelfSearch => "search the local knowledge shelf",
146 }
147 }
148
149 #[must_use]
151 pub const fn command(self) -> &'static str {
152 match self {
153 Self::Retry => ":open <url>",
154 Self::TryHeadless => "index --headless <url>",
155 Self::Extract => ":extract links",
156 Self::Capture => ":capture preview",
157 Self::Repair => ":repair promote <region-id>",
158 Self::AddFixture => "index capture --validate <artifact-file>",
159 Self::ShelfSearch => "index shelf search <query>",
160 }
161 }
162}
163
164impl Display for DiagnosticAction {
165 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
166 f.write_str(self.label())
167 }
168}
169
170impl TelemetryPolicy {
171 #[must_use]
173 pub const fn allows_network_transmission(self) -> bool {
174 match self {
175 Self::LocalOnly => false,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum FailureCause {
183 NetworkUnavailable,
185 ParseFailed,
187 Timeout,
189 EmptyContent,
191 UnsupportedPageShape,
193 ExtractionFailed,
195 RendererFailed,
197 ShelfUnavailable,
199 LowConfidence,
201 BlockedByPolicy,
203 AdapterMismatch,
205 Unknown,
207}
208
209impl FailureCause {
210 #[must_use]
212 pub const fn as_str(self) -> &'static str {
213 match self {
214 Self::NetworkUnavailable => "network-unavailable",
215 Self::ParseFailed => "parse-failed",
216 Self::Timeout => "timeout",
217 Self::EmptyContent => "empty-content",
218 Self::UnsupportedPageShape => "unsupported-page-shape",
219 Self::ExtractionFailed => "extraction-failed",
220 Self::RendererFailed => "renderer-failed",
221 Self::ShelfUnavailable => "shelf-unavailable",
222 Self::LowConfidence => "low-confidence",
223 Self::BlockedByPolicy => "blocked-by-policy",
224 Self::AdapterMismatch => "adapter-mismatch",
225 Self::Unknown => "unknown",
226 }
227 }
228
229 #[must_use]
231 pub const fn explanation(self) -> &'static str {
232 match self {
233 Self::NetworkUnavailable => "Index could not retrieve the requested page.",
234 Self::ParseFailed => "Index could not parse the supplied content into a safe document.",
235 Self::Timeout => "The operation took longer than the configured budget.",
236 Self::EmptyContent => "The page did not expose readable semantic content.",
237 Self::UnsupportedPageShape => {
238 "The static transformer could not map this page shape confidently."
239 }
240 Self::ExtractionFailed => {
241 "Index could not serialize the document into the requested extraction format."
242 }
243 Self::RendererFailed => {
244 "Index could not lay out the document for the current terminal view."
245 }
246 Self::ShelfUnavailable => {
247 "Index could not read, write, or search the local knowledge shelf."
248 }
249 Self::LowConfidence => "Index produced a partial document and needs review or repair.",
250 Self::BlockedByPolicy => {
251 "A security, origin, sandbox, or URL policy rejected the page."
252 }
253 Self::AdapterMismatch => {
254 "A site-specific adapter did not match confidently, so fallback behavior was used."
255 }
256 Self::Unknown => "Index could not classify the failure precisely.",
257 }
258 }
259
260 #[must_use]
262 pub fn classify(source: DiagnosticSource, reason: &str) -> Self {
263 let reason = reason.to_ascii_lowercase();
264 if reason.contains("timeout") || reason.contains("timed out") {
265 Self::Timeout
266 } else if reason.contains("schema")
267 || reason.contains("json")
268 || reason.contains("markdown")
269 || reason.contains("extract")
270 {
271 Self::ExtractionFailed
272 } else if reason.contains("render")
273 || reason.contains("layout")
274 || reason.contains("terminal")
275 || reason.contains("viewport")
276 {
277 Self::RendererFailed
278 } else if reason.contains("shelf")
279 || reason.contains("saved record")
280 || reason.contains("offline record")
281 {
282 Self::ShelfUnavailable
283 } else if reason.contains("low confidence") || reason.contains("partial document") {
284 Self::LowConfidence
285 } else if reason.contains("parse") || reason.contains("malformed") {
286 Self::ParseFailed
287 } else if reason.contains("denied")
288 || reason.contains("blocked")
289 || reason.contains("unsafe")
290 || reason.contains("policy")
291 {
292 Self::BlockedByPolicy
293 } else if reason.contains("empty")
294 || reason.contains("no readable")
295 || reason.contains("missing readable")
296 || reason.contains("did not contain readable")
297 {
298 Self::EmptyContent
299 } else {
300 match source {
301 DiagnosticSource::Network => Self::NetworkUnavailable,
302 DiagnosticSource::Adapter => Self::AdapterMismatch,
303 DiagnosticSource::Parser => Self::ParseFailed,
304 DiagnosticSource::GenericTransformer | DiagnosticSource::Readability => {
305 Self::UnsupportedPageShape
306 }
307 DiagnosticSource::Headless => Self::Unknown,
308 DiagnosticSource::Extraction => Self::ExtractionFailed,
309 DiagnosticSource::Renderer => Self::RendererFailed,
310 DiagnosticSource::Shelf => Self::ShelfUnavailable,
311 DiagnosticSource::LocalInput => Self::Unknown,
312 }
313 }
314 }
315}
316
317impl Display for FailureCause {
318 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
319 f.write_str(self.as_str())
320 }
321}
322
323#[derive(Debug, Clone, PartialEq, Eq)]
325pub struct DiagnosticField {
326 pub key: String,
328 pub value: String,
330}
331
332impl DiagnosticField {
333 #[must_use]
335 pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
336 Self {
337 key: key.into(),
338 value: value.into(),
339 }
340 }
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct DiagnosticRecord {
346 pub severity: DiagnosticSeverity,
348 pub code: String,
350 pub message: String,
352 pub fields: Vec<DiagnosticField>,
354}
355
356impl DiagnosticRecord {
357 #[must_use]
359 pub fn new(
360 severity: DiagnosticSeverity,
361 code: impl Into<String>,
362 message: impl Into<String>,
363 ) -> Self {
364 Self {
365 severity,
366 code: code.into(),
367 message: message.into(),
368 fields: Vec::new(),
369 }
370 }
371
372 #[must_use]
374 pub fn with_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
375 self.fields.push(DiagnosticField::new(key, value));
376 self
377 }
378
379 #[must_use]
381 pub fn redacted(&self, redactor: &Redactor) -> Self {
382 Self {
383 severity: self.severity,
384 code: self.code.clone(),
385 message: redactor.redact(&self.message),
386 fields: self
387 .fields
388 .iter()
389 .map(|field| DiagnosticField::new(&field.key, redactor.redact(&field.value)))
390 .collect(),
391 }
392 }
393
394 #[must_use]
396 pub fn to_local_text(&self) -> String {
397 let mut lines = vec![format!(
398 "{}[{}]: {}",
399 self.severity, self.code, self.message
400 )];
401 for field in &self.fields {
402 lines.push(format!("{}={}", field.key, field.value));
403 }
404 lines.join("\n")
405 }
406}
407
408#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct FailureDiagnostic {
411 pub title: String,
413 pub source: DiagnosticSource,
415 pub confidence: DiagnosticConfidence,
417 pub reason: String,
419 pub cause: FailureCause,
421 pub fallback: Option<String>,
423 pub tried: Vec<String>,
425 pub actions: Vec<DiagnosticAction>,
427 pub commands: Vec<String>,
429 pub records: Vec<DiagnosticRecord>,
431}
432
433impl FailureDiagnostic {
434 #[must_use]
436 pub fn new(
437 title: impl Into<String>,
438 source: DiagnosticSource,
439 confidence: DiagnosticConfidence,
440 reason: impl Into<String>,
441 ) -> Self {
442 let reason = reason.into();
443 let cause = FailureCause::classify(source, &reason);
444 Self {
445 title: title.into(),
446 source,
447 confidence,
448 reason,
449 cause,
450 fallback: None,
451 tried: vec![source.as_str().to_owned()],
452 actions: Vec::new(),
453 commands: Vec::new(),
454 records: Vec::new(),
455 }
456 }
457
458 #[must_use]
460 pub fn with_likely_cause(mut self, cause: FailureCause) -> Self {
461 self.cause = cause;
462 self
463 }
464
465 #[must_use]
467 pub fn with_fallback(mut self, fallback: impl Into<String>) -> Self {
468 self.fallback = Some(fallback.into());
469 self
470 }
471
472 #[must_use]
474 pub fn with_tried(mut self, tried: impl Into<String>) -> Self {
475 self.tried.push(tried.into());
476 self
477 }
478
479 #[must_use]
481 pub fn with_actions(mut self, actions: impl IntoIterator<Item = DiagnosticAction>) -> Self {
482 self.actions.extend(actions);
483 self
484 }
485
486 #[must_use]
488 pub fn with_command(mut self, command: impl Into<String>) -> Self {
489 self.commands.push(command.into());
490 self
491 }
492
493 #[must_use]
495 pub fn with_record(mut self, record: DiagnosticRecord) -> Self {
496 self.records.push(record);
497 self
498 }
499
500 #[must_use]
502 pub fn redacted(&self, redactor: &Redactor) -> Self {
503 Self {
504 title: redactor.redact(&self.title),
505 source: self.source,
506 confidence: self.confidence,
507 reason: redactor.redact(&self.reason),
508 cause: self.cause,
509 fallback: self.fallback.as_ref().map(|value| redactor.redact(value)),
510 tried: self
511 .tried
512 .iter()
513 .map(|value| redactor.redact(value))
514 .collect(),
515 actions: self.actions.clone(),
516 commands: self
517 .commands
518 .iter()
519 .map(|value| redactor.redact(value))
520 .collect(),
521 records: self
522 .records
523 .iter()
524 .map(|record| record.redacted(redactor))
525 .collect(),
526 }
527 }
528
529 #[must_use]
531 pub fn to_local_text(&self) -> String {
532 let mut lines = vec![
533 format!("title={}", self.title),
534 format!("source={}", self.source),
535 format!("confidence={}", self.confidence),
536 format!("reason={}", self.reason),
537 format!("cause={}", self.cause),
538 ];
539 if let Some(fallback) = &self.fallback {
540 lines.push(format!("fallback={fallback}"));
541 }
542 for tried in &self.tried {
543 lines.push(format!("tried={tried}"));
544 }
545 for action in &self.actions {
546 lines.push(format!("action={action}"));
547 }
548 for command in self.suggested_commands() {
549 lines.push(format!("command={command}"));
550 }
551 for record in &self.records {
552 lines.push(record.to_local_text());
553 }
554 lines.join("\n")
555 }
556
557 #[must_use]
559 pub fn suggested_commands(&self) -> Vec<String> {
560 if !self.commands.is_empty() {
561 return self.commands.clone();
562 }
563 self.actions
564 .iter()
565 .map(|action| action.command().to_owned())
566 .collect()
567 }
568
569 #[must_use]
571 pub fn into_document(self) -> IndexDocument {
572 let commands = self.suggested_commands();
573 let mut document = IndexDocument::titled(self.title.clone());
574 document.metadata.quality = Some(DocumentQuality::new(
575 DocumentQualityCategory::Failed,
576 0,
577 [
578 format!("source: {}", self.source),
579 format!("confidence: {}", self.confidence),
580 format!("cause: {}", self.cause),
581 self.reason.clone(),
582 ],
583 ));
584 document.push(IndexNode::Heading {
585 level: 1,
586 text: self.title.clone(),
587 });
588 document.push(IndexNode::Error(self.reason.clone()));
589 document.push(IndexNode::Heading {
590 level: 2,
591 text: "What Index tried".to_owned(),
592 });
593 let mut tried = vec![
594 format!("source: {}", self.source),
595 format!("confidence: {}", self.confidence),
596 ];
597 tried.extend(self.tried.clone());
598 if let Some(fallback) = self.fallback {
599 tried.push(format!("fallback path: {fallback}"));
600 }
601 document.push(IndexNode::List {
602 ordered: false,
603 items: tried,
604 });
605 document.push(IndexNode::Heading {
606 level: 2,
607 text: "Likely cause".to_owned(),
608 });
609 document.push(IndexNode::Paragraph(format!(
610 "{}: {}",
611 self.cause,
612 self.cause.explanation()
613 )));
614 if !commands.is_empty() {
615 document.push(IndexNode::Heading {
616 level: 2,
617 text: "Suggested commands".to_owned(),
618 });
619 document.push(IndexNode::CodeBlock {
620 language: Some("sh".to_owned()),
621 code: commands.join("\n"),
622 });
623 }
624 if !self.actions.is_empty() {
625 document.push(IndexNode::Heading {
626 level: 2,
627 text: "Suggested actions".to_owned(),
628 });
629 document.push(IndexNode::List {
630 ordered: false,
631 items: self
632 .actions
633 .into_iter()
634 .map(|action| action.label().to_owned())
635 .collect(),
636 });
637 }
638 if !self.records.is_empty() {
639 document.push(IndexNode::Heading {
640 level: 2,
641 text: "Diagnostics".to_owned(),
642 });
643 for record in self.records {
644 document.push(IndexNode::Paragraph(record.to_local_text()));
645 }
646 }
647 document
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::{
654 DiagnosticAction, DiagnosticConfidence, DiagnosticRecord, DiagnosticSeverity,
655 DiagnosticSource, FailureCause, FailureDiagnostic, TelemetryPolicy,
656 };
657 use crate::Redactor;
658
659 #[test]
660 fn telemetry_policy_disallows_automatic_network_transmission() {
661 assert!(!TelemetryPolicy::LocalOnly.allows_network_transmission());
662 }
663
664 #[test]
665 fn diagnostic_record_formats_stable_local_text() {
666 let record = DiagnosticRecord::new(
667 DiagnosticSeverity::Warning,
668 "INDEX-WARN",
669 "content was truncated",
670 )
671 .with_field("url", "https://example.test")
672 .with_field("bytes", "1024");
673
674 assert_eq!(
675 record.to_local_text(),
676 "warning[INDEX-WARN]: content was truncated\nurl=https://example.test\nbytes=1024"
677 );
678 }
679
680 #[test]
681 fn diagnostic_record_redacts_message_and_fields() {
682 let mut redactor = Redactor::new();
683 redactor.add_secret("secret-value");
684 let record = DiagnosticRecord::new(
685 DiagnosticSeverity::Error,
686 "INDEX-AUTH",
687 "Authorization: Bearer secret-value",
688 )
689 .with_field("cookie", "Cookie: session=secret-value")
690 .with_field("path", "/tmp/index");
691
692 let redacted = record.redacted(&redactor);
693
694 assert!(redacted.to_local_text().contains("[REDACTED]"));
695 assert!(!redacted.to_local_text().contains("secret-value"));
696 assert!(redacted.to_local_text().contains("path=/tmp/index"));
697 }
698
699 #[test]
700 fn failure_diagnostic_formats_redacts_and_renders_document() {
701 let mut redactor = Redactor::new();
702 redactor.add_secret("secret-token");
703 let diagnostic = FailureDiagnostic::new(
704 "Unsupported page",
705 DiagnosticSource::Readability,
706 DiagnosticConfidence::Low,
707 "could not understand token=secret-token",
708 )
709 .with_fallback("generic transformer")
710 .with_tried("readability extraction")
711 .with_command(":capture save unsupported.capture")
712 .with_actions([
713 DiagnosticAction::TryHeadless,
714 DiagnosticAction::Extract,
715 DiagnosticAction::Capture,
716 DiagnosticAction::AddFixture,
717 ])
718 .with_record(
719 DiagnosticRecord::new(
720 DiagnosticSeverity::Warning,
721 "INDEX-LOW-CONFIDENCE",
722 "private token secret-token",
723 )
724 .with_field("url", "https://example.test/?token=secret-token"),
725 );
726
727 let local_text = diagnostic.to_local_text();
728 assert!(local_text.contains("source=readability"));
729 assert!(local_text.contains("confidence=low"));
730 assert!(local_text.contains("cause=unsupported-page-shape"));
731 assert!(local_text.contains("tried=readability extraction"));
732 assert!(local_text.contains("action=try headless fallback"));
733 assert!(local_text.contains("command=:capture save unsupported.capture"));
734
735 let redacted = diagnostic.redacted(&redactor);
736 assert!(!redacted.to_local_text().contains("secret-token"));
737 assert!(redacted.to_local_text().contains("[REDACTED]"));
738
739 let document = redacted.into_document();
740 assert_eq!(document.title, "Unsupported page");
741 assert!(!document.is_empty());
742 }
743
744 #[test]
745 fn failure_cause_classification_is_stable() {
746 assert_eq!(
747 FailureCause::classify(DiagnosticSource::Network, "dns failed"),
748 FailureCause::NetworkUnavailable
749 );
750 assert_eq!(
751 FailureCause::classify(DiagnosticSource::Headless, "timed out after 1000ms"),
752 FailureCause::Timeout
753 );
754 assert_eq!(
755 FailureCause::classify(DiagnosticSource::LocalInput, "unsafe scheme denied"),
756 FailureCause::BlockedByPolicy
757 );
758 assert_eq!(
759 FailureCause::classify(DiagnosticSource::GenericTransformer, "no readable content"),
760 FailureCause::EmptyContent
761 );
762 assert_eq!(
763 FailureCause::classify(DiagnosticSource::Adapter, "uncertain detection"),
764 FailureCause::AdapterMismatch
765 );
766 assert_eq!(
767 FailureCause::classify(DiagnosticSource::Parser, "malformed HTML parse failed"),
768 FailureCause::ParseFailed
769 );
770 assert_eq!(
771 FailureCause::classify(DiagnosticSource::Extraction, "JSON schema failure"),
772 FailureCause::ExtractionFailed
773 );
774 assert_eq!(
775 FailureCause::classify(DiagnosticSource::Renderer, "terminal layout overflow"),
776 FailureCause::RendererFailed
777 );
778 assert_eq!(
779 FailureCause::classify(DiagnosticSource::Shelf, "shelf index missing"),
780 FailureCause::ShelfUnavailable
781 );
782 assert_eq!(
783 FailureCause::classify(
784 DiagnosticSource::Readability,
785 "low confidence partial document"
786 ),
787 FailureCause::LowConfidence
788 );
789 }
790
791 #[test]
792 fn failure_document_contains_commands_and_capture_action() {
793 let document = FailureDiagnostic::new(
794 "Failed",
795 DiagnosticSource::Network,
796 DiagnosticConfidence::Failed,
797 "could not fetch",
798 )
799 .with_actions([DiagnosticAction::Retry, DiagnosticAction::Capture])
800 .into_document();
801 let rendered = format!("{:?}", document.nodes);
802
803 assert!(rendered.contains("What Index tried"));
804 assert!(rendered.contains("Likely cause"));
805 assert!(rendered.contains("Suggested commands"));
806 assert!(rendered.contains(":capture preview"));
807 }
808
809 #[test]
810 fn failure_documents_cover_major_boundaries_with_exact_commands() {
811 for (source, reason, command, cause) in [
812 (
813 DiagnosticSource::Parser,
814 "malformed parse input",
815 ":capture preview",
816 FailureCause::ParseFailed,
817 ),
818 (
819 DiagnosticSource::Network,
820 "dns failed",
821 ":open <url>",
822 FailureCause::NetworkUnavailable,
823 ),
824 (
825 DiagnosticSource::GenericTransformer,
826 "unsupported page shape",
827 ":repair promote <region-id>",
828 FailureCause::UnsupportedPageShape,
829 ),
830 (
831 DiagnosticSource::Extraction,
832 "JSON schema failure",
833 ":extract links",
834 FailureCause::ExtractionFailed,
835 ),
836 (
837 DiagnosticSource::Renderer,
838 "terminal layout overflow",
839 ":repair promote <region-id>",
840 FailureCause::RendererFailed,
841 ),
842 (
843 DiagnosticSource::Shelf,
844 "shelf index missing",
845 "index shelf search <query>",
846 FailureCause::ShelfUnavailable,
847 ),
848 ] {
849 let document = FailureDiagnostic::new(
850 "Boundary failed",
851 source,
852 DiagnosticConfidence::Failed,
853 reason,
854 )
855 .with_actions([
856 DiagnosticAction::Retry,
857 DiagnosticAction::Extract,
858 DiagnosticAction::Capture,
859 DiagnosticAction::Repair,
860 DiagnosticAction::ShelfSearch,
861 ])
862 .into_document();
863 let rendered = format!("{:?}", document.nodes);
864
865 assert!(
866 rendered.contains(command),
867 "{source} missing command {command}"
868 );
869 assert!(
870 rendered.contains(cause.as_str()),
871 "{source} missing cause {cause}"
872 );
873 assert!(document.metadata.quality.as_ref().is_some_and(|quality| {
874 quality.category == crate::DocumentQualityCategory::Failed
875 }));
876 }
877 }
878
879 #[test]
880 fn diagnostic_enum_names_are_stable() {
881 assert_eq!(DiagnosticSource::LocalInput.as_str(), "local-input");
882 assert_eq!(DiagnosticSource::Network.to_string(), "network");
883 assert_eq!(DiagnosticSource::Parser.to_string(), "parser");
884 assert_eq!(
885 DiagnosticSource::GenericTransformer.to_string(),
886 "generic-transformer"
887 );
888 assert_eq!(DiagnosticSource::Adapter.to_string(), "adapter");
889 assert_eq!(DiagnosticSource::Headless.to_string(), "headless");
890 assert_eq!(DiagnosticSource::Extraction.to_string(), "extraction");
891 assert_eq!(DiagnosticSource::Renderer.to_string(), "renderer");
892 assert_eq!(DiagnosticSource::Shelf.to_string(), "shelf");
893 assert_eq!(DiagnosticConfidence::Failed.as_str(), "failed");
894 assert_eq!(DiagnosticConfidence::Medium.to_string(), "medium");
895 assert_eq!(DiagnosticAction::Retry.to_string(), "retry the request");
896 assert_eq!(
897 DiagnosticAction::Repair.command(),
898 ":repair promote <region-id>"
899 );
900 assert_eq!(FailureCause::Timeout.to_string(), "timeout");
901 assert_eq!(
902 FailureCause::ShelfUnavailable.to_string(),
903 "shelf-unavailable"
904 );
905 }
906}