{"record":{"id":"a9a06863eeec45e7","repo":"astrid-runtime/astrid","slug":"empty-page-deserializes","errorCode":null,"errorMessage":"empty page deserializes","messagePattern":"empty page deserializes","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-gateway/src/routes/sessions_tests.rs","lineNumber":697,"sourceCode":"    let parsed = parse_search_response(body).expect(\"frozen search shape deserializes\");\n    assert_eq!(parsed.results.len(), 2);\n    assert_eq!(parsed.next_cursor.as_deref(), Some(\"page-2\"));\n    let first = &parsed.results[0];\n    assert_eq!(first.session_id, \"s1\");\n    assert_eq!(first.title.as_deref(), Some(\"Trip planning\"));\n    assert_eq!(first.match_count, 2);\n    assert_eq!(first.updated_at, Some(1_719_000_000));\n    let second = &parsed.results[1];\n    assert!(second.title.is_none());\n    assert!(second.snippet.is_none());\n    assert!(second.updated_at.is_none());\n    assert_eq!(second.match_count, 1);\n}\n\n#[test]\nfn parse_search_response_null_next_cursor_is_last_page() {\n    let body = serde_json::json!({ \"results\": [], \"next_cursor\": null });\n    let parsed = parse_search_response(body).expect(\"empty page deserializes\");\n    assert!(parsed.results.is_empty());\n    assert!(parsed.next_cursor.is_none());\n    // Absent next_cursor is also fine (defaults to None).\n    let body = serde_json::json!({ \"results\": [] });\n    assert!(parse_search_response(body).unwrap().next_cursor.is_none());\n}\n\n#[test]\nfn parse_search_response_rejects_garbage() {\n    let body = serde_json::json!({ \"results\": \"not-an-array\" });\n    assert!(matches!(\n        parse_search_response(body).unwrap_err(),\n        GatewayError::Kernel(_)\n    ));\n}\n\n/// Live round-trip over a real `EventBus` for the `update` verb: the\n/// stand-in capsule receives the principal-stamped, present-keys-only","sourceCodeStart":679,"sourceCodeEnd":715,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/routes/sessions_tests.rs#L679-L715","documentation":"This panic comes from `.expect(\"empty page deserializes\")` in the test `parse_search_response_null_next_cursor_is_last_page`. The library helper `parse_search_response` returns an Option and yielded None for a JSON body of `{\"results\": [], \"next_cursor\": null}`, meaning the page failed to deserialize into the search-response shape. The test exists precisely to guarantee that an empty last page with a null cursor is accepted.","triggerScenarios":"Calling `parse_search_response` with a body whose shape does not match the expected struct — e.g. results missing, wrong JSON type for `results`, or `next_cursor` of an unexpected type — so the deserialization path returns None instead of Some(page).","commonSituations":"The session-search API changed its response envelope (field renamed from `results`, cursor renamed); a serializer emits `next_cursor: null` on the last page and an older parser rejects it; hand-rolled JSON in tests drifts from the real payload schema.","solutions":["Inspect the body passed to parse_search_response and confirm it contains a `results` array (possibly empty) and an optional `next_cursor`.","Update parse_search_response so a null `next_cursor` maps to None instead of failing deserialization.","Add a serde-level unit test on the response struct with `#[serde(default)]`/Option fields to lock the empty-page contract.","Regenerate or refresh any client types from the current OpenAPI/schema if the gateway contract changed."],"exampleFix":"// before\nlet parsed = parse_search_response(body).expect(\"empty page deserializes\");\n// after\nlet Some(parsed) = parse_search_response(body) else {\n    eprintln!(\"body did not match search-response schema: {body}\");\n    return;\n};","handlingStrategy":"validation","validationCode":"if body.get(\"results\").and_then(|r| r.as_array()).is_none() {\n    return Err(\"search response missing results array\");\n}","typeGuard":"fn is_valid_search_body(v: &serde_json::Value) -> bool {\n    v.get(\"results\").map_or(false, |r| r.is_array())\n}","tryCatchPattern":"let Some(parsed) = parse_search_response(body) else {\n    eprintln!(\"unparseable search response: {body}\");\n    return Err(ParseError::Schema);\n};","preventionTips":["Model the response with serde Option/default fields so null cursors parse","Lock the empty-page contract with a dedicated unit test","Validate payloads against the schema in CI before parser changes ship"],"tags":["rust","test","panic","deserialization"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}