kanade_shared/ipc/support.rs
1//! `support.upload_diagnostics` types — one-click "サポートに問い
2//! 合わせる" diagnostics bundle.
3//!
4//! Per SPEC §2.1: the agent collects `{pc_id, recent_inventory,
5//! last_N_events, agent_log_tail}`, zips them, uploads to the
6//! JetStream Object Store, and the backend opens a helpdesk
7//! ticket. The Client App shows the resulting ticket URL so the
8//! user can paste it into the chat / email follow-up.
9
10use serde::{Deserialize, Serialize};
11
12/// `support.upload_diagnostics` params — optional user-supplied
13/// context so the helpdesk has triage info at ticket-open time
14/// without a second round-trip.
15#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, Default)]
16pub struct SupportUploadDiagnosticsParams {
17 /// One-line summary the user typed into the support form
18 /// (e.g. "Teams won't open since the update"). May be empty.
19 #[serde(default)]
20 pub summary: String,
21 /// Optional longer description / repro steps.
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub detail: Option<String>,
24 /// User's contact preference (email / Teams handle / phone).
25 /// Free-form because organisations differ; the SPA presents a
26 /// drop-down but stores the chosen value as a string.
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub contact: Option<String>,
29}
30
31/// `support.upload_diagnostics` response.
32#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
33pub struct SupportUploadDiagnosticsResult {
34 /// JetStream Object Store key for the uploaded zip — used by
35 /// the helpdesk's tooling to fetch the bundle without
36 /// re-asking the user to attach it.
37 pub object_key: String,
38 /// Ticket id from whichever helpdesk system the backend
39 /// integrated with (Jira, ServiceNow, …). `None` when the
40 /// upload succeeded but ticket creation deferred (the backend
41 /// retries asynchronously).
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub ticket_id: Option<String>,
44 /// User-friendly URL to view the ticket (or, when `ticket_id`
45 /// is None, a generic "your diagnostics have been uploaded"
46 /// landing page). The Client App shows this as the post-submit
47 /// confirmation.
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub ticket_url: Option<String>,
50 /// Size of the uploaded zip in bytes. Surfaced so the SPA can
51 /// show "Uploaded 4.2 MB" — reassuring proof that the bundle
52 /// went through.
53 pub size_bytes: u64,
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn params_default_is_empty_summary() {
62 let p = SupportUploadDiagnosticsParams::default();
63 assert_eq!(p.summary, "");
64 assert!(p.detail.is_none());
65 assert!(p.contact.is_none());
66 }
67
68 #[test]
69 fn params_minimal_wire_decodes() {
70 let p: SupportUploadDiagnosticsParams = serde_json::from_str("{}").unwrap();
71 assert_eq!(p.summary, "");
72 }
73
74 #[test]
75 fn result_with_ticket_round_trips() {
76 let r = SupportUploadDiagnosticsResult {
77 object_key: "support/2026-05-24/abc123.zip".into(),
78 ticket_id: Some("HELP-42".into()),
79 ticket_url: Some("https://helpdesk.example.com/tickets/HELP-42".into()),
80 size_bytes: 4_200_000,
81 };
82 let json = serde_json::to_string(&r).unwrap();
83 let back: SupportUploadDiagnosticsResult = serde_json::from_str(&json).unwrap();
84 assert_eq!(back.object_key, r.object_key);
85 assert_eq!(back.ticket_id, r.ticket_id);
86 assert_eq!(back.ticket_url, r.ticket_url);
87 assert_eq!(back.size_bytes, r.size_bytes);
88 }
89
90 #[test]
91 fn result_without_ticket_omits_field_on_wire() {
92 // Deferred-ticket path: object uploaded, ticket id pending.
93 // SPA UI key: ticket_id absence ⇒ "uploaded, ticket
94 // pending". Wire MUST be field-absent, not null.
95 let r = SupportUploadDiagnosticsResult {
96 object_key: "x".into(),
97 ticket_id: None,
98 ticket_url: None,
99 size_bytes: 0,
100 };
101 let v = serde_json::to_value(&r).unwrap();
102 assert!(v.get("ticket_id").is_none(), "wire: {v:?}");
103 assert!(v.get("ticket_url").is_none(), "wire: {v:?}");
104 }
105}