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
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn parse_no_frontmatter() {
431 let content = "# Hello\n\nBody text.\n";
432 let (fm, body) = parse(content).unwrap();
433 assert!(fm.session.is_none());
434 assert!(fm.agent.is_none());
435 assert!(fm.model.is_none());
436 assert!(fm.branch.is_none());
437 assert_eq!(body, content);
438 }
439
440 #[test]
441 fn parse_all_fields() {
442 let content = "---\nsession: abc-123\nagent: claude\nmodel: opus\nbranch: main\n---\nBody\n";
443 let (fm, body) = parse(content).unwrap();
444 assert_eq!(fm.session.as_deref(), Some("abc-123"));
445 assert_eq!(fm.agent.as_deref(), Some("claude"));
446 assert_eq!(fm.model.as_deref(), Some("opus"));
447 assert_eq!(fm.branch.as_deref(), Some("main"));
448 assert!(body.contains("Body"));
449 }
450
451 #[test]
452 fn parse_partial_fields() {
453 let content = "---\nsession: xyz\n---\n# Doc\n";
454 let (fm, body) = parse(content).unwrap();
455 assert_eq!(fm.session.as_deref(), Some("xyz"));
456 assert!(fm.agent.is_none());
457 assert!(body.contains("# Doc"));
458 }
459
460 #[test]
461 fn parse_null_fields() {
462 let content = "---\nsession: null\nagent: null\nmodel: null\nbranch: null\n---\nBody\n";
463 let (fm, body) = parse(content).unwrap();
464 assert!(fm.session.is_none());
465 assert!(fm.agent.is_none());
466 assert!(fm.model.is_none());
467 assert!(fm.branch.is_none());
468 assert!(body.contains("Body"));
469 }
470
471 #[test]
472 fn parse_unterminated_frontmatter() {
473 let content = "---\nsession: abc\nno closing block";
474 let err = parse(content).unwrap_err();
475 assert!(err.to_string().contains("Unterminated frontmatter"));
476 }
477
478 #[test]
479 fn parse_closing_at_eof() {
480 let content = "---\nsession: abc\n---";
481 let (fm, body) = parse(content).unwrap();
482 assert_eq!(fm.session.as_deref(), Some("abc"));
483 assert_eq!(body, "");
484 }
485
486 #[test]
487 fn parse_empty_body() {
488 let content = "---\nsession: abc\n---\n";
489 let (fm, _body) = parse(content).unwrap();
490 assert_eq!(fm.session.as_deref(), Some("abc"));
491 }
492
493 #[test]
494 fn write_roundtrip() {
495 let fm = Frontmatter {
497 session: Some("test-id".to_string()),
498 resume: Some("resume-id".to_string()),
499 agent: Some("claude".to_string()),
500 model: Some("opus".to_string()),
501 branch: Some("dev".to_string()),
502 tmux_session: None,
503 mode: None,
504 format: None,
505 write_mode: None,
506 stream_config: None,
507 claude_args: None,
508 links: vec![],
509 };
510 let body = "# Hello\n\nBody text.\n";
511 let written = write(&fm, body).unwrap();
512 let (fm2, body2) = parse(&written).unwrap();
513 assert_eq!(fm2.session, fm.session);
514 assert_eq!(fm2.agent, fm.agent);
515 assert_eq!(fm2.model, fm.model);
516 assert_eq!(fm2.branch, fm.branch);
517 assert!(body2.contains("# Hello"));
519 assert!(body2.contains("Body text."));
520 }
521
522 #[test]
523 fn write_default_frontmatter() {
524 let fm = Frontmatter::default();
525 let result = write(&fm, "body\n").unwrap();
526 assert!(result.starts_with("---\n"));
527 assert!(result.ends_with("---\nbody\n"));
528 }
529
530 #[test]
531 fn write_preserves_body_content() {
532 let fm = Frontmatter::default();
533 let body = "# Title\n\nSome **markdown** with `code`.\n";
534 let result = write(&fm, body).unwrap();
535 assert!(result.contains("# Title"));
536 assert!(result.contains("Some **markdown** with `code`."));
537 }
538
539 #[test]
540 fn set_session_id_creates_frontmatter() {
541 let content = "# No frontmatter\n\nJust body.\n";
542 let result = set_session_id(content, "new-session").unwrap();
543 let (fm, body) = parse(&result).unwrap();
544 assert_eq!(fm.session.as_deref(), Some("new-session"));
545 assert!(body.contains("# No frontmatter"));
546 }
547
548 #[test]
549 fn set_session_id_updates_existing() {
550 let content = "---\nsession: old-id\nagent: claude\n---\nBody\n";
551 let result = set_session_id(content, "new-id").unwrap();
552 let (fm, body) = parse(&result).unwrap();
553 assert_eq!(fm.session.as_deref(), Some("new-id"));
554 assert_eq!(fm.agent.as_deref(), Some("claude"));
555 assert!(body.contains("Body"));
556 }
557
558 #[test]
559 fn set_session_id_preserves_other_fields() {
560 let content = "---\nsession: old\nagent: claude\nmodel: opus\nbranch: dev\n---\nBody\n";
561 let result = set_session_id(content, "new").unwrap();
562 let (fm, _) = parse(&result).unwrap();
563 assert_eq!(fm.session.as_deref(), Some("new"));
564 assert_eq!(fm.agent.as_deref(), Some("claude"));
565 assert_eq!(fm.model.as_deref(), Some("opus"));
566 assert_eq!(fm.branch.as_deref(), Some("dev"));
567 }
568
569 #[test]
570 fn ensure_session_no_frontmatter() {
571 let content = "# Hello\n\nBody.\n";
572 let (updated, sid) = ensure_session(content).unwrap();
573 assert_eq!(sid.len(), 36); let (fm, body) = parse(&updated).unwrap();
576 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
577 assert!(body.contains("# Hello"));
578 }
579
580 #[test]
581 fn ensure_session_null_session() {
582 let content = "---\nsession:\nagent: claude\n---\nBody\n";
583 let (updated, sid) = ensure_session(content).unwrap();
584 assert_eq!(sid.len(), 36);
585 let (fm, body) = parse(&updated).unwrap();
586 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
587 assert_eq!(fm.agent.as_deref(), Some("claude"));
588 assert!(body.contains("Body"));
589 }
590
591 #[test]
592 fn ensure_session_existing_session() {
593 let content = "---\nagent_doc_session: existing-id\nagent: claude\n---\nBody\n";
594 let (updated, sid) = ensure_session(content).unwrap();
595 assert_eq!(sid, "existing-id");
596 assert_eq!(updated, content);
598 }
599
600 #[test]
601 fn parse_legacy_session_field() {
602 let content = "---\nsession: legacy-id\nagent: claude\n---\nBody\n";
604 let (fm, body) = parse(content).unwrap();
605 assert_eq!(fm.session.as_deref(), Some("legacy-id"));
606 assert_eq!(fm.agent.as_deref(), Some("claude"));
607 assert!(body.contains("Body"));
608 }
609
610 #[test]
611 fn parse_agent_doc_mode_canonical() {
612 let content = "---\nagent_doc_mode: template\n---\nBody\n";
613 let (fm, _) = parse(content).unwrap();
614 assert_eq!(fm.mode.as_deref(), Some("template"));
615 }
616
617 #[test]
618 fn parse_mode_shorthand_alias() {
619 let content = "---\nmode: 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_response_mode_legacy_alias() {
626 let content = "---\nresponse_mode: 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 write_uses_agent_doc_mode_field() {
633 #[allow(deprecated)]
634 let fm = Frontmatter {
635 mode: Some("template".to_string()),
636 ..Default::default()
637 };
638 let result = write(&fm, "body\n").unwrap();
639 assert!(result.contains("agent_doc_mode:"));
640 assert!(!result.contains("response_mode:"));
641 assert!(!result.contains("\nmode:"));
642 }
643
644 #[test]
645 fn write_uses_new_field_name() {
646 let fm = Frontmatter {
647 session: Some("test-id".to_string()),
648 ..Default::default()
649 };
650 let result = write(&fm, "body\n").unwrap();
651 assert!(result.contains("agent_doc_session:"));
652 assert!(!result.contains("\nsession:"));
653 }
654
655 #[test]
658 fn resolve_mode_defaults() {
659 let fm = Frontmatter::default();
660 let resolved = fm.resolve_mode();
661 assert_eq!(resolved.format, AgentDocFormat::Template);
662 assert_eq!(resolved.write, AgentDocWrite::Crdt);
663 }
664
665 #[test]
666 fn resolve_mode_from_deprecated_append() {
667 let content = "---\nagent_doc_mode: append\n---\nBody\n";
668 let (fm, _) = parse(content).unwrap();
669 let resolved = fm.resolve_mode();
670 assert_eq!(resolved.format, AgentDocFormat::Append);
671 assert_eq!(resolved.write, AgentDocWrite::Crdt);
672 }
673
674 #[test]
675 fn resolve_mode_from_deprecated_template() {
676 let content = "---\nagent_doc_mode: template\n---\nBody\n";
677 let (fm, _) = parse(content).unwrap();
678 let resolved = fm.resolve_mode();
679 assert_eq!(resolved.format, AgentDocFormat::Template);
680 assert_eq!(resolved.write, AgentDocWrite::Crdt);
681 }
682
683 #[test]
684 fn resolve_mode_from_deprecated_stream() {
685 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
686 let (fm, _) = parse(content).unwrap();
687 let resolved = fm.resolve_mode();
688 assert_eq!(resolved.format, AgentDocFormat::Template);
689 assert_eq!(resolved.write, AgentDocWrite::Crdt);
690 }
691
692 #[test]
693 fn resolve_mode_new_fields_override_deprecated() {
694 let content = "---\nagent_doc_mode: append\nagent_doc_format: template\nagent_doc_write: merge\n---\nBody\n";
695 let (fm, _) = parse(content).unwrap();
696 let resolved = fm.resolve_mode();
697 assert_eq!(resolved.format, AgentDocFormat::Template);
698 assert_eq!(resolved.write, AgentDocWrite::Merge);
699 }
700
701 #[test]
702 fn resolve_mode_explicit_new_fields_only() {
703 let content = "---\nagent_doc_format: append\nagent_doc_write: crdt\n---\nBody\n";
704 let (fm, _) = parse(content).unwrap();
705 let resolved = fm.resolve_mode();
706 assert_eq!(resolved.format, AgentDocFormat::Append);
707 assert_eq!(resolved.write, AgentDocWrite::Crdt);
708 }
709
710 #[test]
711 fn resolve_mode_partial_new_field_format_only() {
712 let content = "---\nagent_doc_format: append\n---\nBody\n";
713 let (fm, _) = parse(content).unwrap();
714 let resolved = fm.resolve_mode();
715 assert_eq!(resolved.format, AgentDocFormat::Append);
716 assert_eq!(resolved.write, AgentDocWrite::Crdt); }
718
719 #[test]
720 fn resolve_mode_partial_new_field_write_only() {
721 let content = "---\nagent_doc_write: merge\n---\nBody\n";
722 let (fm, _) = parse(content).unwrap();
723 let resolved = fm.resolve_mode();
724 assert_eq!(resolved.format, AgentDocFormat::Template); assert_eq!(resolved.write, AgentDocWrite::Merge);
726 }
727
728 #[test]
729 fn resolve_mode_helper_methods() {
730 let fm = Frontmatter::default();
731 let resolved = fm.resolve_mode();
732 assert!(resolved.is_template());
733 assert!(!resolved.is_append());
734 assert!(resolved.is_crdt());
735 }
736
737 #[test]
738 fn parse_new_format_field() {
739 let content = "---\nagent_doc_format: template\n---\nBody\n";
740 let (fm, _) = parse(content).unwrap();
741 assert_eq!(fm.format, Some(AgentDocFormat::Template));
742 }
743
744 #[test]
745 fn parse_new_write_field() {
746 let content = "---\nagent_doc_write: crdt\n---\nBody\n";
747 let (fm, _) = parse(content).unwrap();
748 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
749 }
750
751 #[test]
752 fn write_uses_new_format_write_fields() {
753 let fm = Frontmatter {
754 format: Some(AgentDocFormat::Template),
755 write_mode: Some(AgentDocWrite::Crdt),
756 ..Default::default()
757 };
758 let result = write(&fm, "body\n").unwrap();
759 assert!(result.contains("agent_doc_format:"));
760 assert!(result.contains("agent_doc_write:"));
761 assert!(!result.contains("agent_doc_mode:"));
762 }
763
764 #[test]
765 fn set_format_and_write_clears_deprecated_mode() {
766 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
767 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
768 let (fm, _) = parse(&result).unwrap();
769 assert!(fm.mode.is_none());
770 assert_eq!(fm.format, Some(AgentDocFormat::Template));
771 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
772 }
773
774 #[test]
777 fn merge_fields_adds_new_field() {
778 let content = "---\nagent_doc_session: abc\n---\nBody\n";
779 let result = merge_fields(content, "model: opus").unwrap();
780 let (fm, body) = parse(&result).unwrap();
781 assert_eq!(fm.session.as_deref(), Some("abc"));
782 assert_eq!(fm.model.as_deref(), Some("opus"));
783 assert!(body.contains("Body"));
784 }
785
786 #[test]
787 fn merge_fields_updates_existing_field() {
788 let content = "---\nagent_doc_session: abc\nmodel: sonnet\n---\nBody\n";
789 let result = merge_fields(content, "model: opus").unwrap();
790 let (fm, _) = parse(&result).unwrap();
791 assert_eq!(fm.model.as_deref(), Some("opus"));
792 assert_eq!(fm.session.as_deref(), Some("abc"));
793 }
794
795 #[test]
796 fn merge_fields_multiple_fields() {
797 let content = "---\nagent_doc_session: abc\n---\nBody\n";
798 let result = merge_fields(content, "model: opus\nagent: claude\nbranch: main").unwrap();
799 let (fm, _) = parse(&result).unwrap();
800 assert_eq!(fm.model.as_deref(), Some("opus"));
801 assert_eq!(fm.agent.as_deref(), Some("claude"));
802 assert_eq!(fm.branch.as_deref(), Some("main"));
803 }
804
805 #[test]
806 fn merge_fields_format_enum() {
807 let content = "---\nagent_doc_session: abc\n---\nBody\n";
808 let result = merge_fields(content, "agent_doc_format: append").unwrap();
809 let (fm, _) = parse(&result).unwrap();
810 assert_eq!(fm.format, Some(AgentDocFormat::Append));
811 }
812
813 #[test]
814 fn merge_fields_write_enum() {
815 let content = "---\nagent_doc_session: abc\n---\nBody\n";
816 let result = merge_fields(content, "agent_doc_write: merge").unwrap();
817 let (fm, _) = parse(&result).unwrap();
818 assert_eq!(fm.write_mode, Some(AgentDocWrite::Merge));
819 }
820
821 #[test]
822 fn merge_fields_ignores_unknown() {
823 let content = "---\nagent_doc_session: abc\n---\nBody\n";
824 let result = merge_fields(content, "unknown_field: value\nmodel: opus").unwrap();
825 let (fm, _) = parse(&result).unwrap();
826 assert_eq!(fm.model.as_deref(), Some("opus"));
827 }
828
829 #[test]
830 fn merge_fields_preserves_body() {
831 let content = "---\nagent_doc_session: abc\n---\n# Title\n\nSome **markdown** content.\n";
832 let result = merge_fields(content, "model: opus").unwrap();
833 assert!(result.contains("# Title"));
834 assert!(result.contains("Some **markdown** content."));
835 }
836
837 #[test]
838 fn set_format_and_write_clears_deprecated() {
839 let content = "---\nagent_doc_mode: append\n---\nBody\n";
840 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
841 let (fm, _) = parse(&result).unwrap();
842 assert!(fm.mode.is_none());
843 assert_eq!(fm.format, Some(AgentDocFormat::Template));
844 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
845 }
846}