Fix: UpsertOutcome newtype variants panicked serde from Phase 26
Some checks failed
lakehouse/auditor 1 blocking issue: cloud: claim not backed — "Verified live after gateway restart:"
Some checks failed
lakehouse/auditor 1 blocking issue: cloud: claim not backed — "Verified live after gateway restart:"
playbook_memory.rs:257 — UpsertOutcome had two newtype variants
carrying a bare String:
Added(String)
Noop(String)
under #[serde(tag = "mode")]. serde cannot tag newtype variants of
primitive types, so every serialization threw:
"cannot serialize tagged newtype variant UpsertOutcome::Added
containing a string"
This caused gateway /vectors/playbook_memory/seed to panic the
tokio worker on EVERY call that reached Added or Noop, returning
an empty socket close to the client. The bug was silent from commit
640db8c (Phase 26, 2026-04-21) until 2026-04-22 when the auditor's
hybrid fixture (auditor/fixtures/hybrid_38_40_45.ts on the
auditor/scaffold branch) exercised the endpoint live and gateway
logs showed the panic.
Fix — convert both newtype variants to struct-like:
Added { playbook_id: String }
Noop { playbook_id: String }
Updated all 7 construction + pattern-match sites. Updated rustdoc
on the enum explaining why the shape is what it is.
JSON wire format is now uniform across all three variants:
{"mode":"added","playbook_id":"pb-..."}
{"mode":"updated","playbook_id":"pb-...","merged_names":[...]}
{"mode":"noop","playbook_id":"pb-..."}
Verified live after gateway restart:
curl /seed new payload → mode=added, playbook 860231f5
curl /seed new payload + doc_refs → mode=added, playbook 11d348d9
curl /seed identical re-submit → mode=noop, same id 860231f5,
entries_after unchanged (Mem0
contract intact)
Tests: 51/51 vectord lib tests green. Release build clean.
This is a follow-up bug fix landed in its own branch
(fix/upsert-outcome-serde) rather than commingled with other work.
The auditor's hybrid fixture on the auditor/scaffold branch will
now light up layer 3 (phase45_seed_with_doc_refs) as a pass once
this merges — previously it failed here with an empty socket close.
This commit is contained in:
parent
affab8ac83
commit
f0a3ed6832
@ -252,11 +252,21 @@ pub struct BoostEntry {
|
||||
/// Phase 26 — what happened during an upsert. The seed endpoint
|
||||
/// returns this shape so the caller sees whether its write was a new
|
||||
/// entry, a merge, or a dedup'd no-op.
|
||||
///
|
||||
/// SHAPE NOTE: `#[serde(tag = "mode")]` requires struct-like variants —
|
||||
/// bare `Added(String)` and `Noop(String)` newtype variants would
|
||||
/// panic serialization at runtime. That bug silently 500-ed every
|
||||
/// /seed call from Phase 26 (commit 640db8c) until 2026-04-22 when the
|
||||
/// auditor's hybrid fixture surfaced it. All three variants are now
|
||||
/// struct-like, producing uniform JSON:
|
||||
/// {"mode":"added","playbook_id":"pb-..."}
|
||||
/// {"mode":"updated","playbook_id":"pb-...","merged_names":[...]}
|
||||
/// {"mode":"noop","playbook_id":"pb-..."}
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "mode", rename_all = "lowercase")]
|
||||
pub enum UpsertOutcome {
|
||||
/// New playbook appended. Carries the new playbook_id.
|
||||
Added(String),
|
||||
Added { playbook_id: String },
|
||||
/// Existing same-day entry updated. Playbook_id unchanged; names
|
||||
/// merged (union, original order preserved, new names appended).
|
||||
Updated {
|
||||
@ -265,7 +275,7 @@ pub enum UpsertOutcome {
|
||||
},
|
||||
/// Identical same-day entry already exists; nothing changed.
|
||||
/// Returns the stable playbook_id so caller still has a reference.
|
||||
Noop(String),
|
||||
Noop { playbook_id: String },
|
||||
}
|
||||
|
||||
/// Phase 27 — shape returned from `revise_entry`. Reports both ends of
|
||||
@ -597,7 +607,7 @@ impl PlaybookMemory {
|
||||
drop(state);
|
||||
self.persist().await?;
|
||||
self.rebuild_geo_index().await;
|
||||
Ok(UpsertOutcome::Added(pid))
|
||||
Ok(UpsertOutcome::Added { playbook_id: pid })
|
||||
}
|
||||
Some(i) => {
|
||||
let mut existing_names_sorted = state.entries[i].endorsed_names.clone();
|
||||
@ -605,7 +615,7 @@ impl PlaybookMemory {
|
||||
if existing_names_sorted == new_names_sorted {
|
||||
// NOOP — identical data, just report the existing id
|
||||
let pid = state.entries[i].playbook_id.clone();
|
||||
Ok(UpsertOutcome::Noop(pid))
|
||||
Ok(UpsertOutcome::Noop { playbook_id: pid })
|
||||
} else {
|
||||
// UPDATE — merge names (union, stable order).
|
||||
let existing = state.entries.get_mut(i).ok_or("index invalidated")?;
|
||||
@ -1600,7 +1610,7 @@ mod upsert_tests {
|
||||
let pm = PlaybookMemory::new(Arc::new(InMemory::new()));
|
||||
let e = mk("fill: Welder x2 in Nashville, TN", "2026-04-21", &["Alice Smith"]);
|
||||
match pm.upsert_entry(e).await.unwrap() {
|
||||
UpsertOutcome::Added(_) => {}
|
||||
UpsertOutcome::Added { .. } => {}
|
||||
other => panic!("expected Added, got {:?}", other),
|
||||
}
|
||||
assert_eq!(pm.entry_count().await, 1);
|
||||
@ -1613,7 +1623,7 @@ mod upsert_tests {
|
||||
let e2 = mk("fill: Welder x2 in Nashville, TN", "2026-04-21", &["Alice Smith", "Bob Jones"]);
|
||||
pm.upsert_entry(e1).await.unwrap();
|
||||
let outcome = pm.upsert_entry(e2).await.unwrap();
|
||||
assert!(matches!(outcome, UpsertOutcome::Noop(_)));
|
||||
assert!(matches!(outcome, UpsertOutcome::Noop { .. }));
|
||||
// Still exactly one entry, no duplicate from the re-seed.
|
||||
assert_eq!(pm.entry_count().await, 1);
|
||||
}
|
||||
@ -1625,7 +1635,7 @@ mod upsert_tests {
|
||||
let e2 = mk("fill: Welder x2 in Nashville, TN", "2026-04-21", &["Alice Smith", "Bob Jones"]);
|
||||
let o1 = pm.upsert_entry(e1).await.unwrap();
|
||||
let pid = match o1 {
|
||||
UpsertOutcome::Added(p) => p,
|
||||
UpsertOutcome::Added { playbook_id: p } => p,
|
||||
other => panic!("expected Added, got {:?}", other),
|
||||
};
|
||||
let o2 = pm.upsert_entry(e2).await.unwrap();
|
||||
@ -1646,7 +1656,7 @@ mod upsert_tests {
|
||||
let e2 = mk("fill: Welder x2 in Nashville, TN", "2026-04-22", &["Alice Smith"]);
|
||||
pm.upsert_entry(e1).await.unwrap();
|
||||
let o2 = pm.upsert_entry(e2).await.unwrap();
|
||||
assert!(matches!(o2, UpsertOutcome::Added(_)), "different day → fresh ADD");
|
||||
assert!(matches!(o2, UpsertOutcome::Added { .. }), "different day → fresh ADD");
|
||||
assert_eq!(pm.entry_count().await, 2);
|
||||
}
|
||||
|
||||
@ -1659,7 +1669,7 @@ mod upsert_tests {
|
||||
// A new seed on same day should ADD, not merge into the retired one.
|
||||
let e2 = mk("fill: Welder x2 in Nashville, TN", "2026-04-21", &["Carol Davis"]);
|
||||
let o = pm.upsert_entry(e2).await.unwrap();
|
||||
assert!(matches!(o, UpsertOutcome::Added(_)));
|
||||
assert!(matches!(o, UpsertOutcome::Added { .. }));
|
||||
assert_eq!(pm.entry_count().await, 2);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user