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