From b77e139347af40ba3d32cca42df3a6da1aa3b8fa Mon Sep 17 00:00:00 2001 From: dave Date: Fri, 17 Apr 2026 13:53:36 +0000 Subject: [PATCH] huskies: merge 593_bug_web_ui_work_item_detail_panel_returns_404_for_crdt_only_stories --- server/src/http/agents.rs | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/server/src/http/agents.rs b/server/src/http/agents.rs index a2e20476..4f090947 100644 --- a/server/src/http/agents.rs +++ b/server/src/http/agents.rs @@ -402,6 +402,34 @@ impl AgentsApi { } } + // Filesystem miss — fall back to CRDT-only path (story exists in the CRDT + // but has no corresponding .md file on disk). + if let Some(content) = crate::db::read_content(&story_id.0) { + let item = crate::pipeline_state::read_typed(&story_id.0) + .map_err(|e| bad_request(format!("Pipeline read error: {e}")))?; + let stage = item + .as_ref() + .map(|i| match &i.stage { + crate::pipeline_state::Stage::Backlog => "backlog", + crate::pipeline_state::Stage::Coding => "current", + crate::pipeline_state::Stage::Qa => "qa", + crate::pipeline_state::Stage::Merge { .. } => "merge", + crate::pipeline_state::Stage::Done { .. } => "done", + crate::pipeline_state::Stage::Archived { .. } => "archived", + }) + .unwrap_or("unknown") + .to_string(); + let metadata = crate::io::story_metadata::parse_front_matter(&content).ok(); + let name = metadata.as_ref().and_then(|m| m.name.clone()); + let agent = metadata.and_then(|m| m.agent); + return Ok(Json(WorkItemContentResponse { + content, + stage, + name, + agent, + })); + } + Err(not_found(format!("Work item not found: {}", story_id.0))) } @@ -953,6 +981,50 @@ allowed_tools = ["Read", "Bash"] assert!(result.is_err()); } + #[tokio::test] + async fn get_work_item_content_falls_back_to_crdt_when_no_file() { + let tmp = TempDir::new().unwrap(); + let root = tmp.path().to_path_buf(); + // Seed content + CRDT with no .md file on disk. + crate::db::write_item_with_content( + "44_story_crdt_only", + "1_backlog", + "---\nname: \"CRDT Only\"\n---\n\nCRDT content.", + ); + let ctx = AppContext::new_test(root); + let api = AgentsApi { ctx: Arc::new(ctx) }; + let result = api + .get_work_item_content(Path("44_story_crdt_only".to_string())) + .await + .unwrap() + .0; + assert!(result.content.contains("CRDT content.")); + assert_eq!(result.stage, "backlog"); + assert_eq!(result.name, Some("CRDT Only".to_string())); + } + + #[tokio::test] + async fn get_work_item_content_crdt_fallback_with_current_stage() { + let tmp = TempDir::new().unwrap(); + let root = tmp.path().to_path_buf(); + // Seed a CRDT-only story in the coding/current stage. + crate::db::write_item_with_content( + "45_story_crdt_current", + "2_current", + "---\nname: \"Current CRDT\"\n---\n\nIn progress.", + ); + let ctx = AppContext::new_test(root); + let api = AgentsApi { ctx: Arc::new(ctx) }; + let result = api + .get_work_item_content(Path("45_story_crdt_current".to_string())) + .await + .unwrap() + .0; + assert!(result.content.contains("In progress.")); + assert_eq!(result.stage, "current"); + assert_eq!(result.name, Some("Current CRDT".to_string())); + } + #[tokio::test] async fn get_work_item_content_returns_error_when_no_project_root() { let tmp = TempDir::new().unwrap();