{"record":{"id":"8c738d381872a5d1","repo":"Hmbown/CodeWhale","slug":"effective-permissions-should-round-trip","errorCode":null,"errorMessage":"effective permissions should round-trip","messagePattern":"effective permissions should round-trip","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/protocol/src/fleet.rs","lineNumber":1744,"sourceCode":"        assert_eq!(route.wire_model_id, \"deepseek-v4-pro\");\n        assert_eq!(route.protocol, \"chat_completions\");\n        assert_eq!(route.role.as_deref(), Some(\"builder\"));\n        assert_eq!(route.loadout.as_deref(), Some(\"auto\"));\n        assert_eq!(route.model_class.as_deref(), Some(\"balanced\"));\n        assert_eq!(route.model_route.as_deref(), Some(\"auto\"));\n        assert_eq!(route.reasoning_effort.as_deref(), Some(\"high\"));\n        assert_eq!(route.role_source.as_deref(), Some(\"task.role\"));\n        assert_eq!(route.loadout_source.as_deref(), Some(\"task.loadout\"));\n        assert_eq!(\n            route.model_class_source.as_deref(),\n            Some(\"task.model_class\")\n        );\n        assert_eq!(route.model_source.as_deref(), Some(\"task.model\"));\n        assert_eq!(route.source, \"resolver\");\n\n        let permissions = back\n            .effective_permissions\n            .expect(\"effective permissions should round-trip\");\n        assert!(permissions.write);\n        assert!(permissions.network);\n        assert_eq!(permissions.shell, \"full\");\n        assert_eq!(permissions.tool_scope, \"explicit\");\n        assert_eq!(\n            permissions.tools,\n            vec![\"read_file\".to_string(), \"apply_patch\".to_string()]\n        );\n        assert!(permissions.background);\n        assert_eq!(permissions.max_spawn_depth, 2);\n        assert_eq!(permissions.profile_id.as_deref(), Some(\"builder\"));\n        assert_eq!(permissions.profile_origin.as_deref(), Some(\"built_in\"));\n        assert_eq!(permissions.source, \"worker_runtime_profile\");\n    }\n\n    #[test]\n    fn fleet_receipt_without_resolved_route_still_deserializes() {\n        // An old ledger receipt JSON written before #3154 has no","sourceCodeStart":1726,"sourceCodeEnd":1762,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/protocol/src/fleet.rs#L1726-L1762","documentation":"This is the panic from `back.effective_permissions.expect(\"effective permissions should round-trip\")` in the `fleet_resolved_route_round_trips` test at crates/protocol/src/fleet.rs:1744. `FleetReceipt.effective_permissions` is an `Option<FleetEffectivePermissions>` describing the worker's actual runtime permissions (write, network, shell scope, tool list, spawn depth, profile). The test serializes a receipt that has `effective_permissions: Some(...)`, deserializes it back, and unwraps the option, asserting the permissions survived the JSON round trip. It panics when the deserialized receipt's `effective_permissions` is `None`, meaning serde dropped or failed to restore the field — typically a missing/incorrect `#[serde(default)]`/rename on `effective_permissions`, a field name mismatch in `FleetEffectivePermissions`, or the fixture accidentally producing a receipt without permissions.","triggerScenarios":"The panic occurs when `FleetReceipt::effective_permissions` deserializes to `None` despite being `Some` before serialization: (1) the field or struct lost/misspelled its serde attributes so the key is written under a different name and silently ignored on read; (2) `FleetEffectivePermissions` gained a required (non-default) field that the serializer omits, failing that branch — combined with `#[serde(default)]` on the outer Option this yields `None` instead of an error; (3) a test fixture builds the sample receipt with `effective_permissions: None` after an edit; (4) new fields in `FleetEffectivePermissions` use types that don't match their JSON representation so deserialization of the struct fails and a lenient `default` masks it as `None`.","commonSituations":"A developer adds a permission knob (e.g. a new tool-scope field) to `FleetEffectivePermissions` or renames one of its fields, runs `cargo test -p codewhale-protocol fleet_resolved_route_round_trips`, and the expect at line 1744 panics because the round-tripped option came back `None`. Also hit when serde attribute refactors accidentally apply `skip_serializing_if` to `effective_permissions` without a matching default path, or when the fixture helper `sample_receipt_with_route` is edited to drop the permissions.","solutions":["Check the deserialized receipt directly (print or assert before unwrapping) to confirm whether the JSON even contains the `effective_permissions` key — if absent, the serialization side (skip attribute, renamed key) is at fault; if present, the struct deserialization side is.","Verify `FleetReceipt.effective_permissions` and `FleetEffectivePermissions` still derive `Serialize`/`Deserialize` with matching field names, and that every field of `FleetEffectivePermissions` is JSON-compatible with its declared type (`shell`/`tool_scope`/`source` as `String`, `tools` as `Vec<String>`, `profile_id`/`profile_origin` as `Option<String>`).","If the fixture regressed, restore `effective_permissions: Some(FleetEffectivePermissions { ... })` in `sample_receipt_with_route` as shown at fleet.rs:1702-1713.","If a newly added permission field breaks parsing, annotate it `#[serde(default)]` (with `skip_serializing_if` for Options) so old receipts — like the pre-#3154 legacy ledger JSON covered by `fleet_receipt_without_resolved_route_still_deserializes` — still deserialize.","Re-run: `cargo test -p codewhale-protocol fleet_resolved_route_round_trips`."],"exampleFix":"// before\n#[serde(default, skip_serializing_if = \"Option::is_none\")]\npub effective_permissions: Option<FleetEffectivePermissions>,\n\npub struct FleetEffectivePermissions {\n    pub tool_scope: ToolScope, // renamed type/field: JSON key no longer matches, parse fails => None\n}\n// after\n#[serde(default, skip_serializing_if = \"Option::is_none\")]\npub effective_permissions: Option<FleetEffectivePermissions>,\n\npub struct FleetEffectivePermissions {\n    pub tool_scope: String, // or #[serde(alias = \"old_name\")] on the renamed field\n}","handlingStrategy":"try-catch","validationCode":"// Confirm a receipt round-trips its permissions before unwrapping\nfn permissions_preserved(before: &FleetReceipt, after: &FleetReceipt) -> bool {\n    after.effective_permissions == before.effective_permissions\n}\n// use: assert!(permissions_preserved(&receipt, &back));","typeGuard":"fn effective_permissions(r: &FleetReceipt) -> Option<&FleetEffectivePermissions> {\n    r.effective_permissions.as_ref()\n}\n// use: if let Some(perm) = effective_permissions(&back) { ... } else { /* handle missing */ }","tryCatchPattern":"let Some(permissions) = back.effective_permissions.as_ref() else {\n    panic!(\"effective_permissions lost in round trip: json={} \", serde_json::to_string(&back).unwrap_or_default());\n};","preventionTips":["When adding fields to FleetEffectivePermissions, give each #[serde(default)] so both new and legacy receipts parse; the outer Option must never silently become None.","Mirror field names/types exactly on the struct and the JSON the fixture produces (String vs enum matters).","Never drop effective_permissions from the sample fixture when editing sample_receipt_with_route.","Check round-tripped Options for Some before assert_eq so a serde regression surfaces with context, not a bare expect panic.","Run `cargo test -p codewhale-protocol fleet_resolved_route_round_trips` after any serde/permissions change."],"tags":["rust","serde","option-unwrapping","round-trip","fleet-permissions"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}