fix(auto-improve): drop model-supplied expected_base_body_sha256

The field is server-owned and set when a patch is materialized, but the
schema shows it to the model. A non-hash value on a full-page proposal
reached staging and failed hex_to_sha256, aborting the whole run with a
500. Clear it during normalisation.
This commit is contained in:
Francisco Anghinoni
2026-09-29 14:48:16 -04:00
parent 33d6848376
commit 284a8e3a55
2 changed files with 36 additions and 0 deletions
+8
View File
@@ -18,6 +18,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`["full_page", "patch"]`, and normalisation folds `full`, `full-page` and
`Full Page` into `full_page` (with a warning) for providers without
constrained decoding. Unknown modes still fail validation.
- Auto-improve no longer aborts a whole run when the model fills in
`expected_base_body_sha256`. The field is computed by the server when a
patch is materialized, but the schema shows it to the model, and a
non-hash value on a full-page proposal reached staging and failed
`hex_to_sha256` with `invalid expected_base_body_sha256: expected 64 hex
chars` (HTTP 500 from `/admin/auto-improve`), discarding every other
proposal in the run. Normalisation now drops any model-supplied value; the
patch path still sets it from the materialized target.
## [2.4.2] - 2026-09-29
@@ -1511,6 +1511,11 @@ pub(crate) fn validate_response(
fn normalize_proposal(proposal: &mut AutoImproveProposal, warnings: &mut Vec<String>) {
normalize_kind(proposal, warnings);
// Server-owned: set when a patch is materialized. The schema still shows
// it to the model, and a model-supplied non-hash value on a full-page
// proposal would reach staging and fail the whole run on `hex_to_sha256`.
proposal.expected_base_body_sha256 = None;
let original_edit_mode = proposal.edit_mode.clone();
proposal.edit_mode = normalize_edit_mode(&original_edit_mode);
if !original_edit_mode.trim().is_empty() && proposal.edit_mode != original_edit_mode {
@@ -3331,6 +3336,29 @@ mod tests {
assert_eq!(rejected[0].reason, "unsupported_edit_mode");
}
/// Once `"full"` stopped being rejected, the same `gpt-oss-20b` runs
/// failed at staging with `invalid expected_base_body_sha256: expected 64
/// hex chars`: the model filled in a field only the server can compute.
#[test]
fn a_model_supplied_base_sha_is_dropped_from_full_page_proposals() {
for supplied in ["", "null", "abc123", "N/A"] {
let mut candidate = proposal("gotchas/thing.md", "gotcha", 0.91);
candidate.expected_base_body_sha256 = Some(supplied.into());
let raw = AutoImproveLlmResponse {
summary: "ok".into(),
proposals: vec![candidate],
rejected_candidates: Vec::new(),
};
let (accepted, rejected, _) =
validate_response(raw, &cfg(), &ExistingPageIndex::default());
assert!(rejected.is_empty(), "{supplied:?}: got {rejected:?}");
assert_eq!(
accepted[0].expected_base_body_sha256, None,
"{supplied:?} must not survive to staging"
);
}
}
#[test]
fn patch_to_missing_or_non_context_target_rejects() {
let raw = AutoImproveLlmResponse {