{"record":{"id":"95249a2d608befab","repo":"jdx/mise","slug":"expected-dictionary-got-value","errorCode":null,"errorMessage":"expected dictionary, got {value:?}","messagePattern":"expected dictionary, got (.+?)","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/launchd.rs","lineNumber":741,"sourceCode":"            nice: None,\n            start_calendar_interval: Some(LaunchdCalendarIntervals::Single(\n                LaunchdCalendarInterval {\n                    hour: Some(2),\n                    minute: Some(0),\n                    ..Default::default()\n                },\n            )),\n            queue_directories: vec![\"~/Library/Queues/sync\".to_string()],\n            environment,\n            working_directory: Some(\"~\".to_string()),\n            stdout_path: Some(\"~/Library/Logs/sync.log\".to_string()),\n            stderr_path: Some(\"~/Library/Logs/sync.err.log\".to_string()),\n            kickstart: false,\n        };\n        let plist = render_plist(&request).unwrap();\n        let dict = match Value::from_reader_xml(Cursor::new(plist.as_slice())).unwrap() {\n            Value::Dictionary(dict) => dict,\n            value => panic!(\"expected dictionary, got {value:?}\"),\n        };\n        assert_eq!(\n            dict.get(\"Label\"),\n            Some(&Value::String(\"dev.mise.sync\".to_string()))\n        );\n        assert_eq!(dict.get(\"RunAtLoad\"), Some(&Value::Boolean(true)));\n        assert_eq!(dict.get(\"KeepAlive\"), Some(&Value::Boolean(true)));\n        assert_eq!(dict.get(\"StartInterval\"), Some(&Value::Integer(60.into())));\n        assert_eq!(\n            dict.get(\"ThrottleInterval\"),\n            Some(&Value::Integer(300.into()))\n        );\n        assert_eq!(\n            dict.get(\"QueueDirectories\"),\n            Some(&Value::Array(vec![Value::String(\n                crate::dirs::HOME\n                    .join(\"Library\")\n                    .join(\"Queues\")","sourceCodeStart":723,"sourceCodeEnd":759,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/launchd.rs#L723-L759","documentation":"A test panic in src/system/launchd.rs: after `render_plist` produces XML, the test parses it with `Value::from_reader_xml` and asserts the top-level value is a `Value::Dictionary`. Any other plist value (array, string, etc.) reaches `value => panic!(\"expected dictionary, got {value:?}\")`. It means the generated launchd plist is not a top-level dictionary as launchd requires.","triggerScenarios":"`render_plist(&request).unwrap()` producing XML whose root element is not a `<dict>` — e.g. a serializer regression, empty plist output, or the request rendering path writing a scalar/array.","commonSituations":"Changes to the plist XML writer; malformed `LaunchdRequest` fields producing invalid XML that parses to a non-dictionary; snapshot/format changes in `render_plist`.","solutions":["Print the raw plist XML from `render_plist` to inspect the root element.","Verify `render_plist` always wraps keys (Label, ProgramArguments, etc.) in a top-level `<dict>`.","Check the plist crate version/API (`Value::from_reader_xml`) for parsing behavior changes.","Validate the rendered XML with `plutil -lint` (on macOS) to catch structural issues."],"exampleFix":"// before\nlet dict = match Value::from_reader_xml(Cursor::new(plist.as_slice())).unwrap() {\n    Value::Dictionary(dict) => dict,\n    value => panic!(\"expected dictionary, got {value:?}\"),\n};\n// after\nlet value = Value::from_reader_xml(Cursor::new(plist.as_slice())).unwrap();\nlet Value::Dictionary(dict) = value else {\n    panic!(\"expected dictionary, got {value:?}; xml: {plst}\", plst = plist)\n};","handlingStrategy":"type-guard","validationCode":"// rust\nif !plist.trim_start().starts_with(\"<?xml\") || !plist.contains(\"<dict>\") {\n    return Err(\"rendered plist root is not a dictionary\".into());\n}","typeGuard":"fn as_plist_dict(v: Value) -> Option<Dictionary> {\n    match v {\n        Value::Dictionary(d) => Some(d),\n        _ => None,\n    }\n}","tryCatchPattern":"// rust\nlet value = Value::from_reader_xml(Cursor::new(plist.as_slice()))?;\nlet Value::Dictionary(dict) = value else {\n    return Err(anyhow!(\"expected dictionary, got {value:?}; xml: {plist}\"));\n};","preventionTips":["Lint generated plists (plutil -lint) in CI on macOS runners.","Always emit a top-level <dict> for launchd plists.","Snapshot render_plist output to catch format regressions.","Include raw output in error messages when parsing fails structurally."],"tags":["rust","test-assertion","plist","launchd"],"backgroundTag":"unexpected-response-shape","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}