1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
8pub enum AgentDocFormat {
9 #[clap(alias = "inline")]
11 Append,
12 Template,
14}
15
16impl fmt::Display for AgentDocFormat {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Append => write!(f, "inline"),
20 Self::Template => write!(f, "template"),
21 }
22 }
23}
24
25impl Serialize for AgentDocFormat {
26 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
27 where
28 S: serde::Serializer,
29 {
30 match self {
31 Self::Append => serializer.serialize_str("inline"),
32 Self::Template => serializer.serialize_str("template"),
33 }
34 }
35}
36
37impl<'de> Deserialize<'de> for AgentDocFormat {
38 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
39 where
40 D: serde::Deserializer<'de>,
41 {
42 let s = String::deserialize(deserializer)?;
43 match s.as_str() {
44 "append" | "inline" => Ok(Self::Append),
45 "template" => Ok(Self::Template),
46 other => Err(serde::de::Error::unknown_variant(other, &["inline", "append", "template"])),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
53#[serde(rename_all = "lowercase")]
54pub enum AgentDocWrite {
55 Merge,
57 Crdt,
59}
60
61impl fmt::Display for AgentDocWrite {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Merge => write!(f, "merge"),
65 Self::Crdt => write!(f, "crdt"),
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct ResolvedMode {
73 pub format: AgentDocFormat,
74 pub write: AgentDocWrite,
75}
76
77impl ResolvedMode {
78 pub fn is_template(&self) -> bool {
79 self.format == AgentDocFormat::Template
80 }
81
82 pub fn is_append(&self) -> bool {
83 self.format == AgentDocFormat::Append
84 }
85
86 pub fn is_crdt(&self) -> bool {
87 self.write == AgentDocWrite::Crdt
88 }
89}
90
91#[derive(Debug, Default, Clone, Serialize, Deserialize)]
93pub struct StreamConfig {
94 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub interval: Option<u64>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub strip_ansi: Option<bool>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub target: Option<String>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub thinking: Option<bool>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub thinking_target: Option<String>,
110}
111
112#[derive(Debug, Default, Serialize, Deserialize)]
113pub struct Frontmatter {
114 #[serde(
117 default,
118 skip_serializing_if = "Option::is_none",
119 rename = "agent_doc_session",
120 alias = "session"
121 )]
122 pub session: Option<String>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub resume: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub agent: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub model: Option<String>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub branch: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub tmux_session: Option<String>,
137 #[serde(
141 default,
142 skip_serializing_if = "Option::is_none",
143 rename = "agent_doc_mode",
144 alias = "mode",
145 alias = "response_mode"
146 )]
147 pub mode: Option<String>,
148 #[serde(
150 default,
151 skip_serializing_if = "Option::is_none",
152 rename = "agent_doc_format"
153 )]
154 pub format: Option<AgentDocFormat>,
155 #[serde(
157 default,
158 skip_serializing_if = "Option::is_none",
159 rename = "agent_doc_write"
160 )]
161 pub write_mode: Option<AgentDocWrite>,
162 #[serde(
164 default,
165 skip_serializing_if = "Option::is_none",
166 rename = "agent_doc_stream"
167 )]
168 pub stream_config: Option<StreamConfig>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub claude_args: Option<String>,
173}
174
175impl Frontmatter {
176 pub fn resolve_mode(&self) -> ResolvedMode {
183 let mut format = AgentDocFormat::Template;
185 let mut write = AgentDocWrite::Crdt;
186
187 if let Some(ref mode_str) = self.mode {
189 match mode_str.as_str() {
190 "append" => {
191 format = AgentDocFormat::Append;
192 }
194 "template" => {
195 format = AgentDocFormat::Template;
196 }
198 "stream" => {
199 format = AgentDocFormat::Template;
200 write = AgentDocWrite::Crdt;
201 }
202 _ => {} }
204 }
205
206 if let Some(f) = self.format {
208 format = f;
209 }
210 if let Some(w) = self.write_mode {
211 write = w;
212 }
213
214 ResolvedMode { format, write }
215 }
216}
217
218pub fn parse(content: &str) -> Result<(Frontmatter, &str)> {
221 if !content.starts_with("---\n") {
222 return Ok((Frontmatter::default(), content));
223 }
224 let rest = &content[4..]; let end = rest
226 .find("\n---\n")
227 .or_else(|| rest.find("\n---"))
228 .ok_or_else(|| anyhow::anyhow!("Unterminated frontmatter block"))?;
229 let yaml = &rest[..end];
230 let fm: Frontmatter = serde_yaml::from_str(yaml)?;
231 let body_start = 4 + end + 4; let body = if body_start <= content.len() {
233 &content[body_start..]
234 } else {
235 ""
236 };
237 Ok((fm, body))
238}
239
240pub fn write(fm: &Frontmatter, body: &str) -> Result<String> {
242 let yaml = serde_yaml::to_string(fm)?;
243 Ok(format!("---\n{}---\n{}", yaml, body))
244}
245
246pub fn set_session_id(content: &str, session_id: &str) -> Result<String> {
248 let (mut fm, body) = parse(content)?;
249 fm.session = Some(session_id.to_string());
250 write(&fm, body)
251}
252
253pub fn set_resume_id(content: &str, resume_id: &str) -> Result<String> {
255 let (mut fm, body) = parse(content)?;
256 fm.resume = Some(resume_id.to_string());
257 write(&fm, body)
258}
259
260pub fn set_format_and_write(
262 content: &str,
263 format: AgentDocFormat,
264 write_mode: AgentDocWrite,
265) -> Result<String> {
266 let (mut fm, body) = parse(content)?;
267 fm.format = Some(format);
268 fm.write_mode = Some(write_mode);
269 fm.mode = None;
270 write(&fm, body)
271}
272
273pub fn merge_fields(content: &str, yaml_fields: &str) -> Result<String> {
279 let (mut fm, body) = parse(content)?;
280 let patch: serde_yaml::Value = serde_yaml::from_str(yaml_fields)
281 .unwrap_or(serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
282 let mapping = patch.as_mapping().unwrap_or(&serde_yaml::Mapping::new()).clone();
283
284 for (key, value) in &mapping {
285 let key_str = key.as_str().unwrap_or("");
286 let val_str = || value.as_str().map(|s| s.to_string());
287 match key_str {
288 "agent_doc_session" | "session" => fm.session = val_str(),
289 "resume" => fm.resume = val_str(),
290 "agent" => fm.agent = val_str(),
291 "model" => fm.model = val_str(),
292 "branch" => fm.branch = val_str(),
293 "tmux_session" => fm.tmux_session = val_str(),
294 "agent_doc_mode" | "mode" | "response_mode" => fm.mode = val_str(),
295 "agent_doc_format" => {
296 if let Some(s) = value.as_str()
297 && let Ok(f) = serde_yaml::from_str::<AgentDocFormat>(&format!("\"{}\"", s))
298 {
299 fm.format = Some(f);
300 }
301 }
302 "agent_doc_write" => {
303 if let Some(s) = value.as_str()
304 && let Ok(w) = serde_yaml::from_str::<AgentDocWrite>(&format!("\"{}\"", s))
305 {
306 fm.write_mode = Some(w);
307 }
308 }
309 "claude_args" => fm.claude_args = val_str(),
310 _ => {
311 eprintln!("[frontmatter] ignoring unknown patch field: {}", key_str);
312 }
313 }
314 }
315
316 write(&fm, body)
317}
318
319pub fn set_tmux_session(content: &str, session_name: &str) -> Result<String> {
321 let (mut fm, body) = parse(content)?;
322 fm.tmux_session = Some(session_name.to_string());
323 write(&fm, body)
324}
325
326pub fn ensure_session(content: &str) -> Result<(String, String)> {
331 let (fm, _body) = parse(content)?;
332 if let Some(ref session_id) = fm.session {
333 return Ok((content.to_string(), session_id.clone()));
335 }
336 let session_id = Uuid::new_v4().to_string();
337 let updated = set_session_id(content, &session_id)?;
338 Ok((updated, session_id))
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn parse_no_frontmatter() {
347 let content = "# Hello\n\nBody text.\n";
348 let (fm, body) = parse(content).unwrap();
349 assert!(fm.session.is_none());
350 assert!(fm.agent.is_none());
351 assert!(fm.model.is_none());
352 assert!(fm.branch.is_none());
353 assert_eq!(body, content);
354 }
355
356 #[test]
357 fn parse_all_fields() {
358 let content = "---\nsession: abc-123\nagent: claude\nmodel: opus\nbranch: main\n---\nBody\n";
359 let (fm, body) = parse(content).unwrap();
360 assert_eq!(fm.session.as_deref(), Some("abc-123"));
361 assert_eq!(fm.agent.as_deref(), Some("claude"));
362 assert_eq!(fm.model.as_deref(), Some("opus"));
363 assert_eq!(fm.branch.as_deref(), Some("main"));
364 assert!(body.contains("Body"));
365 }
366
367 #[test]
368 fn parse_partial_fields() {
369 let content = "---\nsession: xyz\n---\n# Doc\n";
370 let (fm, body) = parse(content).unwrap();
371 assert_eq!(fm.session.as_deref(), Some("xyz"));
372 assert!(fm.agent.is_none());
373 assert!(body.contains("# Doc"));
374 }
375
376 #[test]
377 fn parse_null_fields() {
378 let content = "---\nsession: null\nagent: null\nmodel: null\nbranch: null\n---\nBody\n";
379 let (fm, body) = parse(content).unwrap();
380 assert!(fm.session.is_none());
381 assert!(fm.agent.is_none());
382 assert!(fm.model.is_none());
383 assert!(fm.branch.is_none());
384 assert!(body.contains("Body"));
385 }
386
387 #[test]
388 fn parse_unterminated_frontmatter() {
389 let content = "---\nsession: abc\nno closing block";
390 let err = parse(content).unwrap_err();
391 assert!(err.to_string().contains("Unterminated frontmatter"));
392 }
393
394 #[test]
395 fn parse_closing_at_eof() {
396 let content = "---\nsession: abc\n---";
397 let (fm, body) = parse(content).unwrap();
398 assert_eq!(fm.session.as_deref(), Some("abc"));
399 assert_eq!(body, "");
400 }
401
402 #[test]
403 fn parse_empty_body() {
404 let content = "---\nsession: abc\n---\n";
405 let (fm, _body) = parse(content).unwrap();
406 assert_eq!(fm.session.as_deref(), Some("abc"));
407 }
408
409 #[test]
410 fn write_roundtrip() {
411 let fm = Frontmatter {
413 session: Some("test-id".to_string()),
414 resume: Some("resume-id".to_string()),
415 agent: Some("claude".to_string()),
416 model: Some("opus".to_string()),
417 branch: Some("dev".to_string()),
418 tmux_session: None,
419 mode: None,
420 format: None,
421 write_mode: None,
422 stream_config: None,
423 claude_args: None,
424 };
425 let body = "# Hello\n\nBody text.\n";
426 let written = write(&fm, body).unwrap();
427 let (fm2, body2) = parse(&written).unwrap();
428 assert_eq!(fm2.session, fm.session);
429 assert_eq!(fm2.agent, fm.agent);
430 assert_eq!(fm2.model, fm.model);
431 assert_eq!(fm2.branch, fm.branch);
432 assert!(body2.contains("# Hello"));
434 assert!(body2.contains("Body text."));
435 }
436
437 #[test]
438 fn write_default_frontmatter() {
439 let fm = Frontmatter::default();
440 let result = write(&fm, "body\n").unwrap();
441 assert!(result.starts_with("---\n"));
442 assert!(result.ends_with("---\nbody\n"));
443 }
444
445 #[test]
446 fn write_preserves_body_content() {
447 let fm = Frontmatter::default();
448 let body = "# Title\n\nSome **markdown** with `code`.\n";
449 let result = write(&fm, body).unwrap();
450 assert!(result.contains("# Title"));
451 assert!(result.contains("Some **markdown** with `code`."));
452 }
453
454 #[test]
455 fn set_session_id_creates_frontmatter() {
456 let content = "# No frontmatter\n\nJust body.\n";
457 let result = set_session_id(content, "new-session").unwrap();
458 let (fm, body) = parse(&result).unwrap();
459 assert_eq!(fm.session.as_deref(), Some("new-session"));
460 assert!(body.contains("# No frontmatter"));
461 }
462
463 #[test]
464 fn set_session_id_updates_existing() {
465 let content = "---\nsession: old-id\nagent: claude\n---\nBody\n";
466 let result = set_session_id(content, "new-id").unwrap();
467 let (fm, body) = parse(&result).unwrap();
468 assert_eq!(fm.session.as_deref(), Some("new-id"));
469 assert_eq!(fm.agent.as_deref(), Some("claude"));
470 assert!(body.contains("Body"));
471 }
472
473 #[test]
474 fn set_session_id_preserves_other_fields() {
475 let content = "---\nsession: old\nagent: claude\nmodel: opus\nbranch: dev\n---\nBody\n";
476 let result = set_session_id(content, "new").unwrap();
477 let (fm, _) = parse(&result).unwrap();
478 assert_eq!(fm.session.as_deref(), Some("new"));
479 assert_eq!(fm.agent.as_deref(), Some("claude"));
480 assert_eq!(fm.model.as_deref(), Some("opus"));
481 assert_eq!(fm.branch.as_deref(), Some("dev"));
482 }
483
484 #[test]
485 fn ensure_session_no_frontmatter() {
486 let content = "# Hello\n\nBody.\n";
487 let (updated, sid) = ensure_session(content).unwrap();
488 assert_eq!(sid.len(), 36); let (fm, body) = parse(&updated).unwrap();
491 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
492 assert!(body.contains("# Hello"));
493 }
494
495 #[test]
496 fn ensure_session_null_session() {
497 let content = "---\nsession:\nagent: claude\n---\nBody\n";
498 let (updated, sid) = ensure_session(content).unwrap();
499 assert_eq!(sid.len(), 36);
500 let (fm, body) = parse(&updated).unwrap();
501 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
502 assert_eq!(fm.agent.as_deref(), Some("claude"));
503 assert!(body.contains("Body"));
504 }
505
506 #[test]
507 fn ensure_session_existing_session() {
508 let content = "---\nagent_doc_session: existing-id\nagent: claude\n---\nBody\n";
509 let (updated, sid) = ensure_session(content).unwrap();
510 assert_eq!(sid, "existing-id");
511 assert_eq!(updated, content);
513 }
514
515 #[test]
516 fn parse_legacy_session_field() {
517 let content = "---\nsession: legacy-id\nagent: claude\n---\nBody\n";
519 let (fm, body) = parse(content).unwrap();
520 assert_eq!(fm.session.as_deref(), Some("legacy-id"));
521 assert_eq!(fm.agent.as_deref(), Some("claude"));
522 assert!(body.contains("Body"));
523 }
524
525 #[test]
526 fn parse_agent_doc_mode_canonical() {
527 let content = "---\nagent_doc_mode: template\n---\nBody\n";
528 let (fm, _) = parse(content).unwrap();
529 assert_eq!(fm.mode.as_deref(), Some("template"));
530 }
531
532 #[test]
533 fn parse_mode_shorthand_alias() {
534 let content = "---\nmode: template\n---\nBody\n";
535 let (fm, _) = parse(content).unwrap();
536 assert_eq!(fm.mode.as_deref(), Some("template"));
537 }
538
539 #[test]
540 fn parse_response_mode_legacy_alias() {
541 let content = "---\nresponse_mode: template\n---\nBody\n";
542 let (fm, _) = parse(content).unwrap();
543 assert_eq!(fm.mode.as_deref(), Some("template"));
544 }
545
546 #[test]
547 fn write_uses_agent_doc_mode_field() {
548 #[allow(deprecated)]
549 let fm = Frontmatter {
550 mode: Some("template".to_string()),
551 ..Default::default()
552 };
553 let result = write(&fm, "body\n").unwrap();
554 assert!(result.contains("agent_doc_mode:"));
555 assert!(!result.contains("response_mode:"));
556 assert!(!result.contains("\nmode:"));
557 }
558
559 #[test]
560 fn write_uses_new_field_name() {
561 let fm = Frontmatter {
562 session: Some("test-id".to_string()),
563 ..Default::default()
564 };
565 let result = write(&fm, "body\n").unwrap();
566 assert!(result.contains("agent_doc_session:"));
567 assert!(!result.contains("\nsession:"));
568 }
569
570 #[test]
573 fn resolve_mode_defaults() {
574 let fm = Frontmatter::default();
575 let resolved = fm.resolve_mode();
576 assert_eq!(resolved.format, AgentDocFormat::Template);
577 assert_eq!(resolved.write, AgentDocWrite::Crdt);
578 }
579
580 #[test]
581 fn resolve_mode_from_deprecated_append() {
582 let content = "---\nagent_doc_mode: append\n---\nBody\n";
583 let (fm, _) = parse(content).unwrap();
584 let resolved = fm.resolve_mode();
585 assert_eq!(resolved.format, AgentDocFormat::Append);
586 assert_eq!(resolved.write, AgentDocWrite::Crdt);
587 }
588
589 #[test]
590 fn resolve_mode_from_deprecated_template() {
591 let content = "---\nagent_doc_mode: template\n---\nBody\n";
592 let (fm, _) = parse(content).unwrap();
593 let resolved = fm.resolve_mode();
594 assert_eq!(resolved.format, AgentDocFormat::Template);
595 assert_eq!(resolved.write, AgentDocWrite::Crdt);
596 }
597
598 #[test]
599 fn resolve_mode_from_deprecated_stream() {
600 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
601 let (fm, _) = parse(content).unwrap();
602 let resolved = fm.resolve_mode();
603 assert_eq!(resolved.format, AgentDocFormat::Template);
604 assert_eq!(resolved.write, AgentDocWrite::Crdt);
605 }
606
607 #[test]
608 fn resolve_mode_new_fields_override_deprecated() {
609 let content = "---\nagent_doc_mode: append\nagent_doc_format: template\nagent_doc_write: merge\n---\nBody\n";
610 let (fm, _) = parse(content).unwrap();
611 let resolved = fm.resolve_mode();
612 assert_eq!(resolved.format, AgentDocFormat::Template);
613 assert_eq!(resolved.write, AgentDocWrite::Merge);
614 }
615
616 #[test]
617 fn resolve_mode_explicit_new_fields_only() {
618 let content = "---\nagent_doc_format: append\nagent_doc_write: crdt\n---\nBody\n";
619 let (fm, _) = parse(content).unwrap();
620 let resolved = fm.resolve_mode();
621 assert_eq!(resolved.format, AgentDocFormat::Append);
622 assert_eq!(resolved.write, AgentDocWrite::Crdt);
623 }
624
625 #[test]
626 fn resolve_mode_partial_new_field_format_only() {
627 let content = "---\nagent_doc_format: append\n---\nBody\n";
628 let (fm, _) = parse(content).unwrap();
629 let resolved = fm.resolve_mode();
630 assert_eq!(resolved.format, AgentDocFormat::Append);
631 assert_eq!(resolved.write, AgentDocWrite::Crdt); }
633
634 #[test]
635 fn resolve_mode_partial_new_field_write_only() {
636 let content = "---\nagent_doc_write: merge\n---\nBody\n";
637 let (fm, _) = parse(content).unwrap();
638 let resolved = fm.resolve_mode();
639 assert_eq!(resolved.format, AgentDocFormat::Template); assert_eq!(resolved.write, AgentDocWrite::Merge);
641 }
642
643 #[test]
644 fn resolve_mode_helper_methods() {
645 let fm = Frontmatter::default();
646 let resolved = fm.resolve_mode();
647 assert!(resolved.is_template());
648 assert!(!resolved.is_append());
649 assert!(resolved.is_crdt());
650 }
651
652 #[test]
653 fn parse_new_format_field() {
654 let content = "---\nagent_doc_format: template\n---\nBody\n";
655 let (fm, _) = parse(content).unwrap();
656 assert_eq!(fm.format, Some(AgentDocFormat::Template));
657 }
658
659 #[test]
660 fn parse_new_write_field() {
661 let content = "---\nagent_doc_write: crdt\n---\nBody\n";
662 let (fm, _) = parse(content).unwrap();
663 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
664 }
665
666 #[test]
667 fn write_uses_new_format_write_fields() {
668 let fm = Frontmatter {
669 format: Some(AgentDocFormat::Template),
670 write_mode: Some(AgentDocWrite::Crdt),
671 ..Default::default()
672 };
673 let result = write(&fm, "body\n").unwrap();
674 assert!(result.contains("agent_doc_format:"));
675 assert!(result.contains("agent_doc_write:"));
676 assert!(!result.contains("agent_doc_mode:"));
677 }
678
679 #[test]
680 fn set_format_and_write_clears_deprecated_mode() {
681 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
682 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
683 let (fm, _) = parse(&result).unwrap();
684 assert!(fm.mode.is_none());
685 assert_eq!(fm.format, Some(AgentDocFormat::Template));
686 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
687 }
688
689 #[test]
692 fn merge_fields_adds_new_field() {
693 let content = "---\nagent_doc_session: abc\n---\nBody\n";
694 let result = merge_fields(content, "model: opus").unwrap();
695 let (fm, body) = parse(&result).unwrap();
696 assert_eq!(fm.session.as_deref(), Some("abc"));
697 assert_eq!(fm.model.as_deref(), Some("opus"));
698 assert!(body.contains("Body"));
699 }
700
701 #[test]
702 fn merge_fields_updates_existing_field() {
703 let content = "---\nagent_doc_session: abc\nmodel: sonnet\n---\nBody\n";
704 let result = merge_fields(content, "model: opus").unwrap();
705 let (fm, _) = parse(&result).unwrap();
706 assert_eq!(fm.model.as_deref(), Some("opus"));
707 assert_eq!(fm.session.as_deref(), Some("abc"));
708 }
709
710 #[test]
711 fn merge_fields_multiple_fields() {
712 let content = "---\nagent_doc_session: abc\n---\nBody\n";
713 let result = merge_fields(content, "model: opus\nagent: claude\nbranch: main").unwrap();
714 let (fm, _) = parse(&result).unwrap();
715 assert_eq!(fm.model.as_deref(), Some("opus"));
716 assert_eq!(fm.agent.as_deref(), Some("claude"));
717 assert_eq!(fm.branch.as_deref(), Some("main"));
718 }
719
720 #[test]
721 fn merge_fields_format_enum() {
722 let content = "---\nagent_doc_session: abc\n---\nBody\n";
723 let result = merge_fields(content, "agent_doc_format: append").unwrap();
724 let (fm, _) = parse(&result).unwrap();
725 assert_eq!(fm.format, Some(AgentDocFormat::Append));
726 }
727
728 #[test]
729 fn merge_fields_write_enum() {
730 let content = "---\nagent_doc_session: abc\n---\nBody\n";
731 let result = merge_fields(content, "agent_doc_write: merge").unwrap();
732 let (fm, _) = parse(&result).unwrap();
733 assert_eq!(fm.write_mode, Some(AgentDocWrite::Merge));
734 }
735
736 #[test]
737 fn merge_fields_ignores_unknown() {
738 let content = "---\nagent_doc_session: abc\n---\nBody\n";
739 let result = merge_fields(content, "unknown_field: value\nmodel: opus").unwrap();
740 let (fm, _) = parse(&result).unwrap();
741 assert_eq!(fm.model.as_deref(), Some("opus"));
742 }
743
744 #[test]
745 fn merge_fields_preserves_body() {
746 let content = "---\nagent_doc_session: abc\n---\n# Title\n\nSome **markdown** content.\n";
747 let result = merge_fields(content, "model: opus").unwrap();
748 assert!(result.contains("# Title"));
749 assert!(result.contains("Some **markdown** content."));
750 }
751
752 #[test]
753 fn set_format_and_write_clears_deprecated() {
754 let content = "---\nagent_doc_mode: append\n---\nBody\n";
755 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
756 let (fm, _) = parse(&result).unwrap();
757 assert!(fm.mode.is_none());
758 assert_eq!(fm.format, Some(AgentDocFormat::Template));
759 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
760 }
761}