Skip to main content

synaps_cli/sidecar/
spawn.rs

1//! Plugin-supplied sidecar spawn parameters.
2//!
3//! This RPC keeps bootstrap ownership inside the selected plugin. Core asks
4//! `sidecar.spawn_args` and the plugin replies with the args to pass to its
5//! binary plus optional metadata. The plugin is free to source those values
6//! from anywhere it likes (its own config namespace, environment, hardware,
7//! or generated defaults). Core never sees plugin-specific keys.
8//!
9//! ## Wire shape (`sidecar.spawn_args` response)
10//!
11//! ```json
12//! {
13//!   "args": ["--model-path", "/abs/path/to/model.bin", "--language", "en"],
14//!   "language": "en"
15//! }
16//! ```
17//!
18//! Both fields are optional. A plugin that has no overrides at all
19//! can simply return `{}` — core then falls back to manifest defaults
20//! (the `provides.sidecar.model.default_path`, if any).
21
22use serde::{Deserialize, Serialize};
23
24/// Sidecar spawn parameters returned by the plugin's `sidecar.spawn_args` RPC.
25#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
26pub struct SidecarSpawnArgs {
27    /// CLI arguments to pass to the sidecar binary.
28    ///
29    /// The plugin is responsible for resolving any tilde-expansion,
30    /// path validation, and modality-specific knobs. Core treats this
31    /// as opaque.
32    #[serde(default)]
33    pub args: Vec<String>,
34    /// Optional plugin-owned language hint. Core stores and forwards this
35    /// value only where a plugin explicitly asks for it; the sidecar
36    /// protocol does not prescribe language semantics.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub language: Option<String>,
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn deserializes_full_payload() {
47        let payload = r#"{
48            "args": ["--model-path", "/m.bin", "--language", "en"],
49            "language": "en"
50        }"#;
51        let parsed: SidecarSpawnArgs = serde_json::from_str(payload).unwrap();
52        assert_eq!(parsed.args, vec!["--model-path", "/m.bin", "--language", "en"]);
53        assert_eq!(parsed.language.as_deref(), Some("en"));
54    }
55
56    #[test]
57    fn deserializes_empty_object_as_defaults() {
58        let parsed: SidecarSpawnArgs = serde_json::from_str("{}").unwrap();
59        assert!(parsed.args.is_empty());
60        assert_eq!(parsed.language, None);
61    }
62
63    #[test]
64    fn deserializes_args_only() {
65        let parsed: SidecarSpawnArgs =
66            serde_json::from_str(r#"{"args":["--mute"]}"#).unwrap();
67        assert_eq!(parsed.args, vec!["--mute"]);
68        assert_eq!(parsed.language, None);
69    }
70
71    #[test]
72    fn round_trips_through_serde() {
73        let original = SidecarSpawnArgs {
74            args: vec!["--foo".into(), "bar".into()],
75            language: Some("fr".into()),
76        };
77        let json = serde_json::to_string(&original).unwrap();
78        let parsed: SidecarSpawnArgs = serde_json::from_str(&json).unwrap();
79        assert_eq!(parsed, original);
80    }
81
82    #[test]
83    fn skip_serializing_none_language() {
84        let original = SidecarSpawnArgs {
85            args: vec![],
86            language: None,
87        };
88        let json = serde_json::to_string(&original).unwrap();
89        assert!(!json.contains("language"), "`language: None` should be omitted, got: {json}");
90    }
91}