Refactor oneliner storage to use enum

The oneliner is now stored as `OnelinerState` enum which can represent
three states: `Ready`, `Empty` or `Error`. This allows the client to
differentiate between a case with no medical content and a case where
the oneliner generation failed.

The `OnelinerState` enum is serialized to JSON and stored in
`oneliner.json` file. The `OnelinerEntry` struct has been updated to
reflect this change.

The client-desktop application has been updated to handle the new
`OnelinerState` enum and display appropriate UI elements for each state.

The server-side code has also been updated to read and write the
`OnelinerState` enum, and to handle the new file format.
The `reset_case_artefacts` function in
`server/src/routes/case_actions.rs` has been updated to remove
`oneliner.txt` and create `oneliner.json` instead.

The tests have been updated to reflect these changes.
This commit is contained in:
2026-04-20 12:37:48 +02:00
parent 224ee60363
commit d65720c671
15 changed files with 440 additions and 125 deletions
+80 -9
View File
@@ -8,6 +8,7 @@ use axum::http::{Request, StatusCode};
use filetime::FileTime;
use tower::util::ServiceExt;
use doctate_common::oneliners::OnelinerState;
use doctate_server::config::{Config, User};
const TEST_KEY: &str = "test-key-oneliners";
@@ -50,13 +51,29 @@ async fn seed_case(
filetime::set_file_mtime(&m4a, FileTime::from_system_time(target_mtime)).unwrap();
if let Some(text) = oneliner_text {
tokio::fs::write(case_dir.join("oneliner.txt"), text)
.await
.unwrap();
write_ready_state(&case_dir, text).await;
}
case_dir
}
async fn write_ready_state(case_dir: &Path, text: &str) {
let state = OnelinerState::Ready {
text: text.to_owned(),
generated_at: "2026-04-18T10:00:00Z".into(),
};
let bytes = serde_json::to_vec(&state).unwrap();
tokio::fs::write(case_dir.join("oneliner.json"), bytes)
.await
.unwrap();
}
async fn write_state(case_dir: &Path, state: &OnelinerState) {
let bytes = serde_json::to_vec(state).unwrap();
tokio::fs::write(case_dir.join("oneliner.json"), bytes)
.await
.unwrap();
}
async fn mark_deleted(case_dir: &Path) {
tokio::fs::write(case_dir.join(".deleted"), "{}")
.await
@@ -155,7 +172,8 @@ async fn recent_case_with_oneliner_shows_up() {
let arr = body["oneliners"].as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["case_id"], case_id);
assert_eq!(arr[0]["oneliner"], "Kniegelenk re., V.a. Meniskus");
assert_eq!(arr[0]["oneliner"]["kind"], "ready");
assert_eq!(arr[0]["oneliner"]["text"], "Kniegelenk re., V.a. Meniskus");
assert!(arr[0]["created_at"].is_string());
assert!(arr[0]["updated_at"].is_string());
@@ -365,8 +383,8 @@ async fn multiple_cases_sorted_newest_first() {
let body = body_json(app.oneshot(get("/api/oneliners")).await.unwrap()).await;
let arr = body["oneliners"].as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["oneliner"], "newer");
assert_eq!(arr[1]["oneliner"], "older");
assert_eq!(arr[0]["oneliner"]["text"], "newer");
assert_eq!(arr[1]["oneliner"]["text"], "older");
let _ = std::fs::remove_dir_all(&dp);
}
@@ -398,9 +416,7 @@ async fn sorts_by_last_recording_not_created() {
FileTime::from_system_time(SystemTime::now() - Duration::from_secs(60)),
)
.unwrap();
tokio::fs::write(dir_a.join("oneliner.txt"), "A (fresh addendum)")
.await
.unwrap();
write_ready_state(&dir_a, "A (fresh addendum)").await;
// Case B: single recording 10h ago — created_at newer than A's, but
// last_recording_at older than A's fresh addendum.
@@ -421,3 +437,58 @@ async fn sorts_by_last_recording_not_created() {
let _ = std::fs::remove_dir_all(&dp);
}
#[tokio::test]
async fn empty_state_serializes_as_kind_empty() {
let (config, dp) = test_config();
let case_dir = seed_case(
&dp,
"550e8400-e29b-41d4-a716-000000000030",
Duration::from_secs(60),
None,
)
.await;
write_state(
&case_dir,
&OnelinerState::Empty {
generated_at: "2026-04-18T10:00:00Z".into(),
},
)
.await;
let app = doctate_server::create_router(config);
let body = body_json(app.oneshot(get("/api/oneliners")).await.unwrap()).await;
let arr = body["oneliners"].as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["oneliner"]["kind"], "empty");
assert!(arr[0]["oneliner"].get("text").is_none());
let _ = std::fs::remove_dir_all(&dp);
}
#[tokio::test]
async fn error_state_serializes_as_kind_error() {
let (config, dp) = test_config();
let case_dir = seed_case(
&dp,
"550e8400-e29b-41d4-a716-000000000031",
Duration::from_secs(60),
None,
)
.await;
write_state(
&case_dir,
&OnelinerState::Error {
generated_at: "2026-04-18T10:00:00Z".into(),
},
)
.await;
let app = doctate_server::create_router(config);
let body = body_json(app.oneshot(get("/api/oneliners")).await.unwrap()).await;
let arr = body["oneliners"].as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["oneliner"]["kind"], "error");
let _ = std::fs::remove_dir_all(&dp);
}