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:
parent
6f0a72b246
commit
daaf920e06
3 changed files with 132 additions and 46 deletions
|
|
@ -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 [
|
||||
|
|
|
|||
Loading…
Reference in a new issue