Skip to main content

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}