{"record":{"id":"807c1b570f16c4a5","repo":"Hmbown/CodeWhale","slug":"legacy-route-should-parse","errorCode":null,"errorMessage":"legacy route should parse","messagePattern":"legacy route should parse","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/protocol/src/fleet.rs","lineNumber":1803,"sourceCode":"            \"worker_id\": \"worker-route\",\n            \"completed_at\": \"2026-06-23T00:00:00Z\",\n            \"result\": \"pass\",\n            \"artifacts\": [],\n            \"score\": null,\n            \"resolved_route\": {\n                \"provider_id\": \"deepseek\",\n                \"provider_kind\": \"deepseek\",\n                \"canonical_model\": \"deepseek-v4-pro\",\n                \"wire_model_id\": \"deepseek-v4-pro\",\n                \"protocol\": \"chat_completions\",\n                \"role\": \"builder\",\n                \"loadout\": \"fast\",\n                \"source\": \"resolver\"\n            }\n        }\"#;\n\n        let receipt: FleetReceipt = serde_json::from_str(legacy).unwrap();\n        let route = receipt.resolved_route.expect(\"legacy route should parse\");\n        assert_eq!(route.source, \"resolver\");\n        assert_eq!(route.role.as_deref(), Some(\"builder\"));\n        assert_eq!(route.loadout.as_deref(), Some(\"fast\"));\n        assert_eq!(route.model_class, None);\n        assert_eq!(route.model_route, None);\n        assert_eq!(route.reasoning_effort, None);\n        assert_eq!(route.role_source, None);\n        assert_eq!(route.loadout_source, None);\n        assert_eq!(route.model_class_source, None);\n        assert_eq!(route.model_source, None);\n    }\n\n    #[test]\n    fn fleet_resolved_route_serialization_carries_no_secrets() {\n        let receipt = sample_receipt_with_route();\n        // Scan the serialized resolved-route object: this is the field whose\n        // no-secrets invariant we are asserting. Scoping to the route value\n        // avoids false positives from unrelated envelope ids (e.g. a task id","sourceCodeStart":1785,"sourceCodeEnd":1821,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/protocol/src/fleet.rs#L1785-L1821","documentation":"This is a Rust test assertion panic: `Option::expect` on `receipt.resolved_route` fires when deserializing a legacy FleetReceipt JSON succeeds but the `resolved_route` field ends up `None`. The field is `#[serde(default)]` to keep pre-#3154 receipts readable, so this expect guards the newer guarantee that a receipt carrying a `resolved_route` object must actually deserialize into `Some(route)`. It fails when the route's own struct changed shape (new required fields, renamed fields) and no longer accepts the legacy JSON.","triggerScenarios":"Calling serde_json::from_str on a receipt containing a `resolved_route` object, then calling `.expect(...)` on `receipt.resolved_route`: panics only if serde silently dropped the route — typically because a required field of FleetResolvedRoute is missing/unknown in the payload or the route struct's serde attributes reject the legacy shape (e.g. deny_unknown_fields, renamed fields, a removed variant of `protocol`).","commonSituations":"Backward-compatibility tests breaking after a schema migration of the resolved-route struct; renaming or type-changing a field like `role`, `loadout`, `source`, `protocol`, or `provider_kind` so the legacy JSON no longer parses; a new #[serde(deny_unknown_fields)] attribute added to the route struct.","solutions":["Compare the route struct's current serde derives/attributes with the legacy JSON keys and make the struct tolerate the old shape (add #[serde(default)] on new fields, add #[serde(alias = \"old_name\")] on renames).","If a field was intentionally removed or renamed, update the legacy fixture JSON in the test to the new wire shape instead of changing production serde behavior.","Reproduce the inner serde error by changing the test's first from_str to `.unwrap()` (or inspecting the Result) to see which field of the route fails before fixing it."],"exampleFix":"// before\nlet receipt: FleetReceipt = serde_json::from_str(legacy).unwrap();\nlet route = receipt.resolved_route.expect(\"legacy route should parse\");\n// after — struct now defaults new fields so the legacy shape parses\n#[derive(Serialize, Deserialize)]\npub struct FleetResolvedRoute {\n    pub provider_id: String,\n    #[serde(default)]\n    pub model_route: Option<ModelRoute>,\n    // ...\n}","handlingStrategy":"type-guard","validationCode":"// validate the payload maps cleanly before trusting Option::expect\nlet receipt: Result<FleetReceipt, _> = serde_json::from_str(legacy);\nassert!(receipt.is_ok(), \"receipt failed to parse: {:?}\", receipt.err());","typeGuard":"fn resolved_route_of(receipt: &FleetReceipt) -> Option<&FleetResolvedRoute> {\n    receipt.resolved_route.as_ref()\n}","tryCatchPattern":"let route = receipt.resolved_route.unwrap_or_else(|| {\n    panic!(\"resolved_route missing; serde error context: re-parse with detailed errors enabled\")\n});","preventionTips":["Add #[serde(default)] to every newly added field of FleetResolvedRoute so old payloads keep deserializing.","Use #[serde(alias)] when renaming route fields instead of breaking old keys.","In tests, unwrap the outer from_str Result with context before expect-ing on Option fields, so serde errors are visible not swallowed."],"tags":["rust","serde","test-panic","backward-compatibility"],"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"}