Hmbown/CodeWhale · warning
legacy route should parse
Error message
legacy route should parse
What it means
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.
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.
Example fix
// before
let receipt: FleetReceipt = serde_json::from_str(legacy).unwrap();
let route = receipt.resolved_route.expect("legacy route should parse");
// after — struct now defaults new fields so the legacy shape parses
#[derive(Serialize, Deserialize)]
pub struct FleetResolvedRoute {
pub provider_id: String,
#[serde(default)]
pub model_route: Option<ModelRoute>,
// ...
} Defensive patterns
Strategy: type-guard
Validate before calling
// validate the payload maps cleanly before trusting Option::expect
let receipt: Result<FleetReceipt, _> = serde_json::from_str(legacy);
assert!(receipt.is_ok(), "receipt failed to parse: {:?}", receipt.err()); Type guard
fn resolved_route_of(receipt: &FleetReceipt) -> Option<&FleetResolvedRoute> {
receipt.resolved_route.as_ref()
} Try / catch
let route = receipt.resolved_route.unwrap_or_else(|| {
panic!("resolved_route missing; serde error context: re-parse with detailed errors enabled")
}); Prevention
- 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.
When it happens
Trigger: 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`).
Common situations: 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.
Understand the failure class
Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.
Related errors
- continue_goal with a wire-supplied schedule id still parses
- serialize
- cached read tool executes
- deserialize
- effective permissions should round-trip
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/807c1b570f16c4a5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/protocol/src/fleet.rs:1803
"worker_id": "worker-route",
"completed_at": "2026-06-23T00:00:00Z",
"result": "pass",
"artifacts": [],
"score": null,
"resolved_route": {
"provider_id": "deepseek",
"provider_kind": "deepseek",
"canonical_model": "deepseek-v4-pro",
"wire_model_id": "deepseek-v4-pro",
"protocol": "chat_completions",
"role": "builder",
"loadout": "fast",
"source": "resolver"
}
}"#;
let receipt: FleetReceipt = serde_json::from_str(legacy).unwrap();
let route = receipt.resolved_route.expect("legacy route should parse");
assert_eq!(route.source, "resolver");
assert_eq!(route.role.as_deref(), Some("builder"));
assert_eq!(route.loadout.as_deref(), Some("fast"));
assert_eq!(route.model_class, None);
assert_eq!(route.model_route, None);
assert_eq!(route.reasoning_effort, None);
assert_eq!(route.role_source, None);
assert_eq!(route.loadout_source, None);
assert_eq!(route.model_class_source, None);
assert_eq!(route.model_source, None);
}
#[test]
fn fleet_resolved_route_serialization_carries_no_secrets() {
let receipt = sample_receipt_with_route();
// Scan the serialized resolved-route object: this is the field whose
// no-secrets invariant we are asserting. Scoping to the route value
// avoids false positives from unrelated envelope ids (e.g. a task idView on GitHub (pinned to 433685b202)