1use anyhow::Result;
75use serde::{Deserialize, Serialize};
76use std::fmt;
77use uuid::Uuid;
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
81pub enum AgentDocFormat {
82 #[clap(alias = "inline")]
84 Append,
85 Template,
87}
88
89impl fmt::Display for AgentDocFormat {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 Self::Append => write!(f, "inline"),
93 Self::Template => write!(f, "template"),
94 }
95 }
96}
97
98impl Serialize for AgentDocFormat {
99 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
100 where
101 S: serde::Serializer,
102 {
103 match self {
104 Self::Append => serializer.serialize_str("inline"),
105 Self::Template => serializer.serialize_str("template"),
106 }
107 }
108}
109
110impl<'de> Deserialize<'de> for AgentDocFormat {
111 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
112 where
113 D: serde::Deserializer<'de>,
114 {
115 let s = String::deserialize(deserializer)?;
116 match s.as_str() {
117 "append" | "inline" => Ok(Self::Append),
118 "template" => Ok(Self::Template),
119 other => Err(serde::de::Error::unknown_variant(other, &["inline", "append", "template"])),
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
126#[serde(rename_all = "lowercase")]
127pub enum AgentDocWrite {
128 Merge,
130 Crdt,
132}
133
134impl fmt::Display for AgentDocWrite {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 Self::Merge => write!(f, "merge"),
138 Self::Crdt => write!(f, "crdt"),
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct ResolvedMode {
146 pub format: AgentDocFormat,
147 pub write: AgentDocWrite,
148}
149
150impl ResolvedMode {
151 pub fn is_template(&self) -> bool {
152 self.format == AgentDocFormat::Template
153 }
154
155 pub fn is_append(&self) -> bool {
156 self.format == AgentDocFormat::Append
157 }
158
159 pub fn is_crdt(&self) -> bool {
160 self.write == AgentDocWrite::Crdt
161 }
162}
163
164#[derive(Debug, Default, Clone, Serialize, Deserialize)]
166pub struct StreamConfig {
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub interval: Option<u64>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub strip_ansi: Option<bool>,
173 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub target: Option<String>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub thinking: Option<bool>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub thinking_target: Option<String>,
183}
184
185#[derive(Debug, Default, Serialize, Deserialize)]
186pub struct Frontmatter {
187 #[serde(
190 default,
191 skip_serializing_if = "Option::is_none",
192 rename = "agent_doc_session",
193 alias = "session"
194 )]
195 pub session: Option<String>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub resume: Option<String>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub agent: Option<String>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub model: Option<String>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub branch: Option<String>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub tmux_session: Option<String>,
213 #[serde(
217 default,
218 skip_serializing_if = "Option::is_none",
219 rename = "agent_doc_mode",
220 alias = "mode",
221 alias = "response_mode"
222 )]
223 pub mode: Option<String>,
224 #[serde(
226 default,
227 skip_serializing_if = "Option::is_none",
228 rename = "agent_doc_format"
229 )]
230 pub format: Option<AgentDocFormat>,
231 #[serde(
233 default,
234 skip_serializing_if = "Option::is_none",
235 rename = "agent_doc_write"
236 )]
237 pub write_mode: Option<AgentDocWrite>,
238 #[serde(
240 default,
241 skip_serializing_if = "Option::is_none",
242 rename = "agent_doc_stream"
243 )]
244 pub stream_config: Option<StreamConfig>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub claude_args: Option<String>,
249 #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "related_docs")]
252 pub links: Vec<String>,
253}
254
255impl Frontmatter {
256 pub fn resolve_mode(&self) -> ResolvedMode {
263 let mut format = AgentDocFormat::Template;
265 let mut write = AgentDocWrite::Crdt;
266
267 if let Some(ref mode_str) = self.mode {
269 match mode_str.as_str() {
270 "append" => {
271 format = AgentDocFormat::Append;
272 }
274 "template" => {
275 format = AgentDocFormat::Template;
276 }
278 "stream" => {
279 format = AgentDocFormat::Template;
280 write = AgentDocWrite::Crdt;
281 }
282 _ => {} }
284 }
285
286 if let Some(f) = self.format {
288 format = f;
289 }
290 if let Some(w) = self.write_mode {
291 write = w;
292 }
293
294 ResolvedMode { format, write }
295 }
296}
297
298pub fn parse(content: &str) -> Result<(Frontmatter, &str)> {
301 if !content.starts_with("---\n") {
302 return Ok((Frontmatter::default(), content));
303 }
304 let rest = &content[4..]; let end = rest
306 .find("\n---\n")
307 .or_else(|| rest.find("\n---"))
308 .ok_or_else(|| anyhow::anyhow!("Unterminated frontmatter block"))?;
309 let yaml = &rest[..end];
310 let fm: Frontmatter = serde_yaml::from_str(yaml)?;
311 let body_start = 4 + end + 4; let body = if body_start <= content.len() {
313 &content[body_start..]
314 } else {
315 ""
316 };
317 Ok((fm, body))
318}
319
320pub fn write(fm: &Frontmatter, body: &str) -> Result<String> {
322 let yaml = serde_yaml::to_string(fm)?;
323 Ok(format!("---\n{}---\n{}", yaml, body))
324}
325
326pub fn set_session_id(content: &str, session_id: &str) -> Result<String> {
328 let (mut fm, body) = parse(content)?;
329 fm.session = Some(session_id.to_string());
330 write(&fm, body)
331}
332
333pub fn set_resume_id(content: &str, resume_id: &str) -> Result<String> {
335 let (mut fm, body) = parse(content)?;
336 fm.resume = Some(resume_id.to_string());
337 write(&fm, body)
338}
339
340pub fn set_format_and_write(
342 content: &str,
343 format: AgentDocFormat,
344 write_mode: AgentDocWrite,
345) -> Result<String> {
346 let (mut fm, body) = parse(content)?;
347 fm.format = Some(format);
348 fm.write_mode = Some(write_mode);
349 fm.mode = None;
350 write(&fm, body)
351}
352
353pub fn merge_fields(content: &str, yaml_fields: &str) -> Result<String> {
359 let (mut fm, body) = parse(content)?;
360 let patch: serde_yaml::Value = serde_yaml::from_str(yaml_fields)
361 .unwrap_or(serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
362 let mapping = patch.as_mapping().unwrap_or(&serde_yaml::Mapping::new()).clone();
363
364 for (key, value) in &mapping {
365 let key_str = key.as_str().unwrap_or("");
366 let val_str = || value.as_str().map(|s| s.to_string());
367 match key_str {
368 "agent_doc_session" | "session" => fm.session = val_str(),
369 "resume" => fm.resume = val_str(),
370 "agent" => fm.agent = val_str(),
371 "model" => fm.model = val_str(),
372 "branch" => fm.branch = val_str(),
373 "tmux_session" => fm.tmux_session = val_str(),
374 "agent_doc_mode" | "mode" | "response_mode" => fm.mode = val_str(),
375 "agent_doc_format" => {
376 if let Some(s) = value.as_str()
377 && let Ok(f) = serde_yaml::from_str::<AgentDocFormat>(&format!("\"{}\"", s))
378 {
379 fm.format = Some(f);
380 }
381 }
382 "agent_doc_write" => {
383 if let Some(s) = value.as_str()
384 && let Ok(w) = serde_yaml::from_str::<AgentDocWrite>(&format!("\"{}\"", s))
385 {
386 fm.write_mode = Some(w);
387 }
388 }
389 "claude_args" => fm.claude_args = val_str(),
390 _ => {
391 eprintln!("[frontmatter] ignoring unknown patch field: {}", key_str);
392 }
393 }
394 }
395
396 write(&fm, body)
397}
398
399pub fn set_tmux_session(content: &str, session_name: &str) -> Result<String> {
405 let (mut fm, body) = parse(content)?;
406 fm.tmux_session = Some(session_name.to_string());
407 write(&fm, body)
408}
409
410pub fn ensure_session(content: &str) -> Result<(String, String)> {
415 let (fm, _body) = parse(content)?;
416 if let Some(ref session_id) = fm.session {
417 return Ok((content.to_string(), session_id.clone()));
419 }
420 let session_id = Uuid::new_v4().to_string();
421 let updated = set_session_id(content, &session_id)?;
422 Ok((updated, session_id))
423}
424
425pub fn read_session_id(file: &std::path::Path) -> Option<String> {
427 let content = std::fs::read_to_string(file).ok()?;
428 let (fm, _) = parse(&content).ok()?;
429 fm.session
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn parse_no_frontmatter() {
438 let content = "# Hello\n\nBody text.\n";
439 let (fm, body) = parse(content).unwrap();
440 assert!(fm.session.is_none());
441 assert!(fm.agent.is_none());
442 assert!(fm.model.is_none());
443 assert!(fm.branch.is_none());
444 assert_eq!(body, content);
445 }
446
447 #[test]
448 fn parse_all_fields() {
449 let content = "---\nsession: abc-123\nagent: claude\nmodel: opus\nbranch: main\n---\nBody\n";
450 let (fm, body) = parse(content).unwrap();
451 assert_eq!(fm.session.as_deref(), Some("abc-123"));
452 assert_eq!(fm.agent.as_deref(), Some("claude"));
453 assert_eq!(fm.model.as_deref(), Some("opus"));
454 assert_eq!(fm.branch.as_deref(), Some("main"));
455 assert!(body.contains("Body"));
456 }
457
458 #[test]
459 fn parse_partial_fields() {
460 let content = "---\nsession: xyz\n---\n# Doc\n";
461 let (fm, body) = parse(content).unwrap();
462 assert_eq!(fm.session.as_deref(), Some("xyz"));
463 assert!(fm.agent.is_none());
464 assert!(body.contains("# Doc"));
465 }
466
467 #[test]
468 fn parse_null_fields() {
469 let content = "---\nsession: null\nagent: null\nmodel: null\nbranch: null\n---\nBody\n";
470 let (fm, body) = parse(content).unwrap();
471 assert!(fm.session.is_none());
472 assert!(fm.agent.is_none());
473 assert!(fm.model.is_none());
474 assert!(fm.branch.is_none());
475 assert!(body.contains("Body"));
476 }
477
478 #[test]
479 fn parse_unterminated_frontmatter() {
480 let content = "---\nsession: abc\nno closing block";
481 let err = parse(content).unwrap_err();
482 assert!(err.to_string().contains("Unterminated frontmatter"));
483 }
484
485 #[test]
486 fn parse_closing_at_eof() {
487 let content = "---\nsession: abc\n---";
488 let (fm, body) = parse(content).unwrap();
489 assert_eq!(fm.session.as_deref(), Some("abc"));
490 assert_eq!(body, "");
491 }
492
493 #[test]
494 fn parse_empty_body() {
495 let content = "---\nsession: abc\n---\n";
496 let (fm, _body) = parse(content).unwrap();
497 assert_eq!(fm.session.as_deref(), Some("abc"));
498 }
499
500 #[test]
501 fn write_roundtrip() {
502 let fm = Frontmatter {
504 session: Some("test-id".to_string()),
505 resume: Some("resume-id".to_string()),
506 agent: Some("claude".to_string()),
507 model: Some("opus".to_string()),
508 branch: Some("dev".to_string()),
509 tmux_session: None,
510 mode: None,
511 format: None,
512 write_mode: None,
513 stream_config: None,
514 claude_args: None,
515 links: vec![],
516 };
517 let body = "# Hello\n\nBody text.\n";
518 let written = write(&fm, body).unwrap();
519 let (fm2, body2) = parse(&written).unwrap();
520 assert_eq!(fm2.session, fm.session);
521 assert_eq!(fm2.agent, fm.agent);
522 assert_eq!(fm2.model, fm.model);
523 assert_eq!(fm2.branch, fm.branch);
524 assert!(body2.contains("# Hello"));
526 assert!(body2.contains("Body text."));
527 }
528
529 #[test]
530 fn write_default_frontmatter() {
531 let fm = Frontmatter::default();
532 let result = write(&fm, "body\n").unwrap();
533 assert!(result.starts_with("---\n"));
534 assert!(result.ends_with("---\nbody\n"));
535 }
536
537 #[test]
538 fn write_preserves_body_content() {
539 let fm = Frontmatter::default();
540 let body = "# Title\n\nSome **markdown** with `code`.\n";
541 let result = write(&fm, body).unwrap();
542 assert!(result.contains("# Title"));
543 assert!(result.contains("Some **markdown** with `code`."));
544 }
545
546 #[test]
547 fn set_session_id_creates_frontmatter() {
548 let content = "# No frontmatter\n\nJust body.\n";
549 let result = set_session_id(content, "new-session").unwrap();
550 let (fm, body) = parse(&result).unwrap();
551 assert_eq!(fm.session.as_deref(), Some("new-session"));
552 assert!(body.contains("# No frontmatter"));
553 }
554
555 #[test]
556 fn set_session_id_updates_existing() {
557 let content = "---\nsession: old-id\nagent: claude\n---\nBody\n";
558 let result = set_session_id(content, "new-id").unwrap();
559 let (fm, body) = parse(&result).unwrap();
560 assert_eq!(fm.session.as_deref(), Some("new-id"));
561 assert_eq!(fm.agent.as_deref(), Some("claude"));
562 assert!(body.contains("Body"));
563 }
564
565 #[test]
566 fn set_session_id_preserves_other_fields() {
567 let content = "---\nsession: old\nagent: claude\nmodel: opus\nbranch: dev\n---\nBody\n";
568 let result = set_session_id(content, "new").unwrap();
569 let (fm, _) = parse(&result).unwrap();
570 assert_eq!(fm.session.as_deref(), Some("new"));
571 assert_eq!(fm.agent.as_deref(), Some("claude"));
572 assert_eq!(fm.model.as_deref(), Some("opus"));
573 assert_eq!(fm.branch.as_deref(), Some("dev"));
574 }
575
576 #[test]
577 fn ensure_session_no_frontmatter() {
578 let content = "# Hello\n\nBody.\n";
579 let (updated, sid) = ensure_session(content).unwrap();
580 assert_eq!(sid.len(), 36); let (fm, body) = parse(&updated).unwrap();
583 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
584 assert!(body.contains("# Hello"));
585 }
586
587 #[test]
588 fn ensure_session_null_session() {
589 let content = "---\nsession:\nagent: claude\n---\nBody\n";
590 let (updated, sid) = ensure_session(content).unwrap();
591 assert_eq!(sid.len(), 36);
592 let (fm, body) = parse(&updated).unwrap();
593 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
594 assert_eq!(fm.agent.as_deref(), Some("claude"));
595 assert!(body.contains("Body"));
596 }
597
598 #[test]
599 fn ensure_session_existing_session() {
600 let content = "---\nagent_doc_session: existing-id\nagent: claude\n---\nBody\n";
601 let (updated, sid) = ensure_session(content).unwrap();
602 assert_eq!(sid, "existing-id");
603 assert_eq!(updated, content);
605 }
606
607 #[test]
608 fn parse_legacy_session_field() {
609 let content = "---\nsession: legacy-id\nagent: claude\n---\nBody\n";
611 let (fm, body) = parse(content).unwrap();
612 assert_eq!(fm.session.as_deref(), Some("legacy-id"));
613 assert_eq!(fm.agent.as_deref(), Some("claude"));
614 assert!(body.contains("Body"));
615 }
616
617 #[test]
618 fn parse_agent_doc_mode_canonical() {
619 let content = "---\nagent_doc_mode: template\n---\nBody\n";
620 let (fm, _) = parse(content).unwrap();
621 assert_eq!(fm.mode.as_deref(), Some("template"));
622 }
623
624 #[test]
625 fn parse_mode_shorthand_alias() {
626 let content = "---\nmode: template\n---\nBody\n";
627 let (fm, _) = parse(content).unwrap();
628 assert_eq!(fm.mode.as_deref(), Some("template"));
629 }
630
631 #[test]
632 fn parse_response_mode_legacy_alias() {
633 let content = "---\nresponse_mode: template\n---\nBody\n";
634 let (fm, _) = parse(content).unwrap();
635 assert_eq!(fm.mode.as_deref(), Some("template"));
636 }
637
638 #[test]
639 fn write_uses_agent_doc_mode_field() {
640 #[allow(deprecated)]
641 let fm = Frontmatter {
642 mode: Some("template".to_string()),
643 ..Default::default()
644 };
645 let result = write(&fm, "body\n").unwrap();
646 assert!(result.contains("agent_doc_mode:"));
647 assert!(!result.contains("response_mode:"));
648 assert!(!result.contains("\nmode:"));
649 }
650
651 #[test]
652 fn write_uses_new_field_name() {
653 let fm = Frontmatter {
654 session: Some("test-id".to_string()),
655 ..Default::default()
656 };
657 let result = write(&fm, "body\n").unwrap();
658 assert!(result.contains("agent_doc_session:"));
659 assert!(!result.contains("\nsession:"));
660 }
661
662 #[test]
665 fn resolve_mode_defaults() {
666 let fm = Frontmatter::default();
667 let resolved = fm.resolve_mode();
668 assert_eq!(resolved.format, AgentDocFormat::Template);
669 assert_eq!(resolved.write, AgentDocWrite::Crdt);
670 }
671
672 #[test]
673 fn resolve_mode_from_deprecated_append() {
674 let content = "---\nagent_doc_mode: append\n---\nBody\n";
675 let (fm, _) = parse(content).unwrap();
676 let resolved = fm.resolve_mode();
677 assert_eq!(resolved.format, AgentDocFormat::Append);
678 assert_eq!(resolved.write, AgentDocWrite::Crdt);
679 }
680
681 #[test]
682 fn resolve_mode_from_deprecated_template() {
683 let content = "---\nagent_doc_mode: template\n---\nBody\n";
684 let (fm, _) = parse(content).unwrap();
685 let resolved = fm.resolve_mode();
686 assert_eq!(resolved.format, AgentDocFormat::Template);
687 assert_eq!(resolved.write, AgentDocWrite::Crdt);
688 }
689
690 #[test]
691 fn resolve_mode_from_deprecated_stream() {
692 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
693 let (fm, _) = parse(content).unwrap();
694 let resolved = fm.resolve_mode();
695 assert_eq!(resolved.format, AgentDocFormat::Template);
696 assert_eq!(resolved.write, AgentDocWrite::Crdt);
697 }
698
699 #[test]
700 fn resolve_mode_new_fields_override_deprecated() {
701 let content = "---\nagent_doc_mode: append\nagent_doc_format: template\nagent_doc_write: merge\n---\nBody\n";
702 let (fm, _) = parse(content).unwrap();
703 let resolved = fm.resolve_mode();
704 assert_eq!(resolved.format, AgentDocFormat::Template);
705 assert_eq!(resolved.write, AgentDocWrite::Merge);
706 }
707
708 #[test]
709 fn resolve_mode_explicit_new_fields_only() {
710 let content = "---\nagent_doc_format: append\nagent_doc_write: crdt\n---\nBody\n";
711 let (fm, _) = parse(content).unwrap();
712 let resolved = fm.resolve_mode();
713 assert_eq!(resolved.format, AgentDocFormat::Append);
714 assert_eq!(resolved.write, AgentDocWrite::Crdt);
715 }
716
717 #[test]
718 fn resolve_mode_partial_new_field_format_only() {
719 let content = "---\nagent_doc_format: append\n---\nBody\n";
720 let (fm, _) = parse(content).unwrap();
721 let resolved = fm.resolve_mode();
722 assert_eq!(resolved.format, AgentDocFormat::Append);
723 assert_eq!(resolved.write, AgentDocWrite::Crdt); }
725
726 #[test]
727 fn resolve_mode_partial_new_field_write_only() {
728 let content = "---\nagent_doc_write: merge\n---\nBody\n";
729 let (fm, _) = parse(content).unwrap();
730 let resolved = fm.resolve_mode();
731 assert_eq!(resolved.format, AgentDocFormat::Template); assert_eq!(resolved.write, AgentDocWrite::Merge);
733 }
734
735 #[test]
736 fn resolve_mode_helper_methods() {
737 let fm = Frontmatter::default();
738 let resolved = fm.resolve_mode();
739 assert!(resolved.is_template());
740 assert!(!resolved.is_append());
741 assert!(resolved.is_crdt());
742 }
743
744 #[test]
745 fn parse_new_format_field() {
746 let content = "---\nagent_doc_format: template\n---\nBody\n";
747 let (fm, _) = parse(content).unwrap();
748 assert_eq!(fm.format, Some(AgentDocFormat::Template));
749 }
750
751 #[test]
752 fn parse_new_write_field() {
753 let content = "---\nagent_doc_write: crdt\n---\nBody\n";
754 let (fm, _) = parse(content).unwrap();
755 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
756 }
757
758 #[test]
759 fn write_uses_new_format_write_fields() {
760 let fm = Frontmatter {
761 format: Some(AgentDocFormat::Template),
762 write_mode: Some(AgentDocWrite::Crdt),
763 ..Default::default()
764 };
765 let result = write(&fm, "body\n").unwrap();
766 assert!(result.contains("agent_doc_format:"));
767 assert!(result.contains("agent_doc_write:"));
768 assert!(!result.contains("agent_doc_mode:"));
769 }
770
771 #[test]
772 fn set_format_and_write_clears_deprecated_mode() {
773 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
774 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
775 let (fm, _) = parse(&result).unwrap();
776 assert!(fm.mode.is_none());
777 assert_eq!(fm.format, Some(AgentDocFormat::Template));
778 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
779 }
780
781 #[test]
784 fn merge_fields_adds_new_field() {
785 let content = "---\nagent_doc_session: abc\n---\nBody\n";
786 let result = merge_fields(content, "model: opus").unwrap();
787 let (fm, body) = parse(&result).unwrap();
788 assert_eq!(fm.session.as_deref(), Some("abc"));
789 assert_eq!(fm.model.as_deref(), Some("opus"));
790 assert!(body.contains("Body"));
791 }
792
793 #[test]
794 fn merge_fields_updates_existing_field() {
795 let content = "---\nagent_doc_session: abc\nmodel: sonnet\n---\nBody\n";
796 let result = merge_fields(content, "model: opus").unwrap();
797 let (fm, _) = parse(&result).unwrap();
798 assert_eq!(fm.model.as_deref(), Some("opus"));
799 assert_eq!(fm.session.as_deref(), Some("abc"));
800 }
801
802 #[test]
803 fn merge_fields_multiple_fields() {
804 let content = "---\nagent_doc_session: abc\n---\nBody\n";
805 let result = merge_fields(content, "model: opus\nagent: claude\nbranch: main").unwrap();
806 let (fm, _) = parse(&result).unwrap();
807 assert_eq!(fm.model.as_deref(), Some("opus"));
808 assert_eq!(fm.agent.as_deref(), Some("claude"));
809 assert_eq!(fm.branch.as_deref(), Some("main"));
810 }
811
812 #[test]
813 fn merge_fields_format_enum() {
814 let content = "---\nagent_doc_session: abc\n---\nBody\n";
815 let result = merge_fields(content, "agent_doc_format: append").unwrap();
816 let (fm, _) = parse(&result).unwrap();
817 assert_eq!(fm.format, Some(AgentDocFormat::Append));
818 }
819
820 #[test]
821 fn merge_fields_write_enum() {
822 let content = "---\nagent_doc_session: abc\n---\nBody\n";
823 let result = merge_fields(content, "agent_doc_write: merge").unwrap();
824 let (fm, _) = parse(&result).unwrap();
825 assert_eq!(fm.write_mode, Some(AgentDocWrite::Merge));
826 }
827
828 #[test]
829 fn merge_fields_ignores_unknown() {
830 let content = "---\nagent_doc_session: abc\n---\nBody\n";
831 let result = merge_fields(content, "unknown_field: value\nmodel: opus").unwrap();
832 let (fm, _) = parse(&result).unwrap();
833 assert_eq!(fm.model.as_deref(), Some("opus"));
834 }
835
836 #[test]
837 fn merge_fields_preserves_body() {
838 let content = "---\nagent_doc_session: abc\n---\n# Title\n\nSome **markdown** content.\n";
839 let result = merge_fields(content, "model: opus").unwrap();
840 assert!(result.contains("# Title"));
841 assert!(result.contains("Some **markdown** content."));
842 }
843
844 #[test]
845 fn set_format_and_write_clears_deprecated() {
846 let content = "---\nagent_doc_mode: append\n---\nBody\n";
847 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
848 let (fm, _) = parse(&result).unwrap();
849 assert!(fm.mode.is_none());
850 assert_eq!(fm.format, Some(AgentDocFormat::Template));
851 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
852 }
853}