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")]
139 pub tmux_session: Option<String>,
140 #[serde(
144 default,
145 skip_serializing_if = "Option::is_none",
146 rename = "agent_doc_mode",
147 alias = "mode",
148 alias = "response_mode"
149 )]
150 pub mode: Option<String>,
151 #[serde(
153 default,
154 skip_serializing_if = "Option::is_none",
155 rename = "agent_doc_format"
156 )]
157 pub format: Option<AgentDocFormat>,
158 #[serde(
160 default,
161 skip_serializing_if = "Option::is_none",
162 rename = "agent_doc_write"
163 )]
164 pub write_mode: Option<AgentDocWrite>,
165 #[serde(
167 default,
168 skip_serializing_if = "Option::is_none",
169 rename = "agent_doc_stream"
170 )]
171 pub stream_config: Option<StreamConfig>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub claude_args: Option<String>,
176}
177
178impl Frontmatter {
179 pub fn resolve_mode(&self) -> ResolvedMode {
186 let mut format = AgentDocFormat::Template;
188 let mut write = AgentDocWrite::Crdt;
189
190 if let Some(ref mode_str) = self.mode {
192 match mode_str.as_str() {
193 "append" => {
194 format = AgentDocFormat::Append;
195 }
197 "template" => {
198 format = AgentDocFormat::Template;
199 }
201 "stream" => {
202 format = AgentDocFormat::Template;
203 write = AgentDocWrite::Crdt;
204 }
205 _ => {} }
207 }
208
209 if let Some(f) = self.format {
211 format = f;
212 }
213 if let Some(w) = self.write_mode {
214 write = w;
215 }
216
217 ResolvedMode { format, write }
218 }
219}
220
221pub fn parse(content: &str) -> Result<(Frontmatter, &str)> {
224 if !content.starts_with("---\n") {
225 return Ok((Frontmatter::default(), content));
226 }
227 let rest = &content[4..]; let end = rest
229 .find("\n---\n")
230 .or_else(|| rest.find("\n---"))
231 .ok_or_else(|| anyhow::anyhow!("Unterminated frontmatter block"))?;
232 let yaml = &rest[..end];
233 let fm: Frontmatter = serde_yaml::from_str(yaml)?;
234 let body_start = 4 + end + 4; let body = if body_start <= content.len() {
236 &content[body_start..]
237 } else {
238 ""
239 };
240 Ok((fm, body))
241}
242
243pub fn write(fm: &Frontmatter, body: &str) -> Result<String> {
245 let yaml = serde_yaml::to_string(fm)?;
246 Ok(format!("---\n{}---\n{}", yaml, body))
247}
248
249pub fn set_session_id(content: &str, session_id: &str) -> Result<String> {
251 let (mut fm, body) = parse(content)?;
252 fm.session = Some(session_id.to_string());
253 write(&fm, body)
254}
255
256pub fn set_resume_id(content: &str, resume_id: &str) -> Result<String> {
258 let (mut fm, body) = parse(content)?;
259 fm.resume = Some(resume_id.to_string());
260 write(&fm, body)
261}
262
263pub fn set_format_and_write(
265 content: &str,
266 format: AgentDocFormat,
267 write_mode: AgentDocWrite,
268) -> Result<String> {
269 let (mut fm, body) = parse(content)?;
270 fm.format = Some(format);
271 fm.write_mode = Some(write_mode);
272 fm.mode = None;
273 write(&fm, body)
274}
275
276pub fn merge_fields(content: &str, yaml_fields: &str) -> Result<String> {
282 let (mut fm, body) = parse(content)?;
283 let patch: serde_yaml::Value = serde_yaml::from_str(yaml_fields)
284 .unwrap_or(serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
285 let mapping = patch.as_mapping().unwrap_or(&serde_yaml::Mapping::new()).clone();
286
287 for (key, value) in &mapping {
288 let key_str = key.as_str().unwrap_or("");
289 let val_str = || value.as_str().map(|s| s.to_string());
290 match key_str {
291 "agent_doc_session" | "session" => fm.session = val_str(),
292 "resume" => fm.resume = val_str(),
293 "agent" => fm.agent = val_str(),
294 "model" => fm.model = val_str(),
295 "branch" => fm.branch = val_str(),
296 "tmux_session" => fm.tmux_session = val_str(),
297 "agent_doc_mode" | "mode" | "response_mode" => fm.mode = val_str(),
298 "agent_doc_format" => {
299 if let Some(s) = value.as_str()
300 && let Ok(f) = serde_yaml::from_str::<AgentDocFormat>(&format!("\"{}\"", s))
301 {
302 fm.format = Some(f);
303 }
304 }
305 "agent_doc_write" => {
306 if let Some(s) = value.as_str()
307 && let Ok(w) = serde_yaml::from_str::<AgentDocWrite>(&format!("\"{}\"", s))
308 {
309 fm.write_mode = Some(w);
310 }
311 }
312 "claude_args" => fm.claude_args = val_str(),
313 _ => {
314 eprintln!("[frontmatter] ignoring unknown patch field: {}", key_str);
315 }
316 }
317 }
318
319 write(&fm, body)
320}
321
322pub fn set_tmux_session(content: &str, session_name: &str) -> Result<String> {
328 let (mut fm, body) = parse(content)?;
329 fm.tmux_session = Some(session_name.to_string());
330 write(&fm, body)
331}
332
333pub fn ensure_session(content: &str) -> Result<(String, String)> {
338 let (fm, _body) = parse(content)?;
339 if let Some(ref session_id) = fm.session {
340 return Ok((content.to_string(), session_id.clone()));
342 }
343 let session_id = Uuid::new_v4().to_string();
344 let updated = set_session_id(content, &session_id)?;
345 Ok((updated, session_id))
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn parse_no_frontmatter() {
354 let content = "# Hello\n\nBody text.\n";
355 let (fm, body) = parse(content).unwrap();
356 assert!(fm.session.is_none());
357 assert!(fm.agent.is_none());
358 assert!(fm.model.is_none());
359 assert!(fm.branch.is_none());
360 assert_eq!(body, content);
361 }
362
363 #[test]
364 fn parse_all_fields() {
365 let content = "---\nsession: abc-123\nagent: claude\nmodel: opus\nbranch: main\n---\nBody\n";
366 let (fm, body) = parse(content).unwrap();
367 assert_eq!(fm.session.as_deref(), Some("abc-123"));
368 assert_eq!(fm.agent.as_deref(), Some("claude"));
369 assert_eq!(fm.model.as_deref(), Some("opus"));
370 assert_eq!(fm.branch.as_deref(), Some("main"));
371 assert!(body.contains("Body"));
372 }
373
374 #[test]
375 fn parse_partial_fields() {
376 let content = "---\nsession: xyz\n---\n# Doc\n";
377 let (fm, body) = parse(content).unwrap();
378 assert_eq!(fm.session.as_deref(), Some("xyz"));
379 assert!(fm.agent.is_none());
380 assert!(body.contains("# Doc"));
381 }
382
383 #[test]
384 fn parse_null_fields() {
385 let content = "---\nsession: null\nagent: null\nmodel: null\nbranch: null\n---\nBody\n";
386 let (fm, body) = parse(content).unwrap();
387 assert!(fm.session.is_none());
388 assert!(fm.agent.is_none());
389 assert!(fm.model.is_none());
390 assert!(fm.branch.is_none());
391 assert!(body.contains("Body"));
392 }
393
394 #[test]
395 fn parse_unterminated_frontmatter() {
396 let content = "---\nsession: abc\nno closing block";
397 let err = parse(content).unwrap_err();
398 assert!(err.to_string().contains("Unterminated frontmatter"));
399 }
400
401 #[test]
402 fn parse_closing_at_eof() {
403 let content = "---\nsession: abc\n---";
404 let (fm, body) = parse(content).unwrap();
405 assert_eq!(fm.session.as_deref(), Some("abc"));
406 assert_eq!(body, "");
407 }
408
409 #[test]
410 fn parse_empty_body() {
411 let content = "---\nsession: abc\n---\n";
412 let (fm, _body) = parse(content).unwrap();
413 assert_eq!(fm.session.as_deref(), Some("abc"));
414 }
415
416 #[test]
417 fn write_roundtrip() {
418 let fm = Frontmatter {
420 session: Some("test-id".to_string()),
421 resume: Some("resume-id".to_string()),
422 agent: Some("claude".to_string()),
423 model: Some("opus".to_string()),
424 branch: Some("dev".to_string()),
425 tmux_session: None,
426 mode: None,
427 format: None,
428 write_mode: None,
429 stream_config: None,
430 claude_args: None,
431 };
432 let body = "# Hello\n\nBody text.\n";
433 let written = write(&fm, body).unwrap();
434 let (fm2, body2) = parse(&written).unwrap();
435 assert_eq!(fm2.session, fm.session);
436 assert_eq!(fm2.agent, fm.agent);
437 assert_eq!(fm2.model, fm.model);
438 assert_eq!(fm2.branch, fm.branch);
439 assert!(body2.contains("# Hello"));
441 assert!(body2.contains("Body text."));
442 }
443
444 #[test]
445 fn write_default_frontmatter() {
446 let fm = Frontmatter::default();
447 let result = write(&fm, "body\n").unwrap();
448 assert!(result.starts_with("---\n"));
449 assert!(result.ends_with("---\nbody\n"));
450 }
451
452 #[test]
453 fn write_preserves_body_content() {
454 let fm = Frontmatter::default();
455 let body = "# Title\n\nSome **markdown** with `code`.\n";
456 let result = write(&fm, body).unwrap();
457 assert!(result.contains("# Title"));
458 assert!(result.contains("Some **markdown** with `code`."));
459 }
460
461 #[test]
462 fn set_session_id_creates_frontmatter() {
463 let content = "# No frontmatter\n\nJust body.\n";
464 let result = set_session_id(content, "new-session").unwrap();
465 let (fm, body) = parse(&result).unwrap();
466 assert_eq!(fm.session.as_deref(), Some("new-session"));
467 assert!(body.contains("# No frontmatter"));
468 }
469
470 #[test]
471 fn set_session_id_updates_existing() {
472 let content = "---\nsession: old-id\nagent: claude\n---\nBody\n";
473 let result = set_session_id(content, "new-id").unwrap();
474 let (fm, body) = parse(&result).unwrap();
475 assert_eq!(fm.session.as_deref(), Some("new-id"));
476 assert_eq!(fm.agent.as_deref(), Some("claude"));
477 assert!(body.contains("Body"));
478 }
479
480 #[test]
481 fn set_session_id_preserves_other_fields() {
482 let content = "---\nsession: old\nagent: claude\nmodel: opus\nbranch: dev\n---\nBody\n";
483 let result = set_session_id(content, "new").unwrap();
484 let (fm, _) = parse(&result).unwrap();
485 assert_eq!(fm.session.as_deref(), Some("new"));
486 assert_eq!(fm.agent.as_deref(), Some("claude"));
487 assert_eq!(fm.model.as_deref(), Some("opus"));
488 assert_eq!(fm.branch.as_deref(), Some("dev"));
489 }
490
491 #[test]
492 fn ensure_session_no_frontmatter() {
493 let content = "# Hello\n\nBody.\n";
494 let (updated, sid) = ensure_session(content).unwrap();
495 assert_eq!(sid.len(), 36); let (fm, body) = parse(&updated).unwrap();
498 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
499 assert!(body.contains("# Hello"));
500 }
501
502 #[test]
503 fn ensure_session_null_session() {
504 let content = "---\nsession:\nagent: claude\n---\nBody\n";
505 let (updated, sid) = ensure_session(content).unwrap();
506 assert_eq!(sid.len(), 36);
507 let (fm, body) = parse(&updated).unwrap();
508 assert_eq!(fm.session.as_deref(), Some(sid.as_str()));
509 assert_eq!(fm.agent.as_deref(), Some("claude"));
510 assert!(body.contains("Body"));
511 }
512
513 #[test]
514 fn ensure_session_existing_session() {
515 let content = "---\nagent_doc_session: existing-id\nagent: claude\n---\nBody\n";
516 let (updated, sid) = ensure_session(content).unwrap();
517 assert_eq!(sid, "existing-id");
518 assert_eq!(updated, content);
520 }
521
522 #[test]
523 fn parse_legacy_session_field() {
524 let content = "---\nsession: legacy-id\nagent: claude\n---\nBody\n";
526 let (fm, body) = parse(content).unwrap();
527 assert_eq!(fm.session.as_deref(), Some("legacy-id"));
528 assert_eq!(fm.agent.as_deref(), Some("claude"));
529 assert!(body.contains("Body"));
530 }
531
532 #[test]
533 fn parse_agent_doc_mode_canonical() {
534 let content = "---\nagent_doc_mode: 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_mode_shorthand_alias() {
541 let content = "---\nmode: 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 parse_response_mode_legacy_alias() {
548 let content = "---\nresponse_mode: template\n---\nBody\n";
549 let (fm, _) = parse(content).unwrap();
550 assert_eq!(fm.mode.as_deref(), Some("template"));
551 }
552
553 #[test]
554 fn write_uses_agent_doc_mode_field() {
555 #[allow(deprecated)]
556 let fm = Frontmatter {
557 mode: Some("template".to_string()),
558 ..Default::default()
559 };
560 let result = write(&fm, "body\n").unwrap();
561 assert!(result.contains("agent_doc_mode:"));
562 assert!(!result.contains("response_mode:"));
563 assert!(!result.contains("\nmode:"));
564 }
565
566 #[test]
567 fn write_uses_new_field_name() {
568 let fm = Frontmatter {
569 session: Some("test-id".to_string()),
570 ..Default::default()
571 };
572 let result = write(&fm, "body\n").unwrap();
573 assert!(result.contains("agent_doc_session:"));
574 assert!(!result.contains("\nsession:"));
575 }
576
577 #[test]
580 fn resolve_mode_defaults() {
581 let fm = Frontmatter::default();
582 let resolved = fm.resolve_mode();
583 assert_eq!(resolved.format, AgentDocFormat::Template);
584 assert_eq!(resolved.write, AgentDocWrite::Crdt);
585 }
586
587 #[test]
588 fn resolve_mode_from_deprecated_append() {
589 let content = "---\nagent_doc_mode: append\n---\nBody\n";
590 let (fm, _) = parse(content).unwrap();
591 let resolved = fm.resolve_mode();
592 assert_eq!(resolved.format, AgentDocFormat::Append);
593 assert_eq!(resolved.write, AgentDocWrite::Crdt);
594 }
595
596 #[test]
597 fn resolve_mode_from_deprecated_template() {
598 let content = "---\nagent_doc_mode: template\n---\nBody\n";
599 let (fm, _) = parse(content).unwrap();
600 let resolved = fm.resolve_mode();
601 assert_eq!(resolved.format, AgentDocFormat::Template);
602 assert_eq!(resolved.write, AgentDocWrite::Crdt);
603 }
604
605 #[test]
606 fn resolve_mode_from_deprecated_stream() {
607 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
608 let (fm, _) = parse(content).unwrap();
609 let resolved = fm.resolve_mode();
610 assert_eq!(resolved.format, AgentDocFormat::Template);
611 assert_eq!(resolved.write, AgentDocWrite::Crdt);
612 }
613
614 #[test]
615 fn resolve_mode_new_fields_override_deprecated() {
616 let content = "---\nagent_doc_mode: append\nagent_doc_format: template\nagent_doc_write: merge\n---\nBody\n";
617 let (fm, _) = parse(content).unwrap();
618 let resolved = fm.resolve_mode();
619 assert_eq!(resolved.format, AgentDocFormat::Template);
620 assert_eq!(resolved.write, AgentDocWrite::Merge);
621 }
622
623 #[test]
624 fn resolve_mode_explicit_new_fields_only() {
625 let content = "---\nagent_doc_format: append\nagent_doc_write: crdt\n---\nBody\n";
626 let (fm, _) = parse(content).unwrap();
627 let resolved = fm.resolve_mode();
628 assert_eq!(resolved.format, AgentDocFormat::Append);
629 assert_eq!(resolved.write, AgentDocWrite::Crdt);
630 }
631
632 #[test]
633 fn resolve_mode_partial_new_field_format_only() {
634 let content = "---\nagent_doc_format: append\n---\nBody\n";
635 let (fm, _) = parse(content).unwrap();
636 let resolved = fm.resolve_mode();
637 assert_eq!(resolved.format, AgentDocFormat::Append);
638 assert_eq!(resolved.write, AgentDocWrite::Crdt); }
640
641 #[test]
642 fn resolve_mode_partial_new_field_write_only() {
643 let content = "---\nagent_doc_write: merge\n---\nBody\n";
644 let (fm, _) = parse(content).unwrap();
645 let resolved = fm.resolve_mode();
646 assert_eq!(resolved.format, AgentDocFormat::Template); assert_eq!(resolved.write, AgentDocWrite::Merge);
648 }
649
650 #[test]
651 fn resolve_mode_helper_methods() {
652 let fm = Frontmatter::default();
653 let resolved = fm.resolve_mode();
654 assert!(resolved.is_template());
655 assert!(!resolved.is_append());
656 assert!(resolved.is_crdt());
657 }
658
659 #[test]
660 fn parse_new_format_field() {
661 let content = "---\nagent_doc_format: template\n---\nBody\n";
662 let (fm, _) = parse(content).unwrap();
663 assert_eq!(fm.format, Some(AgentDocFormat::Template));
664 }
665
666 #[test]
667 fn parse_new_write_field() {
668 let content = "---\nagent_doc_write: crdt\n---\nBody\n";
669 let (fm, _) = parse(content).unwrap();
670 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
671 }
672
673 #[test]
674 fn write_uses_new_format_write_fields() {
675 let fm = Frontmatter {
676 format: Some(AgentDocFormat::Template),
677 write_mode: Some(AgentDocWrite::Crdt),
678 ..Default::default()
679 };
680 let result = write(&fm, "body\n").unwrap();
681 assert!(result.contains("agent_doc_format:"));
682 assert!(result.contains("agent_doc_write:"));
683 assert!(!result.contains("agent_doc_mode:"));
684 }
685
686 #[test]
687 fn set_format_and_write_clears_deprecated_mode() {
688 let content = "---\nagent_doc_mode: stream\n---\nBody\n";
689 let result = set_format_and_write(content, AgentDocFormat::Template, AgentDocWrite::Crdt).unwrap();
690 let (fm, _) = parse(&result).unwrap();
691 assert!(fm.mode.is_none());
692 assert_eq!(fm.format, Some(AgentDocFormat::Template));
693 assert_eq!(fm.write_mode, Some(AgentDocWrite::Crdt));
694 }
695
696 #[test]
699 fn merge_fields_adds_new_field() {
700 let content = "---\nagent_doc_session: abc\n---\nBody\n";
701 let result = merge_fields(content, "model: opus").unwrap();
702 let (fm, body) = parse(&result).unwrap();
703 assert_eq!(fm.session.as_deref(), Some("abc"));
704 assert_eq!(fm.model.as_deref(), Some("opus"));
705 assert!(body.contains("Body"));
706 }
707
708 #[test]
709 fn merge_fields_updates_existing_field() {
710 let content = "---\nagent_doc_session: abc\nmodel: sonnet\n---\nBody\n";
711 let result = merge_fields(content, "model: opus").unwrap();
712 let (fm, _) = parse(&result).unwrap();
713 assert_eq!(fm.model.as_deref(), Some("opus"));
714 assert_eq!(fm.session.as_deref(), Some("abc"));
715 }
716
717 #[test]
718 fn merge_fields_multiple_fields() {
719 let content = "---\nagent_doc_session: abc\n---\nBody\n";
720 let result = merge_fields(content, "model: opus\nagent: claude\nbranch: main").unwrap();
721 let (fm, _) = parse(&result).unwrap();
722 assert_eq!(fm.model.as_deref(), Some("opus"));
723 assert_eq!(fm.agent.as_deref(), Some("claude"));
724 assert_eq!(fm.branch.as_deref(), Some("main"));
725 }
726
727 #[test]
728 fn merge_fields_format_enum() {
729 let content = "---\nagent_doc_session: abc\n---\nBody\n";
730 let result = merge_fields(content, "agent_doc_format: append").unwrap();
731 let (fm, _) = parse(&result).unwrap();
732 assert_eq!(fm.format, Some(AgentDocFormat::Append));
733 }
734
735 #[test]
736 fn merge_fields_write_enum() {
737 let content = "---\nagent_doc_session: abc\n---\nBody\n";
738 let result = merge_fields(content, "agent_doc_write: merge").unwrap();
739 let (fm, _) = parse(&result).unwrap();
740 assert_eq!(fm.write_mode, Some(AgentDocWrite::Merge));
741 }
742
743 #[test]
744 fn merge_fields_ignores_unknown() {
745 let content = "---\nagent_doc_session: abc\n---\nBody\n";
746 let result = merge_fields(content, "unknown_field: value\nmodel: opus").unwrap();
747 let (fm, _) = parse(&result).unwrap();
748 assert_eq!(fm.model.as_deref(), Some("opus"));
749 }
750
751 #[test]
752 fn merge_fields_preserves_body() {
753 let content = "---\nagent_doc_session: abc\n---\n# Title\n\nSome **markdown** content.\n";
754 let result = merge_fields(content, "model: opus").unwrap();
755 assert!(result.contains("# Title"));
756 assert!(result.contains("Some **markdown** content."));
757 }
758
759 #[test]
760 fn set_format_and_write_clears_deprecated() {
761 let content = "---\nagent_doc_mode: append\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}