Refactor: Simplify case directory structure
The distinction between `open/` and `done/` directories for cases has been removed. All cases for a user now reside directly under the user's directory (e.g., `<data_path>/<slug>/<case_id>/`). This simplifies path management and eliminates redundant directory traversals. Key changes include: - Removed `open/` and `done/` subdirectories in path resolution. - Introduced a `paths::case_dir` function as a single source of truth for case directory layout. - Updated various modules (`recovery`, `auth`, `routes`, `transcribe`, `tests`) to use the new path structure. - Adjusted templates to reflect the simplified case status representation.
This commit is contained in:
@@ -79,7 +79,7 @@ fn config_without_llm(data_path: PathBuf) -> Arc<Config> {
|
||||
}
|
||||
|
||||
fn seed_case(data_path: &Path, slug: &str, case_id: &str) -> PathBuf {
|
||||
let dir = data_path.join(slug).join("open").join(case_id);
|
||||
let dir = data_path.join(slug).join(case_id);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
@@ -298,7 +298,7 @@ async fn close_case_skips_blank_transcript_from_recordings() {
|
||||
#[tokio::test]
|
||||
async fn analyze_worker_writes_document_via_wiremock() {
|
||||
let tmp = unique_tmp("w");
|
||||
let case_dir = tmp.join("dr_a/open/11111111-1111-1111-1111-111111111111");
|
||||
let case_dir = tmp.join("dr_a/11111111-1111-1111-1111-111111111111");
|
||||
std::fs::create_dir_all(&case_dir).unwrap();
|
||||
|
||||
let input = json!({
|
||||
@@ -363,7 +363,7 @@ async fn analyze_worker_writes_document_via_wiremock() {
|
||||
#[tokio::test]
|
||||
async fn recovery_enqueues_pending_analysis() {
|
||||
let tmp = unique_tmp("r1");
|
||||
let case_dir = tmp.join("dr_a/open/11111111-1111-1111-1111-111111111111");
|
||||
let case_dir = tmp.join("dr_a/11111111-1111-1111-1111-111111111111");
|
||||
std::fs::create_dir_all(&case_dir).unwrap();
|
||||
std::fs::write(case_dir.join("analysis_input_v1.json"), "{}").unwrap();
|
||||
|
||||
@@ -379,7 +379,7 @@ async fn recovery_enqueues_pending_analysis() {
|
||||
#[tokio::test]
|
||||
async fn recovery_skips_completed_analysis() {
|
||||
let tmp = unique_tmp("r2");
|
||||
let case_dir = tmp.join("dr_a/open/11111111-1111-1111-1111-111111111111");
|
||||
let case_dir = tmp.join("dr_a/11111111-1111-1111-1111-111111111111");
|
||||
std::fs::create_dir_all(&case_dir).unwrap();
|
||||
std::fs::write(case_dir.join("analysis_input_v1.json"), "{}").unwrap();
|
||||
std::fs::write(case_dir.join("document_v1.md"), "done").unwrap();
|
||||
@@ -566,7 +566,7 @@ async fn reanalyze_second_time_returns_409() {
|
||||
#[tokio::test]
|
||||
async fn reanalyze_worker_writes_document_v2_via_wiremock() {
|
||||
let tmp = unique_tmp("rn-w");
|
||||
let case_dir = tmp.join("dr_a/open/11111111-1111-1111-1111-111111111111");
|
||||
let case_dir = tmp.join("dr_a/11111111-1111-1111-1111-111111111111");
|
||||
std::fs::create_dir_all(&case_dir).unwrap();
|
||||
std::fs::write(case_dir.join("document_v1.md"), "alte Version").unwrap();
|
||||
|
||||
|
||||
@@ -124,8 +124,8 @@ async fn cases_with_valid_cookie_shows_only_own_cases() {
|
||||
make_user("dr_b", "s"),
|
||||
]);
|
||||
// Seed fixtures: dr_a has an open case, dr_b has one too.
|
||||
let case_a = config.data_path.join("dr_a/open/11111111-1111-1111-1111-111111111111");
|
||||
let case_b = config.data_path.join("dr_b/open/22222222-2222-2222-2222-222222222222");
|
||||
let case_a = config.data_path.join("dr_a/11111111-1111-1111-1111-111111111111");
|
||||
let case_b = config.data_path.join("dr_b/22222222-2222-2222-2222-222222222222");
|
||||
std::fs::create_dir_all(&case_a).unwrap();
|
||||
std::fs::create_dir_all(&case_b).unwrap();
|
||||
std::fs::write(case_a.join("2026-04-14T10-00-00Z.m4a"), b"x").unwrap();
|
||||
@@ -159,7 +159,7 @@ async fn case_detail_of_foreign_case_returns_404() {
|
||||
make_user("dr_a", "s"),
|
||||
make_user("dr_b", "s"),
|
||||
]);
|
||||
let case_b = config.data_path.join("dr_b/open/22222222-2222-2222-2222-222222222222");
|
||||
let case_b = config.data_path.join("dr_b/22222222-2222-2222-2222-222222222222");
|
||||
std::fs::create_dir_all(&case_b).unwrap();
|
||||
std::fs::write(case_b.join("2026-04-14T11-00-00Z.m4a"), b"x").unwrap();
|
||||
|
||||
|
||||
@@ -212,23 +212,18 @@ async fn recovery_enqueues_only_pending_recordings() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
// User 1, open case with two .m4a — one pending, one already transcribed.
|
||||
let case_a = root.join("dr_a/open/aaaa");
|
||||
// User 1, case with two .m4a — one pending, one already transcribed.
|
||||
let case_a = root.join("dr_a/aaaa");
|
||||
std::fs::create_dir_all(&case_a).unwrap();
|
||||
std::fs::write(case_a.join("2026-04-10T10-00-00Z.m4a"), b"x").unwrap();
|
||||
std::fs::write(case_a.join("2026-04-11T10-00-00Z.m4a"), b"x").unwrap();
|
||||
std::fs::write(case_a.join("2026-04-11T10-00-00Z.transcript.txt"), b"done").unwrap();
|
||||
|
||||
// User 2, open case with one pending .m4a.
|
||||
let case_b = root.join("dr_b/open/bbbb");
|
||||
// User 2, case with one pending .m4a.
|
||||
let case_b = root.join("dr_b/bbbb");
|
||||
std::fs::create_dir_all(&case_b).unwrap();
|
||||
std::fs::write(case_b.join("2026-04-12T10-00-00Z.m4a"), b"x").unwrap();
|
||||
|
||||
// done/ cases must be ignored (not re-transcribed).
|
||||
let case_c = root.join("dr_a/done/cccc");
|
||||
std::fs::create_dir_all(&case_c).unwrap();
|
||||
std::fs::write(case_c.join("2026-04-09T10-00-00Z.m4a"), b"x").unwrap();
|
||||
|
||||
let (tx, mut rx) = transcribe::channel();
|
||||
scan_and_enqueue(root, &tx).await;
|
||||
drop(tx); // close channel so the loop below terminates
|
||||
@@ -275,7 +270,7 @@ async fn worker_renames_audio_to_failed_on_whisper_error() {
|
||||
|
||||
// Real case dir with a real m4a fixture so ffmpeg remux succeeds.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let case_dir = tmp.path().join("dr_test/open/aaaa");
|
||||
let case_dir = tmp.path().join("dr_test/aaaa");
|
||||
std::fs::create_dir_all(&case_dir).unwrap();
|
||||
let audio = case_dir.join("2026-04-13T10-30-00Z.m4a");
|
||||
std::fs::copy(fixture("sample.m4a"), &audio).unwrap();
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn upload_creates_new_case() {
|
||||
|
||||
// Verify file on disk
|
||||
let file = data_path
|
||||
.join("dr_test/open")
|
||||
.join("dr_test")
|
||||
.join(case_id)
|
||||
.join("2026-04-13T10-30-00Z.m4a");
|
||||
assert!(file.exists(), "Audio file should exist at {file:?}");
|
||||
@@ -242,7 +242,7 @@ async fn upload_second_recording_same_case() {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// Both files should exist
|
||||
let case_dir = data_path.join("dr_test/open").join(case_id);
|
||||
let case_dir = data_path.join("dr_test").join(case_id);
|
||||
assert!(case_dir.join("2026-04-13T10-30-00Z.m4a").exists());
|
||||
assert!(case_dir.join("2026-04-13T10-45-00Z.m4a").exists());
|
||||
|
||||
@@ -250,82 +250,3 @@ async fn upload_second_recording_same_case() {
|
||||
let _ = std::fs::remove_dir_all(&data_path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_late_recording_to_done_case() {
|
||||
let config = test_config();
|
||||
let data_path = config.data_path.clone();
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let case_id = "770e8400-e29b-41d4-a716-446655440000";
|
||||
|
||||
// Pre-create a done/ case directory (simulating a closed case).
|
||||
let done_dir = data_path.join("dr_test/done").join(case_id);
|
||||
std::fs::create_dir_all(&done_dir).unwrap();
|
||||
|
||||
let (boundary, body) = multipart_body(case_id, "2026-04-13T11:00:00Z", b"late recording");
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/upload")
|
||||
.header("X-API-Key", "test-key-123")
|
||||
.header(
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={boundary}"),
|
||||
)
|
||||
.body(Body::from(body))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// File should be in done/, not open/
|
||||
assert!(done_dir.join("2026-04-13T11-00-00Z.m4a").exists());
|
||||
assert!(!data_path.join("dr_test/open").join(case_id).exists());
|
||||
|
||||
// Cleanup
|
||||
let _ = std::fs::remove_dir_all(&data_path);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upload_removes_remove_marker_on_late_upload() {
|
||||
let config = test_config();
|
||||
let data_path = config.data_path.clone();
|
||||
let app = doctate_server::create_router(config);
|
||||
|
||||
let case_id = "880e8400-e29b-41d4-a716-446655440000";
|
||||
|
||||
// Pre-create a done/ case with .remove marker.
|
||||
let done_dir = data_path.join("dr_test/done").join(case_id);
|
||||
std::fs::create_dir_all(&done_dir).unwrap();
|
||||
std::fs::write(done_dir.join(".remove"), b"").unwrap();
|
||||
assert!(done_dir.join(".remove").exists());
|
||||
|
||||
let (boundary, body) = multipart_body(case_id, "2026-04-13T12:00:00Z", b"surprise recording");
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/upload")
|
||||
.header("X-API-Key", "test-key-123")
|
||||
.header(
|
||||
"Content-Type",
|
||||
format!("multipart/form-data; boundary={boundary}"),
|
||||
)
|
||||
.body(Body::from(body))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// .remove marker should be gone, audio should be there
|
||||
assert!(!done_dir.join(".remove").exists());
|
||||
assert!(done_dir.join("2026-04-13T12-00-00Z.m4a").exists());
|
||||
|
||||
// Cleanup
|
||||
let _ = std::fs::remove_dir_all(&data_path);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ async fn web_index_shows_cases() {
|
||||
let case1 = "550e8400-e29b-41d4-a716-446655440000";
|
||||
let case2 = "660e8400-e29b-41d4-a716-446655440000";
|
||||
|
||||
let open_case = data_path.join("dr_test/open").join(case1);
|
||||
let open_case = data_path.join("dr_test").join(case1);
|
||||
std::fs::create_dir_all(&open_case).unwrap();
|
||||
std::fs::write(
|
||||
open_case.join("2026-04-13T10-30-00Z.m4a"),
|
||||
@@ -71,7 +71,7 @@ async fn web_index_shows_cases() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let done_case = data_path.join("dr_test/done").join(case2);
|
||||
let done_case = data_path.join("dr_test").join(case2);
|
||||
std::fs::create_dir_all(&done_case).unwrap();
|
||||
std::fs::write(
|
||||
done_case.join("2026-04-12T09-00-00Z.m4a"),
|
||||
@@ -125,7 +125,7 @@ async fn web_audio_serves_file() {
|
||||
let data_path = config.data_path.clone();
|
||||
|
||||
let case_id = "770e8400-e29b-41d4-a716-446655440000";
|
||||
let case_dir = data_path.join("dr_test/open").join(case_id);
|
||||
let case_dir = data_path.join("dr_test").join(case_id);
|
||||
std::fs::create_dir_all(&case_dir).unwrap();
|
||||
|
||||
let audio_bytes = b"fake audio content for test";
|
||||
|
||||
Reference in New Issue
Block a user