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 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub max_lines: Option<usize>,
187}
188
189#[derive(Debug, Default, Serialize, Deserialize)]
190pub struct Frontmatter {
191 #[serde(
194 default,
195 skip_serializing_if = "Option::is_none",
196 rename = "agent_doc_session",
197 alias = "session"
198 )]
199 pub session: Option<String>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub resume: Option<String>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub agent: Option<String>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub model: Option<String>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub branch: Option<String>,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub tmux_session: Option<String>,
217 #[serde(
221 default,
222 skip_serializing_if = "Option::is_none",
223 rename = "agent_doc_mode",
224 alias = "mode",
225 alias = "response_mode"
226 )]
227 pub mode: Option<String>,
228 #[serde(
230 default,
231 skip_serializing_if = "Option::is_none",
232 rename = "agent_doc_format"
233 )]
234 pub format: Option<AgentDocFormat>,
235 #[serde(
237 default,
238 skip_serializing_if = "Option::is_none",
239 rename = "agent_doc_write"
240 )]
241 pub write_mode: Option<AgentDocWrite>,
242 #[serde(
244 default,
245 skip_serializing_if = "Option::is_none",
246 rename = "agent_doc_stream"
247 )]
248 pub stream_config: Option<StreamConfig>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub claude_args: Option<String>,
253 #[serde(
256 default,
257 skip_serializing_if = "Option::is_none",
258 rename = "agent_doc_debounce"
259 )]
260 pub debounce_ms: Option<u64>,
261 #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "related_docs")]
264 pub links: Vec<String>,
265 #[serde(
269 default,
270 skip_serializing_if = "Option::is_none",
271 rename = "agent_doc_auto_compact"
272 )]
273 pub auto_compact: Option<usize>,
274}
275
276impl Frontmatter {
277 pub fn resolve_mode(&self) -> ResolvedMode {
284 let mut format = AgentDocFormat::Template;
286 let mut write = AgentDocWrite::Crdt;
287
288 if let Some(ref mode_str) = self.mode {
290 match mode_str.as_str() {
291 "append" => {
292 format = AgentDocFormat::Append;
293 }
295 "template" => {
296 format = AgentDocFormat::Template;
297 }
299 "stream" => {
300 format = AgentDocFormat::Template;
301 write = AgentDocWrite::Crdt;
302 }
303 _ => {} }
305 }
306
307 if let Some(f) = self.format {
309 format = f;
310 }
311 if let Some(w) = self.write_mode {
312 write = w;
313 }
314
315 ResolvedMode { format, write }
316 }
317}
318
319pub fn parse(content: &str) -> Result<(Frontmatter, &str)> {
322 if !content.starts_with("---\n") {
323 return Ok((Frontmatter::default(), content));
324 }
325 let rest = &content[4..]; let end = rest
327 .find("\n---\n")
328 .or_else(|| rest.find("\n---"))
329 .ok_or_else(|| anyhow::anyhow!("Unterminated frontmatter block"))?;
330 let yaml = &rest[..end];
331 let fm: Frontmatter = serde_yaml::from_str(yaml)?;
332 let body_start = 4 + end + 4; let body = if body_start <= content.len() {
334 &content[body_start..]
335 } else {
336 ""
337 };
338 Ok((fm, body))
339}
340
341pub fn write(fm: &Frontmatter, body: &str) -> Result<String> {
343 let yaml = serde_yaml::to_string(fm)?;
344 Ok(format!("---\n{}---\n{}", yaml, body))
345}
346
347pub fn set_session_id(content: &str, session_id: &str) -> Result<String> {
349 let (mut fm, body) = parse(content)?;
350 fm.session = Some(session_id.to_string());
351 write(&fm, body)
352}
353
354pub fn set_resume_id(content: &str, resume_id: &str) -> Result<String> {
356 let (mut fm, body) = parse(content)?;
357 fm.resume = Some(resume_id.to_string());
358 write(&fm, body)
359}
360
361pub fn set_format_and_write(
363 content: &str,
364 format: AgentDocFormat,
365 write_mode: AgentDocWrite,
366) -> Result<String> {
367 let (mut fm, body) = parse(content)?;
368 fm.format = Some(format);
369 fm.write_mode = Some(write_mode);
370 fm.mode = None;
371 write(&fm, body)
372}
373
374pub fn merge_fields(content: &str, yaml_fields: &str) -> Result<String> {
380 let (mut fm, body) = parse(content)?;
381 let patch: serde_yaml::Value = serde_yaml::from_str(yaml_fields)
382 .unwrap_or(serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
383 let mapping = patch.as_mapping().unwrap_or(&serde_yaml::Mapping::new()).clone();
384
385 for (key, value) in &mapping {
386 let key_str = key.as_str().unwrap_or("");
387 let val_str = || value.as_str().map(|s| s.to_string());
388 match key_str {
389 "agent_doc_session" | "session" => fm.session = val_str(),
390 "resume" => fm.resume = val_str(),
391 "agent" => fm.agent = val_str(),
392 "model" => fm.model = val_str(),
393 "branch" => fm.branch = val_str(),
394 "tmux_session" => fm.tmux_session = val_str(),
395 "agent_doc_mode" | "mode" | "response_mode" => fm.mode = val_str(),
396 "agent_doc_format" => {
397 if let Some(s) = value.as_str()
398 && let Ok(f) = serde_yaml::from_str::<AgentDocFormat>(&format!("\"{}\"", s))
399 {
400 fm.format = Some(f);
401 }
402 }
403 "agent_doc_write" => {
404 if let Some(s) = value.as_str()
405 && let Ok(w) = serde_yaml::from_str::<AgentDocWrite>(&format!("\"{}\"", s))
406 {
407 fm.write_mode = Some(w);
408 }
409 }
410 "claude_args" => fm.claude_args = val_str(),
411 _ => {
412 eprintln!("[frontmatter] ignoring unknown patch field: {}", key_str);
413 }
414 }
415 }
416
417 write(&fm, body)
418}
419
420pub fn set_tmux_session(content: &str, session_name: &str) -> Result<String> {
426 let (mut fm, body) = parse(content)?;
427 fm.tmux_session = Some(session_name.to_string());
428 write(&fm, body)
429}
430
431pub fn ensure_session(content: &str) -> Result<(String, String)> {
436 let (fm, _body) = parse(content)?;
437 if let Some(ref session_id) = fm.session {
438 return Ok((content.to_string(), session_id.clone()));
440 }
441 let session_id = Uuid::new_v4().to_string();
442 let updated = set_session_id(content, &session_id)?;
443 Ok((updated, session_id))
444}
445
446pub fn read_session_id(file: &std::path::Path) -> Option<String> {
448 let content = std::fs::read_to_string(file).ok()?;
449 let (fm, _) = parse(&content).ok()?;
450 fm.session
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn parse_no_frontmatter() {
459 let content = "# Hello\n\nBody text.\n";
460 let (fm, body) = parse(content).unwrap();
461 assert!(fm.session.is_none());
462 assert!(fm.agent.is_none());
463 assert!(fm.model.is_none());
464 assert!(fm.branch.is_none());
465 assert_eq!(body, content);
466 }
467
468 #[test]
469 fn parse_all_fields() {
470 let content = "---\nsession: abc-123\nagent: claude\nmodel: opus\nbranch: main\n---\nBody\n";
471 let (fm, body) = parse(content).unwrap();
472 assert_eq!(fm.session.as_deref(), Some("abc-123"));
473 assert_eq!(fm.agent.as_deref(), Some("claude"));
474 assert_eq!(fm.model.as_deref(), Some("opus"));
475 assert_eq!(fm.branch.as_deref(), Some("main"));
476 assert!(body.contains("Body"));
477 }
478
479 #[test]
480 fn parse_partial_fields() {
481 let content = "---\nsession: xyz\n---\n# Doc\n";
482 let (fm, body) = parse(content).unwrap();
483 assert_eq!(fm.session.as_deref(), Some("xyz"));
484 assert!(fm.agent.is_none());
485 assert!(body.contains("# Doc"));
486 }
487
488 #[test]
489 fn parse_null_fields() {
490 let content = "---\nsession: null\nagent: null\nmodel: null\nbranch: null\n---\nBody\n";
491 let (fm, body) = parse(content).unwrap();
492 assert!(fm.session.is_none());
493 assert!(fm.agent.is_none());
494 assert!(fm.model.is_none());
495 assert!(fm.branch.is_none());
496 assert!(body.contains("Body"));
497 }
498
499 #[test]
500 fn parse_unterminated_frontmatter() {
501 let content = "---\nsession: abc\nno closing block";
502 let err = parse(content).unwrap_err();
503 assert!(err.to_string().contains("Unterminated frontmatter"));
504 }
505
506 #[test]
507 fn parse_closing_at_eof() {
508 let content = "---\nsession: abc\n---";
509 let (fm, body) = parse(content).unwrap();
510 assert_eq!(fm.session.as_deref(), Some("abc"));
511 assert_eq!(body, "");
512 }
513
514 #[test]
515 fn parse_empty_body() {
516 let content = "---\nsession: abc\n---\n";
517 let (fm, _body) = parse(content).unwrap();
518 assert_eq!(fm.session.as_deref(), Some("abc"));
519 }
520
521 #[test]
522 fn write_roundtrip() {
523 let fm = Frontmatter {
525 session: Some("test-id".to_string()),
526 resume: Some("resume-id".to_string()),
527 agent: Some("claude".to_string()),
528 model: Some("opus".to_string()),
529 branch: Some("dev".to_string()),
530 tmux_session: None,
531 mode: None,
532 format: None,
533 write_mode: None,
534 stream_config: None,
535 claude_args: None,
536 debounce_ms: None,
537 links: vec![],
538 auto_compact: None,
539 };
540 let body = "# Hello\n\nBody text.\n";
541 let written = write(&fm, body).unwrap();
542 let (fm2, body2) = parse(&written).unwrap();
543 assert_eq!(fm2.session, fm.session);
544 assert_eq!(fm2.agent, fm.agent);
545 assert_eq!(fm2.model, fm.model);
546 assert_eq!(fm2.branch, fm.branch);
547 assert!(body2.contains("# Hello"));
549 assert!(body2.contains("Body text."));
550 }
551
552 #[test]
553 fn write_default_frontmatter() {
554 let fm = Frontmatter::default();
555 let result = write(&fm, "body\n").unwrap();
556 assert!(result.starts_with("---\n"));
557 assert!(result.ends_with("---\nbody\n"));
558 }
559
560 #[test]
561 fn write_preserves_body_content() {
562 let fm = Frontmatter::default();
563 let body = "# Title\n\nSome **markdown** with `code`.\n";
564 let result = write(&fm, body).unwrap();
565 assert!(result.contains("# Title"));
566 assert!(result.contains("Some **markdown** with `code`."));
567 }
568
569 #[test]
570 fn set_session_id_creates_frontmatter() {
571 let content = "# No frontmatter\n\nJust body.\n";
572 let result = set_session_id(content, "new-session").unwrap();
573 let (fm, body) = parse(&result).unwrap();
574 assert_eq!(fm.session.as_deref(), Some("new-session"));
575 assert!(body.contains("# No frontmatter"));
576 }
577
578 #[test]
579 fn set_session_id_updates_existing() {
580 let content = "---\nsession: old-id\nagent: claude\n---\nBody\n";
581 let result = set_session_id(content, "new-id").unwrap();
582 let (fm, body) = parse(&result).unwrap();
583 assert_eq!(fm.session.as_deref(), Some("new-id"));
584 assert_eq!(fm.agent.as_deref(), Some("claude"));
585 assert!(body.contains("Body"));
586 }
587
588 #[test]
589 fn set_session_id_preserves_other_fields() {
590 let content = "---\nsession: old\nagent: claude\nmodel: opus\nbranch: dev\n---\nBody\n";
591 let result = set_session_id(content, "new").unwrap();
592 let (fm, _) = parse(&result).unwrap();
593 assert_eq!(fm.session.as_deref(), Some("new"));
594 assert_eq!(fm.agent.as_deref(), Some("claude"));
595 assert_eq!(fm.model.as_deref(), Some("opus"));
596 assert_eq!(fm.branch.as_deref(), Some("dev"));
597 }
598
599 #[test]
600 fn ensure_session_no_frontmatter() {
601 let content = "# Hello\n\nBody.\n";
602 let (updated, sid) = ensure_session(content).unwrap();
603 assert_eq!(sid.len(), 36); let (fm, body) = parse(&updated).unwrap();
606 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
607 assert!(body.contains("# Hello"));
608 }
609
610 #[test]
611 fn ensure_session_null_session() {
612 let content = "---\nsession:\nagent: claude\n---\nBody\n";
613 let (updated, sid) = ensure_session(content).unwrap();
614 assert_eq!(sid.len(), 36);
615 let (fm, body) = parse(&updated).unwrap();
616 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
617 assert_eq!(fm.agent.as_deref(), Some("claude"));
618 assert!(body.contains("Body"));
619 }
620
621 #[test]
622 fn ensure_session_existing_session() {
623 let content = "---\nagent_doc_session: existing-id\nagent: claude\n---\nBody\n";
624 let (updated, sid) = ensure_session(content).unwrap();
625 assert_eq!(sid, "existing-id");
626 assert_eq!(updated, content);
628 }
629
630 #[test]
631 fn parse_legacy_session_field() {
632 let content = "---\nsession: legacy-id\nagent: claude\n---\nBody\n";
634 let (fm, body) = parse(content).unwrap();
635 assert_eq!(fm.session.as_deref(), Some("legacy-id"));
636 assert_eq!(fm.agent.as_deref(), Some("claude"));
637 assert!(body.contains("Body"));
638 }
639
640 #[test]
641 fn parse_agent_doc_mode_canonical() {
642 let content = "---\nagent_doc_mode: template\n---\nBody\n";
643 let (fm, _) = parse(content).unwrap();
644 assert_eq!(fm.mode.as_deref(), Some("template"));
645 }
646
647 #[test]
648 fn parse_mode_shorthand_alias() {
649 let content = "---\nmode: template\n---\nBody\n";
650 let (fm, _) = parse(content).unwrap();
651 assert_eq!(fm.mode.as_deref(), Some("template"));
652 }
653
654 #[test]
655 fn parse_response_mode_legacy_alias() {
656 let content = "---\nresponse_mode: template\n---\nBody\n";
657 let (fm, _) = parse(content).unwrap();
658 assert_eq!(fm.mode.as_deref(), Some("template"));
659 }
660
661 #[test]
662 fn write_uses_agent_doc_mode_field() {
663 #[allow(deprecated)]
664 let fm = Frontmatter {
665 mode: Some("template".to_string()),
666 ..Default::default()
667 };
668 let result = write(&fm, "body\n").unwrap();
669 assert!(result.contains("agent_doc_mode:"));
670 assert!(!result.contains("response_mode:"));
671 assert!(!result.contains("\nmode:"));
672 }
673
674 #[test]
675 fn write_uses_new_field_name() {
676 let fm = Frontmatter {
677 session: Some("test-id".to_string()),
678 ..Default::default()
679 };
680 let result = write(&fm, "body\n").unwrap();
681 assert!(result.contains("agent_doc_session:"));
682 assert!(!result.contains("\nsession:"));
683 }
684
685 #[test]
688 fn resolve_mode_defaults() {
689 let fm = Frontmatter::default();
690 let resolved = fm.resolve_mode();
691 assert_eq!(resolved.format, AgentDocFormat::Template);
692 assert_eq!(resolved.write, AgentDocWrite::Crdt);
693 }
694
695 #[test]
696 fn resolve_mode_from_deprecated_append() {
697 let content = "---\nagent_doc_mode: append\n---\nBody\n";
698 let (fm, _) = parse(content).unwrap();
699 let resolved = fm.resolve_mode();
700 assert_eq!(resolved.format, AgentDocFormat::Append);
701 assert_eq!(resolved.write, AgentDocWrite::Crdt);
702 }
703
704 #[test]
705 fn resolve_mode_from_deprecated_template() {
706 let content = "---\nagent_doc_mode: template\n---\nBody\n";
707 let (fm, _) = parse(content).unwrap();
708 let resolved = fm.resolve_mode();
709 assert_eq!(resolved.format, AgentDocFormat::Template);
710 assert_eq!(resolved.write, AgentDocWrite::Crdt);
711 }
712
713 #[test]
714 fn resolve_mode_from_deprecated_stream() {
715 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
716 let (fm, _) = parse(content).unwrap();
717 let resolved = fm.resolve_mode();
718 assert_eq!(resolved.format, AgentDocFormat::Template);
719 assert_eq!(resolved.write, AgentDocWrite::Crdt);
720 }
721
722 #[test]
723 fn resolve_mode_new_fields_override_deprecated() {
724 let content = "---\nagent_doc_mode: append\nagent_doc_format: template\nagent_doc_write: merge\n---\nBody\n";
725 let (fm, _) = parse(content).unwrap();
726 let resolved = fm.resolve_mode();
727 assert_eq!(resolved.format, AgentDocFormat::Template);
728 assert_eq!(resolved.write, AgentDocWrite::Merge);
729 }
730
731 #[test]
732 fn resolve_mode_explicit_new_fields_only() {
733 let content = "---\nagent_doc_format: append\nagent_doc_write: crdt\n---\nBody\n";
734 let (fm, _) = parse(content).unwrap();
735 let resolved = fm.resolve_mode();
736 assert_eq!(resolved.format, AgentDocFormat::Append);
737 assert_eq!(resolved.write, AgentDocWrite::Crdt);
738 }
739
740 #[test]
741 fn resolve_mode_partial_new_field_format_only() {
742 let content = "---\nagent_doc_format: append\n---\nBody\n";
743 let (fm, _) = parse(content).unwrap();
744 let resolved = fm.resolve_mode();
745 assert_eq!(resolved.format, AgentDocFormat::Append);
746 assert_eq!(resolved.write, AgentDocWrite::Crdt); }
748
749 #[test]
750 fn resolve_mode_partial_new_field_write_only() {
751 let content = "---\nagent_doc_write: merge\n---\nBody\n";
752 let (fm, _) = parse(content).unwrap();
753 let resolved = fm.resolve_mode();
754 assert_eq!(resolved.format, AgentDocFormat::Template); assert_eq!(resolved.write, AgentDocWrite::Merge);
756 }
757
758 #[test]
759 fn resolve_mode_helper_methods() {
760 let fm = Frontmatter::default();
761 let resolved = fm.resolve_mode();
762 assert!(resolved.is_template());
763 assert!(!resolved.is_append());
764 assert!(resolved.is_crdt());
765 }
766
767 #[test]
768 fn parse_new_format_field() {
769 let content = "---\nagent_doc_format: template\n---\nBody\n";
770 let (fm, _) = parse(content).unwrap();
771 assert_eq!(fm.format, Some(AgentDocFormat::Template));
772 }
773
774 #[test]
775 fn parse_new_write_field() {
776 let content = "---\nagent_doc_write: crdt\n---\nBody\n";
777 let (fm, _) = parse(content).unwrap();
778 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
779 }
780
781 #[test]
782 fn write_uses_new_format_write_fields() {
783 let fm = Frontmatter {
784 format: Some(AgentDocFormat::Template),
785 write_mode: Some(AgentDocWrite::Crdt),
786 ..Default::default()
787 };
788 let result = write(&fm, "body\n").unwrap();
789 assert!(result.contains("agent_doc_format:"));
790 assert!(result.contains("agent_doc_write:"));
791 assert!(!result.contains("agent_doc_mode:"));
792 }
793
794 #[test]
795 fn set_format_and_write_clears_deprecated_mode() {
796 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
797 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
798 let (fm, _) = parse(&result).unwrap();
799 assert!(fm.mode.is_none());
800 assert_eq!(fm.format, Some(AgentDocFormat::Template));
801 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
802 }
803
804 #[test]
807 fn merge_fields_adds_new_field() {
808 let content = "---\nagent_doc_session: abc\n---\nBody\n";
809 let result = merge_fields(content, "model: opus").unwrap();
810 let (fm, body) = parse(&result).unwrap();
811 assert_eq!(fm.session.as_deref(), Some("abc"));
812 assert_eq!(fm.model.as_deref(), Some("opus"));
813 assert!(body.contains("Body"));
814 }
815
816 #[test]
817 fn merge_fields_updates_existing_field() {
818 let content = "---\nagent_doc_session: abc\nmodel: sonnet\n---\nBody\n";
819 let result = merge_fields(content, "model: opus").unwrap();
820 let (fm, _) = parse(&result).unwrap();
821 assert_eq!(fm.model.as_deref(), Some("opus"));
822 assert_eq!(fm.session.as_deref(), Some("abc"));
823 }
824
825 #[test]
826 fn merge_fields_multiple_fields() {
827 let content = "---\nagent_doc_session: abc\n---\nBody\n";
828 let result = merge_fields(content, "model: opus\nagent: claude\nbranch: main").unwrap();
829 let (fm, _) = parse(&result).unwrap();
830 assert_eq!(fm.model.as_deref(), Some("opus"));
831 assert_eq!(fm.agent.as_deref(), Some("claude"));
832 assert_eq!(fm.branch.as_deref(), Some("main"));
833 }
834
835 #[test]
836 fn merge_fields_format_enum() {
837 let content = "---\nagent_doc_session: abc\n---\nBody\n";
838 let result = merge_fields(content, "agent_doc_format: append").unwrap();
839 let (fm, _) = parse(&result).unwrap();
840 assert_eq!(fm.format, Some(AgentDocFormat::Append));
841 }
842
843 #[test]
844 fn merge_fields_write_enum() {
845 let content = "---\nagent_doc_session: abc\n---\nBody\n";
846 let result = merge_fields(content, "agent_doc_write: merge").unwrap();
847 let (fm, _) = parse(&result).unwrap();
848 assert_eq!(fm.write_mode, Some(AgentDocWrite::Merge));
849 }
850
851 #[test]
852 fn merge_fields_ignores_unknown() {
853 let content = "---\nagent_doc_session: abc\n---\nBody\n";
854 let result = merge_fields(content, "unknown_field: value\nmodel: opus").unwrap();
855 let (fm, _) = parse(&result).unwrap();
856 assert_eq!(fm.model.as_deref(), Some("opus"));
857 }
858
859 #[test]
860 fn merge_fields_preserves_body() {
861 let content = "---\nagent_doc_session: abc\n---\n# Title\n\nSome **markdown** content.\n";
862 let result = merge_fields(content, "model: opus").unwrap();
863 assert!(result.contains("# Title"));
864 assert!(result.contains("Some **markdown** content."));
865 }
866
867 #[test]
868 fn set_format_and_write_clears_deprecated() {
869 let content = "---\nagent_doc_mode: append\n---\nBody\n";
870 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
871 let (fm, _) = parse(&result).unwrap();
872 assert!(fm.mode.is_none());
873 assert_eq!(fm.format, Some(AgentDocFormat::Template));
874 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
875 }
876}