Skip to main content

agentmux_srv/backend/wconfig/
mod.rs

1// Copyright 2025, Command Line Inc.
2// Copyright 2025-2026, AgentMux Corp.
3// SPDX-License-Identifier: Apache-2.0
4
5//! Configuration system: settings, themes, widgets, bookmarks, connections.
6//! Port of Go's pkg/wconfig/.
7//!
8//! Provides the full configuration type hierarchy and a thread-safe
9//! in-memory config holder (`ConfigState`).
10//!
11//! Note `ConfigState` does NOT watch the filesystem, despite the name this
12//! module's type carried until 2026-09-06. Filesystem watching lives in
13//! `backend::config_watcher_fs`, on top of the shared `fs_watch::pool`. Two
14//! similarly-named modules where only one watched cost real debugging time;
15//! see REPORT_NETWORK_ARCHITECTURE_DRYNESS_AND_ROBUST_LAN_2026_09_06.md §7.
16
17mod loader;
18pub mod redact;
19pub mod types;
20mod state;
21
22// Re-export all public APIs so callers can continue using `wconfig::Type`.
23pub use loader::*;
24pub use redact::redact_full_config_for_renderer;
25pub use types::*;
26pub use state::*;
27
28// ---- Config file constants ----
29
30pub const SETTINGS_FILE: &str = "settings.json";
31pub const SETTINGS_TEMPLATE: &str = include_str!("../../../../../settings-template.jsonc");
32#[allow(dead_code)]
33pub const CONNECTIONS_FILE: &str = "connections.json";
34#[allow(dead_code)]
35pub const PROFILES_FILE: &str = "profiles.json";
36
37// ---- Tests ----
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use std::path::PathBuf;
43
44    // -- SettingsType serde --
45
46    #[test]
47    fn test_settings_default_empty() {
48        let s = SettingsType::default();
49        let json = serde_json::to_string(&s).unwrap();
50        assert_eq!(json, "{}");
51    }
52
53    #[test]
54    fn test_settings_terminal_fields() {
55        let s = SettingsType {
56            term_font_size: 14.0,
57            term_theme: "dracula".to_string(),
58            term_scrollback: Some(10000),
59            term_copy_on_select: Some(true),
60            ..Default::default()
61        };
62        let json = serde_json::to_string(&s).unwrap();
63        assert!(json.contains("\"term:fontsize\":14.0"));
64        assert!(json.contains("\"term:theme\":\"dracula\""));
65        assert!(json.contains("\"term:scrollback\":10000"));
66        assert!(json.contains("\"term:copyonselect\":true"));
67    }
68
69    #[test]
70    fn test_settings_window_fields() {
71        let s = SettingsType {
72            window_transparent: true,
73            window_opacity: Some(0.9),
74            window_zoom: Some(1.5),
75            window_dimensions: "1920x1080".to_string(),
76            ..Default::default()
77        };
78        let json = serde_json::to_string(&s).unwrap();
79        assert!(json.contains("\"window:transparent\":true"));
80        assert!(json.contains("\"window:opacity\":0.9"));
81        assert!(json.contains("\"window:zoom\":1.5"));
82        assert!(json.contains("\"window:dimensions\":\"1920x1080\""));
83    }
84
85    #[test]
86    fn test_settings_from_json() {
87        let json_str = r#"{
88            "term:fontsize": 13,
89            "term:theme": "solarized-dark",
90            "term:scrollback": 5000,
91            "window:transparent": true,
92            "window:opacity": 0.85,
93            "telemetry:enabled": true
94        }"#;
95        let s: SettingsType = serde_json::from_str(json_str).unwrap();
96        assert_eq!(s.term_font_size, 13.0);
97        assert_eq!(s.term_theme, "solarized-dark");
98        assert_eq!(s.term_scrollback, Some(5000));
99        assert!(s.window_transparent);
100        assert_eq!(s.window_opacity, Some(0.85));
101        assert!(s.telemetry_enabled);
102    }
103
104    #[test]
105    fn test_settings_roundtrip() {
106        let s = SettingsType {
107            term_font_size: 14.0,
108            term_theme: "dracula".to_string(),
109            window_opacity: Some(0.95),
110            ..Default::default()
111        };
112        let json = serde_json::to_string(&s).unwrap();
113        let parsed: SettingsType = serde_json::from_str(&json).unwrap();
114        assert_eq!(parsed.term_font_size, 14.0);
115        assert_eq!(parsed.term_theme, "dracula");
116        assert_eq!(parsed.window_opacity, Some(0.95));
117    }
118
119    #[test]
120    fn test_settings_unknown_keys_passthrough() {
121        // Legacy keys (ai:*, autoupdate:*, editor:*, markdown:*, and
122        // pane:tabstrip — SPEC_PANE_TAB_DRAG_LANDING_FLASH_AND_LAST_TAB_CLOSE_2026_09_24.md
123        // §8) removed from SettingsType should land in `extra` via the
124        // flatten catch-all so existing user settings.json files don't error.
125        let json_str = r#"{
126            "ai:model": "claude-3-opus",
127            "autoupdate:enabled": true,
128            "editor:wordwrap": true,
129            "pane:tabstrip": "multi-only",
130            "term:fontsize": 14
131        }"#;
132        let s: SettingsType = serde_json::from_str(json_str).unwrap();
133        assert_eq!(s.term_font_size, 14.0);
134        assert!(s.extra.contains_key("ai:model"));
135        assert!(s.extra.contains_key("autoupdate:enabled"));
136        assert!(s.extra.contains_key("editor:wordwrap"));
137        assert!(s.extra.contains_key("pane:tabstrip"));
138    }
139
140    // -- TermThemeType serde --
141
142    #[test]
143    fn test_term_theme_serde() {
144        let theme = TermThemeType {
145            display_name: "Dracula".to_string(),
146            black: "#282a36".to_string(),
147            red: "#ff5555".to_string(),
148            foreground: "#f8f8f2".to_string(),
149            background: "#282a36".to_string(),
150            cursor: "#f8f8f2".to_string(),
151            bright_red: "#ff6e6e".to_string(),
152            ..Default::default()
153        };
154        let json = serde_json::to_string(&theme).unwrap();
155        assert!(json.contains(r#""display:name":"Dracula""#));
156        assert!(json.contains(r##""brightRed":"#ff6e6e""##));
157        assert!(json.contains(r##""foreground":"#f8f8f2""##));
158
159        let parsed: TermThemeType = serde_json::from_str(&json).unwrap();
160        assert_eq!(parsed.display_name, "Dracula");
161        assert_eq!(parsed.bright_red, "#ff6e6e");
162    }
163
164    #[test]
165    fn test_term_theme_from_go_json() {
166        let go_json = r##"{
167            "display:name": "Solarized Dark",
168            "display:order": 1.0,
169            "black": "#073642",
170            "red": "#dc322f",
171            "green": "#859900",
172            "yellow": "#b58900",
173            "blue": "#268bd2",
174            "magenta": "#d33682",
175            "cyan": "#2aa198",
176            "white": "#eee8d5",
177            "brightBlack": "#002b36",
178            "brightRed": "#cb4b16",
179            "brightGreen": "#586e75",
180            "brightYellow": "#657b83",
181            "brightBlue": "#839496",
182            "brightMagenta": "#6c71c4",
183            "brightCyan": "#93a1a1",
184            "brightWhite": "#fdf6e3",
185            "foreground": "#839496",
186            "background": "#002b36",
187            "cursor": "#839496",
188            "selectionBackground": "#073642"
189        }"##;
190        let parsed: TermThemeType = serde_json::from_str(go_json).unwrap();
191        assert_eq!(parsed.display_name, "Solarized Dark");
192        assert_eq!(parsed.display_order, 1.0);
193        assert_eq!(parsed.black, "#073642");
194        assert_eq!(parsed.bright_cyan, "#93a1a1");
195        assert_eq!(parsed.selection_background, "#073642");
196    }
197
198    // -- WebBookmark serde --
199
200    #[test]
201    fn test_web_bookmark_serde() {
202        let bm = WebBookmark {
203            url: "https://example.com".to_string(),
204            title: "Example".to_string(),
205            icon_url: "https://example.com/favicon.ico".to_string(),
206            display_order: 1.0,
207            ..Default::default()
208        };
209        let json = serde_json::to_string(&bm).unwrap();
210        assert!(json.contains("\"iconurl\":\"https://example.com/favicon.ico\""));
211        assert!(json.contains("\"display:order\":1.0"));
212
213        let parsed: WebBookmark = serde_json::from_str(&json).unwrap();
214        assert_eq!(parsed.url, "https://example.com");
215    }
216
217    // -- ConnKeywords serde --
218
219    #[test]
220    fn test_conn_keywords_serde() {
221        let kw = ConnKeywords {
222            ssh_user: Some("admin".to_string()),
223            ssh_hostname: Some("server.example.com".to_string()),
224            ssh_port: Some("2222".to_string()),
225            ssh_identity_file: vec!["~/.ssh/id_rsa".to_string()],
226            ..Default::default()
227        };
228        let json = serde_json::to_string(&kw).unwrap();
229        assert!(json.contains("\"ssh:user\":\"admin\""));
230        assert!(json.contains("\"ssh:hostname\":\"server.example.com\""));
231        assert!(json.contains("\"ssh:port\":\"2222\""));
232        assert!(json.contains("\"ssh:identityfile\":[\"~/.ssh/id_rsa\"]"));
233    }
234
235    #[test]
236    fn test_conn_keywords_from_go_json() {
237        let go_json = r#"{
238            "ssh:user": "deploy",
239            "ssh:hostname": "prod.example.com",
240            "ssh:port": "22",
241            "ssh:identityfile": ["~/.ssh/deploy_key", "~/.ssh/id_ed25519"],
242            "ssh:pubkeyauthentication": true,
243            "ssh:proxyjump": ["bastion.example.com"],
244            "term:theme": "monokai",
245            "cmd:env": {"NODE_ENV": "production"}
246        }"#;
247        let parsed: ConnKeywords = serde_json::from_str(go_json).unwrap();
248        assert_eq!(parsed.ssh_user, Some("deploy".to_string()));
249        assert_eq!(parsed.ssh_identity_file.len(), 2);
250        assert_eq!(parsed.ssh_pubkey_authentication, Some(true));
251        assert_eq!(parsed.ssh_proxy_jump, vec!["bastion.example.com"]);
252        assert_eq!(parsed.term_theme, "monokai");
253        assert_eq!(parsed.cmd_env.get("NODE_ENV").unwrap(), "production");
254    }
255
256    // -- WidgetConfigType serde --
257
258    #[test]
259    fn test_widget_config_serde() {
260        let w = WidgetConfigType {
261            display_order: 1.5,
262            icon: "terminal".to_string(),
263            label: "Shell".to_string(),
264            description: "Terminal emulator".to_string(),
265            ..Default::default()
266        };
267        let json = serde_json::to_string(&w).unwrap();
268        assert!(json.contains("\"display:order\":1.5"));
269        assert!(json.contains("\"label\":\"Shell\""));
270    }
271
272    // -- MimeTypeConfigType serde --
273
274    #[test]
275    fn test_mime_type_config_serde() {
276        let mt = MimeTypeConfigType {
277            icon: "file-code".to_string(),
278            color: "#e06c75".to_string(),
279        };
280        let json = serde_json::to_string(&mt).unwrap();
281        assert!(json.contains("\"icon\":\"file-code\""));
282        assert!(json.contains(r##""color":"#e06c75""##));
283    }
284
285    // -- FullConfigType serde --
286
287    #[test]
288    fn test_full_config_default() {
289        let config = FullConfigType::default();
290        let json = serde_json::to_string(&config).unwrap();
291        // Should have all top-level keys even if empty
292        assert!(json.contains("\"settings\""));
293        assert!(json.contains("\"mimetypes\""));
294        assert!(json.contains("\"termthemes\""));
295    }
296
297    #[test]
298    fn test_full_config_with_data() {
299        let mut config = FullConfigType::default();
300        config.settings.term_theme = "dracula".to_string();
301        config.term_themes.insert(
302            "test".to_string(),
303            TermThemeType {
304                display_name: "Test Theme".to_string(),
305                ..Default::default()
306            },
307        );
308        config.bookmarks.insert(
309            "example".to_string(),
310            WebBookmark {
311                url: "https://example.com".to_string(),
312                ..Default::default()
313            },
314        );
315
316        let json = serde_json::to_string(&config).unwrap();
317        let parsed: FullConfigType = serde_json::from_str(&json).unwrap();
318        assert_eq!(parsed.settings.term_theme, "dracula");
319        assert_eq!(parsed.term_themes.len(), 1);
320        assert_eq!(
321            parsed.term_themes.get("test").unwrap().display_name,
322            "Test Theme"
323        );
324        assert_eq!(parsed.bookmarks.len(), 1);
325    }
326
327    #[test]
328    fn test_full_config_from_json() {
329        let json = r##"{
330            "settings": {
331                "term:theme": "solarized-dark",
332                "term:fontsize": 14,
333                "window:transparent": true
334            },
335            "mimetypes": {
336                "text/rust": {"icon": "brands@rust", "color": "#dea584"}
337            },
338            "termthemes": {
339                "dracula": {
340                    "display:name": "Dracula",
341                    "black": "#282a36",
342                    "foreground": "#f8f8f2"
343                }
344            },
345            "bookmarks": {
346                "docs": {"url": "https://docs.rs", "title": "Rust Docs"}
347            },
348            "connections": {
349                "prod-server": {
350                    "ssh:user": "admin",
351                    "ssh:hostname": "prod.example.com"
352                }
353            },
354            "widgets": {},
355            "presets": {}
356        }"##;
357        let parsed: FullConfigType = serde_json::from_str(json).unwrap();
358        assert_eq!(parsed.settings.term_theme, "solarized-dark");
359        assert_eq!(parsed.settings.term_font_size, 14.0);
360        assert!(parsed.settings.window_transparent);
361        assert_eq!(parsed.mime_types.len(), 1);
362        assert_eq!(parsed.term_themes.get("dracula").unwrap().display_name, "Dracula");
363        assert_eq!(parsed.bookmarks.get("docs").unwrap().title, "Rust Docs");
364        assert_eq!(
365            parsed.connections.get("prod-server").unwrap().ssh_user,
366            Some("admin".to_string())
367        );
368    }
369
370    // -- ConfigError serde --
371
372    #[test]
373    fn test_config_error_serde() {
374        let err = ConfigError {
375            file: "settings.json".to_string(),
376            err: "unexpected token at line 5".to_string(),
377        };
378        let json = serde_json::to_string(&err).unwrap();
379        assert!(json.contains(r#""file":"settings.json""#));
380        assert!(json.contains(r#""err":"unexpected token at line 5""#));
381    }
382
383    // -- WebhookConfigType serde --
384
385    #[test]
386    fn test_webhook_config_serde() {
387        let wh = WebhookConfigType {
388            version: "1".to_string(),
389            workspace_id: "ws-123".to_string(),
390            auth_token: "tok-abc".to_string(),
391            cloud_endpoint: "wss://cloud.example.com".to_string(),
392            enabled: true,
393            terminals: vec!["term-1".to_string()],
394        };
395        let json = serde_json::to_string(&wh).unwrap();
396        assert!(json.contains("\"workspaceId\":\"ws-123\""));
397        assert!(json.contains("\"authToken\":\"tok-abc\""));
398        assert!(json.contains("\"cloudEndpoint\":\"wss://cloud.example.com\""));
399    }
400
401    // -- ConfigState --
402
403    #[test]
404    fn test_config_watcher_default() {
405        let watcher = ConfigState::new();
406        let config = watcher.get_full_config();
407        assert!(config.settings.term_theme.is_empty());
408    }
409
410    #[test]
411    fn test_config_watcher_with_initial() {
412        let mut config = FullConfigType::default();
413        config.settings.term_theme = "dracula".to_string();
414        let watcher = ConfigState::with_config(config);
415        assert_eq!(watcher.get_settings().term_theme, "dracula");
416    }
417
418    #[test]
419    fn test_config_watcher_update_settings() {
420        let watcher = ConfigState::new();
421        let settings = SettingsType {
422            term_theme: "solarized-dark".to_string(),
423            ..Default::default()
424        };
425        watcher.update_settings(settings);
426        assert_eq!(watcher.get_settings().term_theme, "solarized-dark");
427    }
428
429    #[test]
430    fn test_config_watcher_thread_safety() {
431        use std::sync::Arc;
432        use std::thread;
433
434        let watcher = Arc::new(ConfigState::new());
435        let handles: Vec<_> = (0..4)
436            .map(|i| {
437                let w = watcher.clone();
438                thread::spawn(move || {
439                    let s = SettingsType {
440                        term_theme: format!("theme-{}", i),
441                        ..Default::default()
442                    };
443                    w.update_settings(s);
444                    let _ = w.get_full_config();
445                })
446            })
447            .collect();
448
449        for h in handles {
450            h.join().unwrap();
451        }
452        // Should not panic — proves thread safety
453        let _ = watcher.get_settings();
454    }
455
456    // -- expand_env_vars --
457
458    #[test]
459    fn test_expand_env_vars_no_vars() {
460        assert_eq!(expand_env_vars("hello world"), "hello world");
461    }
462
463    #[test]
464    fn test_expand_env_vars_with_var() {
465        std::env::set_var("TEST_WCONFIG_VAR", "replaced");
466        let result = expand_env_vars("prefix $ENV:TEST_WCONFIG_VAR suffix");
467        assert_eq!(result, "prefix replaced suffix");
468        std::env::remove_var("TEST_WCONFIG_VAR");
469    }
470
471    #[test]
472    fn test_expand_env_vars_with_fallback() {
473        let result = expand_env_vars("$ENV:NONEXISTENT_VAR_12345:fallback_value");
474        assert_eq!(result, "fallback_value");
475    }
476
477    #[test]
478    fn test_expand_env_vars_missing_no_fallback() {
479        let result = expand_env_vars("$ENV:NONEXISTENT_VAR_99999");
480        assert_eq!(result, "");
481    }
482
483    // -- read_config_file --
484
485    #[test]
486    fn test_read_config_file_missing() {
487        let path = PathBuf::from("/nonexistent/settings.json");
488        let (config, errors): (SettingsType, _) = read_config_file(&path);
489        assert!(errors.is_empty()); // Missing file is not an error
490        assert!(config.term_theme.is_empty());
491    }
492
493    #[test]
494    fn test_read_config_file_with_comments() {
495        let dir = std::env::temp_dir().join("agentmux_test_jsonc");
496        std::fs::create_dir_all(&dir).unwrap();
497        let path = dir.join("settings_comments.json");
498        std::fs::write(
499            &path,
500            r#"
501// Terminal settings
502{
503    "term:fontsize": 16.0,
504    /* theme */
505    "term:theme": "dracula" // inline comment
506}
507"#,
508        )
509        .unwrap();
510
511        let (config, errors): (SettingsType, _) = read_config_file(&path);
512        assert!(errors.is_empty(), "errors: {:?}", errors);
513        assert_eq!(config.term_font_size, 16.0);
514        assert_eq!(config.term_theme, "dracula");
515
516        std::fs::remove_dir_all(&dir).ok();
517    }
518
519    #[test]
520    fn test_read_config_file_trailing_commas() {
521        let dir = std::env::temp_dir().join("agentmux_test_trailing");
522        std::fs::create_dir_all(&dir).unwrap();
523        let path = dir.join("settings_trailing.json");
524        std::fs::write(
525            &path,
526            r#"{
527    // "term:fontsize": 12,
528     "window:opacity": 0.7,
529    // "window:bgcolor": ""
530}"#,
531        )
532        .unwrap();
533
534        let (config, errors): (SettingsType, _) = read_config_file(&path);
535        assert!(errors.is_empty(), "trailing comma should be tolerated: {:?}", errors);
536        assert_eq!(config.window_opacity, Some(0.7));
537
538        std::fs::remove_dir_all(&dir).ok();
539    }
540
541    #[test]
542    fn test_strip_trailing_commas_basic() {
543        assert_eq!(loader::strip_trailing_commas(r#"{"a": 1,}"#), r#"{"a": 1 }"#);
544        assert_eq!(loader::strip_trailing_commas(r#"[1, 2,]"#), r#"[1, 2 ]"#);
545        assert_eq!(loader::strip_trailing_commas(r#"{"a": "b,}"}"#), r#"{"a": "b,}"}"#);
546        assert_eq!(loader::strip_trailing_commas(r#"{"a": 1, "b": 2}"#), r#"{"a": 1, "b": 2}"#);
547    }
548
549    // -- merge_into_template --
550
551    #[test]
552    fn test_merge_into_template_empty_settings() {
553        let template = "// header\n{\n    // \"foo:bar\": 1,\n}\n";
554        let settings = serde_json::Map::new();
555        let result = merge_into_template(template, &settings);
556        assert_eq!(result, template);
557    }
558
559    #[test]
560    fn test_merge_into_template_uncomments_known_key() {
561        let template = "{\n    // \"window:transparent\":       false,\n}\n";
562        let mut settings = serde_json::Map::new();
563        settings.insert("window:transparent".to_string(), serde_json::Value::Bool(true));
564        let result = merge_into_template(template, &settings);
565        assert!(result.contains("    \"window:transparent\": true,"));
566        assert!(!result.contains("//"));
567    }
568
569    #[test]
570    fn test_merge_into_template_appends_unknown_key() {
571        let template = "{\n    // \"term:fontsize\":            12,\n}\n";
572        let mut settings = serde_json::Map::new();
573        settings.insert(
574            "widget:order".to_string(),
575            serde_json::json!(["agent", "settings"]),
576        );
577        let result = merge_into_template(template, &settings);
578        assert!(result.contains("// -- User Overrides --"));
579        assert!(result.contains("\"widget:order\": [\"agent\",\"settings\"]"));
580        // Template line should still be commented
581        assert!(result.contains("// \"term:fontsize\""));
582    }
583
584    #[test]
585    fn test_merge_into_template_mixed() {
586        let template = "{\n    // \"window:blur\":              false,\n    // \"term:fontsize\":            12,\n}\n";
587        let mut settings = serde_json::Map::new();
588        settings.insert("window:blur".to_string(), serde_json::Value::Bool(true));
589        settings.insert("custom:key".to_string(), serde_json::json!("hello"));
590        let result = merge_into_template(template, &settings);
591        // Known key uncommented
592        assert!(result.contains("    \"window:blur\": true,"));
593        // Other known key still commented
594        assert!(result.contains("// \"term:fontsize\""));
595        // Unknown key appended
596        assert!(result.contains("\"custom:key\": \"hello\""));
597    }
598
599    #[test]
600    fn test_merge_into_template_idempotent() {
601        let template = SETTINGS_TEMPLATE;
602        let mut settings = serde_json::Map::new();
603        settings.insert("window:transparent".to_string(), serde_json::Value::Bool(true));
604        settings.insert("widget:order".to_string(), serde_json::json!(["a", "b"]));
605
606        let first = merge_into_template(template, &settings);
607        // Parse the result back and merge again
608        let parsed = parse_jsonc_to_map(&first);
609        let second = merge_into_template(template, &parsed);
610        assert_eq!(first, second, "merge_into_template should be idempotent");
611    }
612
613    /// `pane:colors` (SPEC_WIDGET_DEFAULT_PANE_COLORS_2026_10_05.md §3.4) has no
614    /// typed field: it rides in `extra`. A `null` entry means "no color for this
615    /// widget", unlike a missing key, so it must survive the write to disk and
616    /// the read back.
617    #[test]
618    fn test_pane_colors_round_trip_keeps_null_entries() {
619        let colors = serde_json::json!({"term": 120, "agent": null});
620        let mut settings = serde_json::Map::new();
621        settings.insert("pane:colors".to_string(), colors.clone());
622
623        let written = merge_into_template(SETTINGS_TEMPLATE, &settings);
624        assert!(!written.contains("// -- User Overrides --"), "written in place, on the template's line");
625        let read_back = parse_jsonc_to_map(&written);
626        assert_eq!(read_back.get("pane:colors"), Some(&colors));
627
628        let typed: SettingsType = serde_json::from_value(serde_json::Value::Object(read_back)).unwrap();
629        assert_eq!(serde_json::to_value(&typed).unwrap()["pane:colors"], colors);
630    }
631
632    #[test]
633    fn test_merge_into_template_preserves_indentation() {
634        let template = "{\n        // \"deep:key\":   42,\n}\n";
635        let mut settings = serde_json::Map::new();
636        settings.insert("deep:key".to_string(), serde_json::json!(99));
637        let result = merge_into_template(template, &settings);
638        assert!(result.contains("        \"deep:key\": 99,"));
639    }
640
641    #[test]
642    fn test_parse_jsonc_to_map() {
643        let content = r#"// comment
644{
645    // "commented": true,
646    "active": 42,
647    "name": "test",
648}
649"#;
650        let map = parse_jsonc_to_map(content);
651        assert_eq!(map.get("active"), Some(&serde_json::json!(42)));
652        assert_eq!(map.get("name"), Some(&serde_json::json!("test")));
653        assert!(map.get("commented").is_none());
654    }
655}