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

  1. 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).
  2. 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.
  3. 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

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


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 id

View on GitHub (pinned to 433685b202)