//! Admin-only case reset endpoint: POST /cases/{case_id}/reset. //! //! Verifies the sticky-Manual rule for the reset path: a clinician-set //! `OnelinerState::Manual` must survive an admin-triggered reset, while //! all LLM-derived oneliner states (`Ready`, `Empty`, `Error`) are //! wiped as before. The wider „transcripts removed, .m4a.failed //! re-armed" contract is covered in `analyze_test.rs`. mod common; use axum::http::{StatusCode, header}; use doctate_common::oneliners::OnelinerState; use tower::util::ServiceExt; use common::{ ONELINER_FILENAME, TestConfig, csrf_form_post, login_with_csrf, paths, seed_case, seed_oneliner_ready, seed_oneliner_state, seed_recording, test_admin, }; fn cfg_admin(label: &'static str) -> std::sync::Arc { TestConfig::new() .with_label(label) .with_user(test_admin("dr_a")) .build() } #[tokio::test] async fn reset_case_preserves_manual_oneliner_override() { // Bug regression: an admin reset must NOT destroy a clinician's // manual oneliner override. The Manual variant is sticky for the // lifetime of the case. let cfg = cfg_admin("reset-manual"); let case_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; let dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording(&dir, "2026-04-26T10-00-00Z", Some("Knie links")); let manual_text = "55 J., Knieschmerz links — manuell vom Arzt"; seed_oneliner_state( &dir, &OnelinerState::Manual { text: manual_text.into(), set_at: "2026-04-26T12:00:00Z".into(), }, ); let (app, store) = doctate_server::create_router_and_session_store(cfg); let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await; let resp = app .oneshot(csrf_form_post( paths::case_reset(case_id), &cookie, &csrf, "", )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::SEE_OTHER); assert_eq!( resp.headers().get(header::LOCATION).unwrap(), &format!("/cases/{case_id}") ); // Recording metadata sidecar was wiped (the rest of the reset // semantics still works). assert!(!dir.join("2026-04-26T10-00-00Z.json").exists()); // …but the Manual override is untouched. let bytes = std::fs::read(dir.join(ONELINER_FILENAME)) .expect("oneliner.json must still exist after reset"); let on_disk: OnelinerState = serde_json::from_slice(&bytes).expect("oneliner.json must still be valid JSON"); match on_disk { OnelinerState::Manual { text, .. } => { assert_eq!(text, manual_text, "manual text must be byte-identical"); } other => panic!("expected Manual after reset, got {other:?}"), } } #[tokio::test] async fn reset_case_removes_auto_oneliner() { // Positive counterpart: a `Ready` oneliner (LLM-derived auto state) // must still be wiped by reset — otherwise the wrapper would be // protecting too much. let cfg = cfg_admin("reset-auto"); let case_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; let dir = seed_case(&cfg.data_path, "dr_a", case_id); seed_recording(&dir, "2026-04-26T10-00-00Z", Some("Knie links")); seed_oneliner_ready(&dir, "knee, left, pain"); let (app, store) = doctate_server::create_router_and_session_store(cfg); let (cookie, csrf) = login_with_csrf(&app, &store, "dr_a", "s").await; let resp = app .oneshot(csrf_form_post( paths::case_reset(case_id), &cookie, &csrf, "", )) .await .unwrap(); assert_eq!(resp.status(), StatusCode::SEE_OTHER); assert!( !dir.join(ONELINER_FILENAME).exists(), "Ready oneliner must be wiped by reset" ); }