fix: Preserve manual oneliner across delete and reset paths

Consolidate the sticky-Manual rule for oneliner.json into a single
helper paths::delete_oneliner_unless_manual that holds the per-case
lock, reads the state, keeps OnelinerState::Manual, and removes any
LLM-derived variant. Three previously-direct deletion paths now route
through it: handle_delete_recording, reset_case_artefacts (admin
single-reset), and bulk_reset (admin bulk action). Before this fix a
clinician's manual override was wiped by a recording delete, letting
the LLM auto-regen path overwrite it on the next view.

Tests: 4 unit tests for the wrapper contract, integration tests for
the recording-delete and case-reset endpoints (Manual preserved,
Ready wiped), and a static-scan invariant that fails on direct
remove_file(... ONELINER_FILENAME) calls outside paths.rs.
This commit is contained in:
2026-04-26 22:09:08 +02:00
parent eaed8f0dcd
commit 9a00b592f0
6 changed files with 421 additions and 13 deletions
+7 -2
View File
@@ -14,6 +14,7 @@ use crate::config::Config;
use crate::csrf::{CsrfForm, HasCsrfToken};
use crate::error::AppError;
use crate::events::{self, CaseEventKind, EventSender};
use crate::oneliner_locks::OnelinerLocks;
use crate::paths::{CloseMarker, write_close_marker};
use crate::routes::case_actions::{
build_analysis_input, reset_case_artefacts, write_input_create_new,
@@ -45,6 +46,7 @@ pub async fn handle_bulk_action(
State(config): State<Arc<Config>>,
State(analyze_tx): State<AnalyzeSender>,
State(events_tx): State<EventSender>,
State(locks): State<OnelinerLocks>,
CsrfForm(form): CsrfForm<BulkForm>,
) -> Result<Redirect, AppError> {
// Bulk actions are admin-only across the board: the UI hides the
@@ -78,7 +80,9 @@ pub async fn handle_bulk_action(
.await
}
BulkAction::Close => bulk_close(&events_tx, &user.slug, &user_root, &form.case_ids).await,
BulkAction::Reset => bulk_reset(&events_tx, &user.slug, &user_root, &form.case_ids).await,
BulkAction::Reset => {
bulk_reset(&events_tx, &locks, &user.slug, &user_root, &form.case_ids).await
}
}
Ok(Redirect::to("/web/cases"))
@@ -185,6 +189,7 @@ async fn bulk_close(
async fn bulk_reset(
events_tx: &EventSender,
locks: &OnelinerLocks,
slug: &str,
user_root: &std::path::Path,
case_ids: &[String],
@@ -202,7 +207,7 @@ async fn bulk_reset(
skipped += 1;
continue;
};
if let Err(e) = reset_case_artefacts(&case_dir).await {
if let Err(e) = reset_case_artefacts(&case_dir, locks).await {
warn!(slug = %slug, case_id = %case_id, error = %e, "bulk-reset: failed");
skipped += 1;
continue;