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
+40 -1
View File
@@ -4,6 +4,7 @@ use std::sync::Arc;
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use doctate_common::oneliners::OnelinerState;
use doctate_server::config::{Config, User};
use tower::util::ServiceExt;
@@ -32,7 +33,17 @@ fn make_admin(slug: &str) -> User {
}
fn seed_oneliner(case_dir: &Path, text: &str) {
std::fs::write(case_dir.join("oneliner.txt"), text).unwrap();
let state = OnelinerState::Ready {
text: text.to_owned(),
generated_at: "2026-04-18T10:00:00Z".into(),
};
let bytes = serde_json::to_vec(&state).unwrap();
std::fs::write(case_dir.join("oneliner.json"), bytes).unwrap();
}
fn seed_oneliner_state(case_dir: &Path, state: &OnelinerState) {
let bytes = serde_json::to_vec(state).unwrap();
std::fs::write(case_dir.join("oneliner.json"), bytes).unwrap();
}
fn config_with_llm_users(data_path: PathBuf, llm_url: String, users: Vec<User>) -> Arc<Config> {
@@ -326,6 +337,34 @@ async fn case_page_uses_oneliner_as_h1_when_present() {
);
}
#[tokio::test]
async fn case_page_falls_back_to_generic_title_for_empty_state() {
// An `Empty` state (LLM deliberately returned no medical content) is
// not a text; the H1 must fall back to the generic "Fall" label, not
// leak the kind string or crash the template.
let config = config_with_llm(unique_tmp("cp-empty"));
let case_id = "22222222-2222-2222-2222-222222222222";
let case_dir = seed_case(&config.data_path, "dr_a", case_id);
seed_oneliner_state(
&case_dir,
&OnelinerState::Empty {
generated_at: "2026-04-18T10:00:00Z".into(),
},
);
let app = doctate_server::create_router(config);
let cookie = login(app.clone(), "dr_a").await;
let resp = app.oneshot(get_page(case_id, &cookie, "")).await.unwrap();
let body = body_string(resp).await;
assert!(body.contains("Fall"), "expected generic 'Fall' fallback");
assert!(
!body.contains("\"kind\""),
"must not leak JSON kind discriminator into HTML"
);
}
#[tokio::test]
async fn case_page_admin_sees_admin_view_toggle() {
let data_path = unique_tmp("cp-toggle");