From 473fd9d942bff007145404c646b842f7d7fa0c0c Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Wed, 8 Apr 2026 11:09:59 +1000 Subject: [PATCH] Fix backticks in project yaml read error --- crates/g3-core/src/tools/plan.rs | 65 +++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/crates/g3-core/src/tools/plan.rs b/crates/g3-core/src/tools/plan.rs index 10875fd..d179c04 100644 --- a/crates/g3-core/src/tools/plan.rs +++ b/crates/g3-core/src/tools/plan.rs @@ -610,15 +610,21 @@ pub fn write_plan(session_id: &str, plan: &Plan) -> Result<()> { /// Extract YAML content from a markdown file with ```yaml code block. fn extract_yaml_from_markdown(content: &str) -> Result { - // Look for ```yaml ... ``` block let start_marker = "```yaml"; - let end_marker = "```"; if let Some(start_idx) = content.find(start_marker) { let yaml_start = start_idx + start_marker.len(); - if let Some(end_idx) = content[yaml_start..].find(end_marker) { - let yaml = content[yaml_start..yaml_start + end_idx].trim(); - return Ok(yaml.to_string()); + // Find closing ``` that appears at the start of a line. + // A simple .find("```") would match backticks embedded inside YAML + // string values (e.g., descriptions containing code fences), truncating + // the YAML and causing parse errors. + let remainder = &content[yaml_start..]; + for (i, line) in remainder.split('\n').enumerate() { + if i > 0 && line.starts_with("```") { + let offset: usize = remainder.split('\n').take(i).map(|l| l.len() + 1).sum(); + let yaml = remainder[..offset].trim(); + return Ok(yaml.to_string()); + } } } @@ -1282,6 +1288,55 @@ items: [] assert!(yaml.contains("plan_id: test")); } + #[test] + fn test_yaml_extraction_with_backticks_in_values() { + // This is the exact bug: YAML values containing ``` caused + // extract_yaml_from_markdown to truncate at the embedded backticks + // instead of finding the real closing fence. + let md = "# Plan: test\n\n## Plan Data\n\n\ +```yaml\n\ +plan_id: test\n\ +revision: 1\n\ +items:\n\ + - id: I1\n\ + description: 'Fix the ```yaml parsing issue with ```'\n\ + state: todo\n\ + touches:\n\ + - src/plan.rs\n\ + checks:\n\ + happy:\n\ + desc: Works\n\ + target: plan\n\ + negative:\n\ + - desc: Fails gracefully\n\ + target: plan\n\ + boundary:\n\ + - desc: Edge case\n\ + target: plan\n\ +```\n"; + + let yaml = extract_yaml_from_markdown(md).unwrap(); + // Must contain the full YAML, not truncated at the embedded backticks + assert!(yaml.contains("plan_id: test"), "should contain plan_id"); + assert!(yaml.contains("description:"), "should contain description field"); + assert!(yaml.contains("state: todo"), "should contain state field"); + assert!(yaml.contains("checks:"), "should contain checks"); + } + + #[test] + fn test_yaml_extraction_no_code_block_fallback() { + let raw_yaml = "plan_id: test\nrevision: 1\nitems: []\n"; + let yaml = extract_yaml_from_markdown(raw_yaml).unwrap(); + assert_eq!(yaml, raw_yaml); + } + + #[test] + fn test_yaml_extraction_closing_fence_no_trailing_newline() { + let md = "```yaml\nplan_id: test\nrevision: 1\nitems: []\n```"; + let yaml = extract_yaml_from_markdown(md).unwrap(); + assert!(yaml.contains("plan_id: test")); + } + #[test] fn test_plan_serialization_roundtrip() { let mut plan = Plan::new("test-plan");