Watch
0
0
Fork
You've already forked hyperhive
0

review classifier: keep stored runs this build can't decode

A run record this build couldn't decode was dropped from the list it
read, and the next recorded run wrote the list back without it, so a
rollback followed by a roll-forward lost the newer build's runs for
good. The stored list is now a list of entries, each a decoded run or
the raw JSON of one that didn't decode. The route still shows only the
decoded runs. An undecodable entry keeps its place, counts toward the
100, and is written back as it was read. A read that finds any logs one
WARN with their count and the first error.

The settings decode derived its known fields from what
`Settings::default()` serializes, so a later field skipped when unset
would have been stripped from a valid stored document and lost on the
next PUT. The known fields are now an explicit list. A test serializes
a fully populated `Settings`, checks that its keys match the list, and
checks that `decode` round-trips it.
This commit is contained in:
atlas 2026-10-13 02:54:07 +02:00 • committed by mara
commit daaf920e06
3 changed files with 132 additions and 46 deletions

View file

@ -102,16 +102,18 @@ async fn stored(store: &Store) -> anyhow::Result<(Settings, Option<u64>)> {
read_with(store, KEY, |bytes| Ok(decode(bytes))).await
}
/// Every field of [`Settings`]. `a_full_document_round_trips` fails when one
/// is missing.
const FIELDS: &[&str] = &["model", "effort", "max_concurrent_reviews"];
/// `bytes`, the stored document, as settings. Unlike a `PUT` body, a field
/// this build does not know, such as one a newer build added, is ignored;
/// a document that still does not decode is [`Settings::default`], with a
/// warning, until the next `PUT` replaces it. The known fields are those
/// [`Settings::default`] encodes, which is every field.
/// not in [`FIELDS`], such as one a newer build added, is ignored; a document
/// that still does not decode is [`Settings::default`], with a warning,
/// until the next `PUT` replaces it.
fn decode(bytes: &[u8]) -> Settings {
let decoded = serde_json::from_slice(bytes).and_then(|mut value: serde_json::Value| {
let known = serde_json::to_value(Settings::default())?;
if let (Some(fields), Some(known)) = (value.as_object_mut(), known.as_object()) {
fields.retain(|name, _| known.contains_key(name));
if let Some(fields) = value.as_object_mut() {
fields.retain(|name, _| FIELDS.contains(&name.as_str()));
}
serde_json::from_value(value)
});
@ -249,6 +251,24 @@ mod tests {
);
}
/// Every field set, every `Option` `Some`: a stored document keeps all of
/// them, so a field missing from [`FIELDS`] fails here.
#[test]
fn a_full_document_round_trips() {
let full = Settings {
model: "sonnet".to_owned(),
effort: Some(Effort::High),
max_concurrent_reviews: 5,
};
let encoded = serde_json::to_value(&full).unwrap();
let mut keys: Vec<_> = encoded.as_object().unwrap().keys().collect();
let mut fields = FIELDS.to_vec();
keys.sort_unstable();
fields.sort_unstable();
assert_eq!(keys, fields);
assert_eq!(decode(&serde_json::to_vec(&full).unwrap()), full);
}
#[test]
fn bad_settings_are_refused() {
for json in [