refactor: rename soft-delete marker from .deleted to .closed
Internal rename: DELETE_MARKER -> CLOSE_MARKER, DeleteMarker -> CloseMarker, is_deleted -> is_closed. The batch UUID is dropped; undo now groups cases by their exact closed_at timestamp, which bulk-close writes identically across its selection while per-case close writes a unique stamp. Behavior unchanged. No migration (project is pre-production); legacy .deleted files in tmpdata were removed manually.
This commit is contained in:
@@ -628,14 +628,13 @@ async fn delete_writes_marker_and_redirects() {
|
||||
.unwrap(),
|
||||
"/web/cases"
|
||||
);
|
||||
let marker = case_dir.join(".deleted");
|
||||
assert!(marker.exists(), ".deleted marker missing");
|
||||
let marker = case_dir.join(".closed");
|
||||
assert!(marker.exists(), ".closed marker missing");
|
||||
let raw = std::fs::read_to_string(&marker).unwrap();
|
||||
let parsed: Value = serde_json::from_str(&raw).unwrap();
|
||||
assert!(parsed["batch"].is_string(), "batch must be a UUID string");
|
||||
assert!(
|
||||
parsed["deleted_at"].is_string(),
|
||||
"deleted_at must be a string"
|
||||
parsed["closed_at"].is_string(),
|
||||
"closed_at must be a string"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -670,7 +669,7 @@ async fn deleted_case_returns_404_on_detail() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn undo_delete_restores_latest_batch_only() {
|
||||
async fn undo_delete_restores_latest_group_only() {
|
||||
let config = config_with_llm(unique_tmp("undo-1"), "http://unused".into());
|
||||
let case_a = "11111111-1111-1111-1111-111111111111";
|
||||
let case_b = "22222222-2222-2222-2222-222222222222";
|
||||
@@ -682,15 +681,16 @@ async fn undo_delete_restores_latest_batch_only() {
|
||||
let app = doctate_server::create_router(config);
|
||||
let cookie = login(app.clone(), "dr_a").await;
|
||||
|
||||
// Two separate delete clicks → two distinct batches.
|
||||
// Two separate close clicks → two distinct close-groups (distinct
|
||||
// `closed_at` timestamps).
|
||||
let _ = app
|
||||
.clone()
|
||||
.oneshot(delete_request(case_a, &cookie))
|
||||
.await
|
||||
.unwrap();
|
||||
// Batch IDs derive from `now_rfc3339()`, which rounds to whole
|
||||
// seconds — so the sleep must cross a second boundary for the two
|
||||
// deletes to land in distinct batches.
|
||||
// `closed_at` is `now_rfc3339()` with whole-second granularity — the
|
||||
// sleep must cross a second boundary so the two closes land in
|
||||
// distinct groups.
|
||||
tokio::time::sleep(Duration::from_millis(1100)).await;
|
||||
let _ = app
|
||||
.clone()
|
||||
@@ -712,16 +712,16 @@ async fn undo_delete_restores_latest_batch_only() {
|
||||
.unwrap();
|
||||
assert_eq!(undo.status(), StatusCode::SEE_OTHER);
|
||||
|
||||
// case_b (latest delete) restored, case_a still deleted.
|
||||
// case_b (latest close) restored, case_a still closed.
|
||||
assert!(
|
||||
!dir_b.join(".deleted").exists(),
|
||||
!dir_b.join(".closed").exists(),
|
||||
"case_b marker should be gone"
|
||||
);
|
||||
assert!(dir_a.join(".deleted").exists(), "case_a marker must remain");
|
||||
assert!(dir_a.join(".closed").exists(), "case_a marker must remain");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bulk_delete_shares_one_batch_uuid() {
|
||||
async fn bulk_delete_shares_one_closed_at_timestamp() {
|
||||
let config = config_with_llm(unique_tmp("bulk-d"), "http://unused".into());
|
||||
let case_a = "11111111-1111-1111-1111-111111111111";
|
||||
let case_b = "22222222-2222-2222-2222-222222222222";
|
||||
@@ -749,12 +749,12 @@ async fn bulk_delete_shares_one_batch_uuid() {
|
||||
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
|
||||
|
||||
let m_a: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(dir_a.join(".deleted")).unwrap()).unwrap();
|
||||
serde_json::from_str(&std::fs::read_to_string(dir_a.join(".closed")).unwrap()).unwrap();
|
||||
let m_b: Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(dir_b.join(".deleted")).unwrap()).unwrap();
|
||||
serde_json::from_str(&std::fs::read_to_string(dir_b.join(".closed")).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
m_a["batch"], m_b["batch"],
|
||||
"bulk-delete must share batch UUID"
|
||||
m_a["closed_at"], m_b["closed_at"],
|
||||
"bulk-delete must share one closed_at timestamp so undo can group them"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -797,8 +797,8 @@ async fn recovery_skips_deleted_cases() {
|
||||
std::fs::create_dir_all(&case_dir).unwrap();
|
||||
std::fs::write(case_dir.join("analysis_input.json"), "{}").unwrap();
|
||||
std::fs::write(
|
||||
case_dir.join(".deleted"),
|
||||
r#"{"batch":"00000000-0000-0000-0000-000000000000","deleted_at":"2026-04-15T10:00:00Z"}"#,
|
||||
case_dir.join(".closed"),
|
||||
r#"{"closed_at":"2026-04-15T10:00:00Z"}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ async fn write_state(case_dir: &Path, state: &OnelinerState) {
|
||||
}
|
||||
|
||||
async fn mark_deleted(case_dir: &Path) {
|
||||
tokio::fs::write(case_dir.join(".deleted"), "{}")
|
||||
tokio::fs::write(case_dir.join(".closed"), "{}")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
+16
-17
@@ -6,7 +6,7 @@ use axum::http::{Request, StatusCode};
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
use doctate_server::config::{Config, User};
|
||||
use doctate_server::paths::{DELETE_MARKER, DeleteMarker, write_delete_marker};
|
||||
use doctate_server::paths::{CLOSE_MARKER, CloseMarker, write_close_marker};
|
||||
|
||||
fn test_config() -> Arc<Config> {
|
||||
let data_path = std::env::temp_dir().join(format!(
|
||||
@@ -247,11 +247,11 @@ async fn upload_second_recording_same_case() {
|
||||
let _ = std::fs::remove_dir_all(&data_path);
|
||||
}
|
||||
|
||||
/// Scenario A: a new upload to a *soft-deleted* case must silently
|
||||
/// remove the `.deleted` marker and store the new audio. Reopens the
|
||||
/// case so it reappears in /api/oneliners without any manual "undo".
|
||||
/// Scenario A: a new upload to a *closed* case must silently remove
|
||||
/// the `.closed` marker and store the new audio. Reopens the case so
|
||||
/// it reappears in /api/oneliners without any manual "undo".
|
||||
#[tokio::test]
|
||||
async fn upload_reopens_soft_deleted_case() {
|
||||
async fn upload_reopens_closed_case() {
|
||||
let config = test_config();
|
||||
let data_path = config.data_path.clone();
|
||||
let app = doctate_server::create_router(config);
|
||||
@@ -278,18 +278,17 @@ async fn upload_reopens_soft_deleted_case() {
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// Soft-delete the case by writing the marker directly — simulates
|
||||
// the user hitting the delete button in the web UI without having
|
||||
// to drive the /web/cases/{id}/delete handler here. Direct write
|
||||
// also means the watermark is NOT bumped here; that way the next
|
||||
// GET /api/oneliners pins down the pre-reopen ETag cleanly.
|
||||
// Close the case by writing the marker directly — simulates the
|
||||
// user hitting the close button in the web UI without having to
|
||||
// drive the /web/cases/{id}/delete handler here. Direct write also
|
||||
// means the watermark is NOT bumped here; that way the next GET
|
||||
// /api/oneliners pins down the pre-reopen ETag cleanly.
|
||||
let case_dir = data_path.join("dr_test").join(case_id);
|
||||
let marker = DeleteMarker {
|
||||
batch: uuid::Uuid::new_v4(),
|
||||
deleted_at: "2026-04-13T11:00:00Z".into(),
|
||||
let marker = CloseMarker {
|
||||
closed_at: "2026-04-13T11:00:00Z".into(),
|
||||
};
|
||||
write_delete_marker(&case_dir, &marker).await.unwrap();
|
||||
assert!(case_dir.join(DELETE_MARKER).exists());
|
||||
write_close_marker(&case_dir, &marker).await.unwrap();
|
||||
assert!(case_dir.join(CLOSE_MARKER).exists());
|
||||
|
||||
// Capture the ETag before the reopen.
|
||||
let etag_before = {
|
||||
@@ -335,8 +334,8 @@ async fn upload_reopens_soft_deleted_case() {
|
||||
|
||||
// Marker is gone → case is visible again.
|
||||
assert!(
|
||||
!case_dir.join(DELETE_MARKER).exists(),
|
||||
".deleted marker must be removed after re-upload"
|
||||
!case_dir.join(CLOSE_MARKER).exists(),
|
||||
".closed marker must be removed after re-upload"
|
||||
);
|
||||
// Both audios are on disk — the old one was not touched.
|
||||
assert!(case_dir.join("2026-04-13T10-30-00Z.m4a").exists());
|
||||
|
||||
Reference in New Issue
Block a user