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}
250
251impl Frontmatter {
252 pub fn resolve_mode(&self) -> ResolvedMode {
259 let mut format = AgentDocFormat::Template;
261 let mut write = AgentDocWrite::Crdt;
262
263 if let Some(ref mode_str) = self.mode {
265 match mode_str.as_str() {
266 "append" => {
267 format = AgentDocFormat::Append;
268 }
270 "template" => {
271 format = AgentDocFormat::Template;
272 }
274 "stream" => {
275 format = AgentDocFormat::Template;
276 write = AgentDocWrite::Crdt;
277 }
278 _ => {} }
280 }
281
282 if let Some(f) = self.format {
284 format = f;
285 }
286 if let Some(w) = self.write_mode {
287 write = w;
288 }
289
290 ResolvedMode { format, write }
291 }
292}
293
294pub fn parse(content: &str) -> Result<(Frontmatter, &str)> {
297 if !content.starts_with("---\n") {
298 return Ok((Frontmatter::default(), content));
299 }
300 let rest = &content[4..]; let end = rest
302 .find("\n---\n")
303 .or_else(|| rest.find("\n---"))
304 .ok_or_else(|| anyhow::anyhow!("Unterminated frontmatter block"))?;
305 let yaml = &rest[..end];
306 let fm: Frontmatter = serde_yaml::from_str(yaml)?;
307 let body_start = 4 + end + 4; let body = if body_start <= content.len() {
309 &content[body_start..]
310 } else {
311 ""
312 };
313 Ok((fm, body))
314}
315
316pub fn write(fm: &Frontmatter, body: &str) -> Result<String> {
318 let yaml = serde_yaml::to_string(fm)?;
319 Ok(format!("---\n{}---\n{}", yaml, body))
320}
321
322pub fn set_session_id(content: &str, session_id: &str) -> Result<String> {
324 let (mut fm, body) = parse(content)?;
325 fm.session = Some(session_id.to_string());
326 write(&fm, body)
327}
328
329pub fn set_resume_id(content: &str, resume_id: &str) -> Result<String> {
331 let (mut fm, body) = parse(content)?;
332 fm.resume = Some(resume_id.to_string());
333 write(&fm, body)
334}
335
336pub fn set_format_and_write(
338 content: &str,
339 format: AgentDocFormat,
340 write_mode: AgentDocWrite,
341) -> Result<String> {
342 let (mut fm, body) = parse(content)?;
343 fm.format = Some(format);
344 fm.write_mode = Some(write_mode);
345 fm.mode = None;
346 write(&fm, body)
347}
348
349pub fn merge_fields(content: &str, yaml_fields: &str) -> Result<String> {
355 let (mut fm, body) = parse(content)?;
356 let patch: serde_yaml::Value = serde_yaml::from_str(yaml_fields)
357 .unwrap_or(serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
358 let mapping = patch.as_mapping().unwrap_or(&serde_yaml::Mapping::new()).clone();
359
360 for (key, value) in &mapping {
361 let key_str = key.as_str().unwrap_or("");
362 let val_str = || value.as_str().map(|s| s.to_string());
363 match key_str {
364 "agent_doc_session" | "session" => fm.session = val_str(),
365 "resume" => fm.resume = val_str(),
366 "agent" => fm.agent = val_str(),
367 "model" => fm.model = val_str(),
368 "branch" => fm.branch = val_str(),
369 "tmux_session" => fm.tmux_session = val_str(),
370 "agent_doc_mode" | "mode" | "response_mode" => fm.mode = val_str(),
371 "agent_doc_format" => {
372 if let Some(s) = value.as_str()
373 && let Ok(f) = serde_yaml::from_str::<AgentDocFormat>(&format!("\"{}\"", s))
374 {
375 fm.format = Some(f);
376 }
377 }
378 "agent_doc_write" => {
379 if let Some(s) = value.as_str()
380 && let Ok(w) = serde_yaml::from_str::<AgentDocWrite>(&format!("\"{}\"", s))
381 {
382 fm.write_mode = Some(w);
383 }
384 }
385 "claude_args" => fm.claude_args = val_str(),
386 _ => {
387 eprintln!("[frontmatter] ignoring unknown patch field: {}", key_str);
388 }
389 }
390 }
391
392 write(&fm, body)
393}
394
395pub fn set_tmux_session(content: &str, session_name: &str) -> Result<String> {
401 let (mut fm, body) = parse(content)?;
402 fm.tmux_session = Some(session_name.to_string());
403 write(&fm, body)
404}
405
406pub fn ensure_session(content: &str) -> Result<(String, String)> {
411 let (fm, _body) = parse(content)?;
412 if let Some(ref session_id) = fm.session {
413 return Ok((content.to_string(), session_id.clone()));
415 }
416 let session_id = Uuid::new_v4().to_string();
417 let updated = set_session_id(content, &session_id)?;
418 Ok((updated, session_id))
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn parse_no_frontmatter() {
427 let content = "# Hello\n\nBody text.\n";
428 let (fm, body) = parse(content).unwrap();
429 assert!(fm.session.is_none());
430 assert!(fm.agent.is_none());
431 assert!(fm.model.is_none());
432 assert!(fm.branch.is_none());
433 assert_eq!(body, content);
434 }
435
436 #[test]
437 fn parse_all_fields() {
438 let content = "---\nsession: abc-123\nagent: claude\nmodel: opus\nbranch: main\n---\nBody\n";
439 let (fm, body) = parse(content).unwrap();
440 assert_eq!(fm.session.as_deref(), Some("abc-123"));
441 assert_eq!(fm.agent.as_deref(), Some("claude"));
442 assert_eq!(fm.model.as_deref(), Some("opus"));
443 assert_eq!(fm.branch.as_deref(), Some("main"));
444 assert!(body.contains("Body"));
445 }
446
447 #[test]
448 fn parse_partial_fields() {
449 let content = "---\nsession: xyz\n---\n# Doc\n";
450 let (fm, body) = parse(content).unwrap();
451 assert_eq!(fm.session.as_deref(), Some("xyz"));
452 assert!(fm.agent.is_none());
453 assert!(body.contains("# Doc"));
454 }
455
456 #[test]
457 fn parse_null_fields() {
458 let content = "---\nsession: null\nagent: null\nmodel: null\nbranch: null\n---\nBody\n";
459 let (fm, body) = parse(content).unwrap();
460 assert!(fm.session.is_none());
461 assert!(fm.agent.is_none());
462 assert!(fm.model.is_none());
463 assert!(fm.branch.is_none());
464 assert!(body.contains("Body"));
465 }
466
467 #[test]
468 fn parse_unterminated_frontmatter() {
469 let content = "---\nsession: abc\nno closing block";
470 let err = parse(content).unwrap_err();
471 assert!(err.to_string().contains("Unterminated frontmatter"));
472 }
473
474 #[test]
475 fn parse_closing_at_eof() {
476 let content = "---\nsession: abc\n---";
477 let (fm, body) = parse(content).unwrap();
478 assert_eq!(fm.session.as_deref(), Some("abc"));
479 assert_eq!(body, "");
480 }
481
482 #[test]
483 fn parse_empty_body() {
484 let content = "---\nsession: abc\n---\n";
485 let (fm, _body) = parse(content).unwrap();
486 assert_eq!(fm.session.as_deref(), Some("abc"));
487 }
488
489 #[test]
490 fn write_roundtrip() {
491 let fm = Frontmatter {
493 session: Some("test-id".to_string()),
494 resume: Some("resume-id".to_string()),
495 agent: Some("claude".to_string()),
496 model: Some("opus".to_string()),
497 branch: Some("dev".to_string()),
498 tmux_session: None,
499 mode: None,
500 format: None,
501 write_mode: None,
502 stream_config: None,
503 claude_args: None,
504 };
505 let body = "# Hello\n\nBody text.\n";
506 let written = write(&fm, body).unwrap();
507 let (fm2, body2) = parse(&written).unwrap();
508 assert_eq!(fm2.session, fm.session);
509 assert_eq!(fm2.agent, fm.agent);
510 assert_eq!(fm2.model, fm.model);
511 assert_eq!(fm2.branch, fm.branch);
512 assert!(body2.contains("# Hello"));
514 assert!(body2.contains("Body text."));
515 }
516
517 #[test]
518 fn write_default_frontmatter() {
519 let fm = Frontmatter::default();
520 let result = write(&fm, "body\n").unwrap();
521 assert!(result.starts_with("---\n"));
522 assert!(result.ends_with("---\nbody\n"));
523 }
524
525 #[test]
526 fn write_preserves_body_content() {
527 let fm = Frontmatter::default();
528 let body = "# Title\n\nSome **markdown** with `code`.\n";
529 let result = write(&fm, body).unwrap();
530 assert!(result.contains("# Title"));
531 assert!(result.contains("Some **markdown** with `code`."));
532 }
533
534 #[test]
535 fn set_session_id_creates_frontmatter() {
536 let content = "# No frontmatter\n\nJust body.\n";
537 let result = set_session_id(content, "new-session").unwrap();
538 let (fm, body) = parse(&result).unwrap();
539 assert_eq!(fm.session.as_deref(), Some("new-session"));
540 assert!(body.contains("# No frontmatter"));
541 }
542
543 #[test]
544 fn set_session_id_updates_existing() {
545 let content = "---\nsession: old-id\nagent: claude\n---\nBody\n";
546 let result = set_session_id(content, "new-id").unwrap();
547 let (fm, body) = parse(&result).unwrap();
548 assert_eq!(fm.session.as_deref(), Some("new-id"));
549 assert_eq!(fm.agent.as_deref(), Some("claude"));
550 assert!(body.contains("Body"));
551 }
552
553 #[test]
554 fn set_session_id_preserves_other_fields() {
555 let content = "---\nsession: old\nagent: claude\nmodel: opus\nbranch: dev\n---\nBody\n";
556 let result = set_session_id(content, "new").unwrap();
557 let (fm, _) = parse(&result).unwrap();
558 assert_eq!(fm.session.as_deref(), Some("new"));
559 assert_eq!(fm.agent.as_deref(), Some("claude"));
560 assert_eq!(fm.model.as_deref(), Some("opus"));
561 assert_eq!(fm.branch.as_deref(), Some("dev"));
562 }
563
564 #[test]
565 fn ensure_session_no_frontmatter() {
566 let content = "# Hello\n\nBody.\n";
567 let (updated, sid) = ensure_session(content).unwrap();
568 assert_eq!(sid.len(), 36); let (fm, body) = parse(&updated).unwrap();
571 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
572 assert!(body.contains("# Hello"));
573 }
574
575 #[test]
576 fn ensure_session_null_session() {
577 let content = "---\nsession:\nagent: claude\n---\nBody\n";
578 let (updated, sid) = ensure_session(content).unwrap();
579 assert_eq!(sid.len(), 36);
580 let (fm, body) = parse(&updated).unwrap();
581 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
582 assert_eq!(fm.agent.as_deref(), Some("claude"));
583 assert!(body.contains("Body"));
584 }
585
586 #[test]
587 fn ensure_session_existing_session() {
588 let content = "---\nagent_doc_session: existing-id\nagent: claude\n---\nBody\n";
589 let (updated, sid) = ensure_session(content).unwrap();
590 assert_eq!(sid, "existing-id");
591 assert_eq!(updated, content);
593 }
594
595 #[test]
596 fn parse_legacy_session_field() {
597 let content = "---\nsession: legacy-id\nagent: claude\n---\nBody\n";
599 let (fm, body) = parse(content).unwrap();
600 assert_eq!(fm.session.as_deref(), Some("legacy-id"));
601 assert_eq!(fm.agent.as_deref(), Some("claude"));
602 assert!(body.contains("Body"));
603 }
604
605 #[test]
606 fn parse_agent_doc_mode_canonical() {
607 let content = "---\nagent_doc_mode: template\n---\nBody\n";
608 let (fm, _) = parse(content).unwrap();
609 assert_eq!(fm.mode.as_deref(), Some("template"));
610 }
611
612 #[test]
613 fn parse_mode_shorthand_alias() {
614 let content = "---\nmode: template\n---\nBody\n";
615 let (fm, _) = parse(content).unwrap();
616 assert_eq!(fm.mode.as_deref(), Some("template"));
617 }
618
619 #[test]
620 fn parse_response_mode_legacy_alias() {
621 let content = "---\nresponse_mode: template\n---\nBody\n";
622 let (fm, _) = parse(content).unwrap();
623 assert_eq!(fm.mode.as_deref(), Some("template"));
624 }
625
626 #[test]
627 fn write_uses_agent_doc_mode_field() {
628 #[allow(deprecated)]
629 let fm = Frontmatter {
630 mode: Some("template".to_string()),
631 ..Default::default()
632 };
633 let result = write(&fm, "body\n").unwrap();
634 assert!(result.contains("agent_doc_mode:"));
635 assert!(!result.contains("response_mode:"));
636 assert!(!result.contains("\nmode:"));
637 }
638
639 #[test]
640 fn write_uses_new_field_name() {
641 let fm = Frontmatter {
642 session: Some("test-id".to_string()),
643 ..Default::default()
644 };
645 let result = write(&fm, "body\n").unwrap();
646 assert!(result.contains("agent_doc_session:"));
647 assert!(!result.contains("\nsession:"));
648 }
649
650 #[test]
653 fn resolve_mode_defaults() {
654 let fm = Frontmatter::default();
655 let resolved = fm.resolve_mode();
656 assert_eq!(resolved.format, AgentDocFormat::Template);
657 assert_eq!(resolved.write, AgentDocWrite::Crdt);
658 }
659
660 #[test]
661 fn resolve_mode_from_deprecated_append() {
662 let content = "---\nagent_doc_mode: append\n---\nBody\n";
663 let (fm, _) = parse(content).unwrap();
664 let resolved = fm.resolve_mode();
665 assert_eq!(resolved.format, AgentDocFormat::Append);
666 assert_eq!(resolved.write, AgentDocWrite::Crdt);
667 }
668
669 #[test]
670 fn resolve_mode_from_deprecated_template() {
671 let content = "---\nagent_doc_mode: template\n---\nBody\n";
672 let (fm, _) = parse(content).unwrap();
673 let resolved = fm.resolve_mode();
674 assert_eq!(resolved.format, AgentDocFormat::Template);
675 assert_eq!(resolved.write, AgentDocWrite::Crdt);
676 }
677
678 #[test]
679 fn resolve_mode_from_deprecated_stream() {
680 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
681 let (fm, _) = parse(content).unwrap();
682 let resolved = fm.resolve_mode();
683 assert_eq!(resolved.format, AgentDocFormat::Template);
684 assert_eq!(resolved.write, AgentDocWrite::Crdt);
685 }
686
687 #[test]
688 fn resolve_mode_new_fields_override_deprecated() {
689 let content = "---\nagent_doc_mode: append\nagent_doc_format: template\nagent_doc_write: merge\n---\nBody\n";
690 let (fm, _) = parse(content).unwrap();
691 let resolved = fm.resolve_mode();
692 assert_eq!(resolved.format, AgentDocFormat::Template);
693 assert_eq!(resolved.write, AgentDocWrite::Merge);
694 }
695
696 #[test]
697 fn resolve_mode_explicit_new_fields_only() {
698 let content = "---\nagent_doc_format: append\nagent_doc_write: crdt\n---\nBody\n";
699 let (fm, _) = parse(content).unwrap();
700 let resolved = fm.resolve_mode();
701 assert_eq!(resolved.format, AgentDocFormat::Append);
702 assert_eq!(resolved.write, AgentDocWrite::Crdt);
703 }
704
705 #[test]
706 fn resolve_mode_partial_new_field_format_only() {
707 let content = "---\nagent_doc_format: append\n---\nBody\n";
708 let (fm, _) = parse(content).unwrap();
709 let resolved = fm.resolve_mode();
710 assert_eq!(resolved.format, AgentDocFormat::Append);
711 assert_eq!(resolved.write, AgentDocWrite::Crdt); }
713
714 #[test]
715 fn resolve_mode_partial_new_field_write_only() {
716 let content = "---\nagent_doc_write: merge\n---\nBody\n";
717 let (fm, _) = parse(content).unwrap();
718 let resolved = fm.resolve_mode();
719 assert_eq!(resolved.format, AgentDocFormat::Template); assert_eq!(resolved.write, AgentDocWrite::Merge);
721 }
722
723 #[test]
724 fn resolve_mode_helper_methods() {
725 let fm = Frontmatter::default();
726 let resolved = fm.resolve_mode();
727 assert!(resolved.is_template());
728 assert!(!resolved.is_append());
729 assert!(resolved.is_crdt());
730 }
731
732 #[test]
733 fn parse_new_format_field() {
734 let content = "---\nagent_doc_format: template\n---\nBody\n";
735 let (fm, _) = parse(content).unwrap();
736 assert_eq!(fm.format, Some(AgentDocFormat::Template));
737 }
738
739 #[test]
740 fn parse_new_write_field() {
741 let content = "---\nagent_doc_write: crdt\n---\nBody\n";
742 let (fm, _) = parse(content).unwrap();
743 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
744 }
745
746 #[test]
747 fn write_uses_new_format_write_fields() {
748 let fm = Frontmatter {
749 format: Some(AgentDocFormat::Template),
750 write_mode: Some(AgentDocWrite::Crdt),
751 ..Default::default()
752 };
753 let result = write(&fm, "body\n").unwrap();
754 assert!(result.contains("agent_doc_format:"));
755 assert!(result.contains("agent_doc_write:"));
756 assert!(!result.contains("agent_doc_mode:"));
757 }
758
759 #[test]
760 fn set_format_and_write_clears_deprecated_mode() {
761 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
762 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
763 let (fm, _) = parse(&result).unwrap();
764 assert!(fm.mode.is_none());
765 assert_eq!(fm.format, Some(AgentDocFormat::Template));
766 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
767 }
768
769 #[test]
772 fn merge_fields_adds_new_field() {
773 let content = "---\nagent_doc_session: abc\n---\nBody\n";
774 let result = merge_fields(content, "model: opus").unwrap();
775 let (fm, body) = parse(&result).unwrap();
776 assert_eq!(fm.session.as_deref(), Some("abc"));
777 assert_eq!(fm.model.as_deref(), Some("opus"));
778 assert!(body.contains("Body"));
779 }
780
781 #[test]
782 fn merge_fields_updates_existing_field() {
783 let content = "---\nagent_doc_session: abc\nmodel: sonnet\n---\nBody\n";
784 let result = merge_fields(content, "model: opus").unwrap();
785 let (fm, _) = parse(&result).unwrap();
786 assert_eq!(fm.model.as_deref(), Some("opus"));
787 assert_eq!(fm.session.as_deref(), Some("abc"));
788 }
789
790 #[test]
791 fn merge_fields_multiple_fields() {
792 let content = "---\nagent_doc_session: abc\n---\nBody\n";
793 let result = merge_fields(content, "model: opus\nagent: claude\nbranch: main").unwrap();
794 let (fm, _) = parse(&result).unwrap();
795 assert_eq!(fm.model.as_deref(), Some("opus"));
796 assert_eq!(fm.agent.as_deref(), Some("claude"));
797 assert_eq!(fm.branch.as_deref(), Some("main"));
798 }
799
800 #[test]
801 fn merge_fields_format_enum() {
802 let content = "---\nagent_doc_session: abc\n---\nBody\n";
803 let result = merge_fields(content, "agent_doc_format: append").unwrap();
804 let (fm, _) = parse(&result).unwrap();
805 assert_eq!(fm.format, Some(AgentDocFormat::Append));
806 }
807
808 #[test]
809 fn merge_fields_write_enum() {
810 let content = "---\nagent_doc_session: abc\n---\nBody\n";
811 let result = merge_fields(content, "agent_doc_write: merge").unwrap();
812 let (fm, _) = parse(&result).unwrap();
813 assert_eq!(fm.write_mode, Some(AgentDocWrite::Merge));
814 }
815
816 #[test]
817 fn merge_fields_ignores_unknown() {
818 let content = "---\nagent_doc_session: abc\n---\nBody\n";
819 let result = merge_fields(content, "unknown_field: value\nmodel: opus").unwrap();
820 let (fm, _) = parse(&result).unwrap();
821 assert_eq!(fm.model.as_deref(), Some("opus"));
822 }
823
824 #[test]
825 fn merge_fields_preserves_body() {
826 let content = "---\nagent_doc_session: abc\n---\n# Title\n\nSome **markdown** content.\n";
827 let result = merge_fields(content, "model: opus").unwrap();
828 assert!(result.contains("# Title"));
829 assert!(result.contains("Some **markdown** content."));
830 }
831
832 #[test]
833 fn set_format_and_write_clears_deprecated() {
834 let content = "---\nagent_doc_mode: append\n---\nBody\n";
835 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
836 let (fm, _) = parse(&result).unwrap();
837 assert!(fm.mode.is_none());
838 assert_eq!(fm.format, Some(AgentDocFormat::Template));
839 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
840 }
841}