1use std::collections::BTreeSet;
2use std::path::Path;
3
4use chrono::Utc;
5
6use crate::errors::{CoreError, CoreResult};
7
8use markers::update_file_with_markers;
9
10mod markers;
11
12use ito_config::ConfigContext;
13use ito_config::ito_dir::get_ito_dir_name;
14use ito_templates::project_templates::WorktreeTemplateContext;
15
16pub const TOOL_CLAUDE: &str = "claude";
18pub const TOOL_CODEX: &str = "codex";
20pub const TOOL_GITHUB_COPILOT: &str = "github-copilot";
22pub const TOOL_OPENCODE: &str = "opencode";
24
25const CONFIG_SCHEMA_RELEASE_TAG_PLACEHOLDER: &str = "__ITO_RELEASE_TAG__";
26
27pub fn available_tool_ids() -> &'static [&'static str] {
29 &[TOOL_CLAUDE, TOOL_CODEX, TOOL_GITHUB_COPILOT, TOOL_OPENCODE]
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct InitOptions {
35 pub tools: BTreeSet<String>,
37 pub force: bool,
39 pub update: bool,
46}
47
48impl InitOptions {
49 pub fn new(tools: BTreeSet<String>, force: bool, update: bool) -> Self {
51 Self {
52 tools,
53 force,
54 update,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum InstallMode {
62 Init,
64 Update,
66}
67
68pub fn install_default_templates(
74 project_root: &Path,
75 ctx: &ConfigContext,
76 mode: InstallMode,
77 opts: &InitOptions,
78 worktree_ctx: Option<&WorktreeTemplateContext>,
79) -> CoreResult<()> {
80 let ito_dir_name = get_ito_dir_name(project_root, ctx);
81 let ito_dir = ito_templates::normalize_ito_dir(&ito_dir_name);
82
83 install_project_templates(project_root, &ito_dir, mode, opts, worktree_ctx)?;
84
85 if mode == InstallMode::Init {
88 ensure_repo_gitignore_ignores_session_json(project_root, &ito_dir)?;
89 ensure_repo_gitignore_ignores_audit_session(project_root, &ito_dir)?;
90 ensure_repo_gitignore_unignores_audit_events(project_root, &ito_dir)?;
92 }
93
94 ensure_repo_gitignore_ignores_local_configs(project_root, &ito_dir)?;
96
97 install_adapter_files(project_root, mode, opts, worktree_ctx)?;
98 install_agent_templates(project_root, mode, opts)?;
99 Ok(())
100}
101
102fn ensure_repo_gitignore_ignores_local_configs(
103 project_root: &Path,
104 ito_dir: &str,
105) -> CoreResult<()> {
106 let entry = format!("{ito_dir}/config.local.json");
109 ensure_gitignore_contains_line(project_root, &entry)?;
110
111 let entry = ".local/ito/config.json";
113 ensure_gitignore_contains_line(project_root, entry)?;
114 Ok(())
115}
116
117fn ensure_repo_gitignore_ignores_session_json(
118 project_root: &Path,
119 ito_dir: &str,
120) -> CoreResult<()> {
121 let entry = format!("{ito_dir}/session.json");
122 ensure_gitignore_contains_line(project_root, &entry)
123}
124
125fn ensure_repo_gitignore_ignores_audit_session(
127 project_root: &Path,
128 ito_dir: &str,
129) -> CoreResult<()> {
130 let entry = format!("{ito_dir}/.state/audit/.session");
131 ensure_gitignore_contains_line(project_root, &entry)
132}
133
134fn ensure_repo_gitignore_unignores_audit_events(
140 project_root: &Path,
141 ito_dir: &str,
142) -> CoreResult<()> {
143 let entry = format!("!{ito_dir}/.state/audit/");
144 ensure_gitignore_contains_line(project_root, &entry)
145}
146
147fn ensure_gitignore_contains_line(project_root: &Path, entry: &str) -> CoreResult<()> {
148 let path = project_root.join(".gitignore");
149 let existing = match ito_common::io::read_to_string_std(&path) {
150 Ok(s) => Some(s),
151 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
152 Err(e) => return Err(CoreError::io(format!("reading {}", path.display()), e)),
153 };
154
155 let Some(mut s) = existing else {
156 ito_common::io::write_std(&path, format!("{entry}\n"))
157 .map_err(|e| CoreError::io(format!("writing {}", path.display()), e))?;
158 return Ok(());
159 };
160
161 if gitignore_has_exact_line(&s, entry) {
162 return Ok(());
163 }
164
165 if !s.ends_with('\n') {
166 s.push('\n');
167 }
168 s.push_str(entry);
169 s.push('\n');
170
171 ito_common::io::write_std(&path, s)
172 .map_err(|e| CoreError::io(format!("writing {}", path.display()), e))?;
173 Ok(())
174}
175
176fn gitignore_has_exact_line(contents: &str, entry: &str) -> bool {
177 contents.lines().map(|l| l.trim()).any(|l| l == entry)
178}
179
180fn install_project_templates(
181 project_root: &Path,
182 ito_dir: &str,
183 mode: InstallMode,
184 opts: &InitOptions,
185 worktree_ctx: Option<&WorktreeTemplateContext>,
186) -> CoreResult<()> {
187 use ito_templates::project_templates::render_project_template;
188
189 let selected = &opts.tools;
190 let current_date = Utc::now().format("%Y-%m-%d").to_string();
191 let state_rel = format!("{ito_dir}/planning/STATE.md");
192 let project_md_rel = format!("{ito_dir}/project.md");
193 let config_json_rel = format!("{ito_dir}/config.json");
194 let release_tag = release_tag();
195 let default_ctx = WorktreeTemplateContext::default();
196 let ctx = worktree_ctx.unwrap_or(&default_ctx);
197
198 for f in ito_templates::default_project_files() {
199 let rel = ito_templates::render_rel_path(f.relative_path, ito_dir);
200 let rel = rel.as_ref();
201
202 if !should_install_project_rel(rel, selected) {
203 continue;
204 }
205
206 let mut bytes = ito_templates::render_bytes(f.contents, ito_dir).into_owned();
207 if let Ok(s) = std::str::from_utf8(&bytes) {
208 if rel == state_rel {
209 bytes = s.replace("__CURRENT_DATE__", ¤t_date).into_bytes();
210 } else if rel == config_json_rel {
211 bytes = s
212 .replace(CONFIG_SCHEMA_RELEASE_TAG_PLACEHOLDER, &release_tag)
213 .into_bytes();
214 }
215 }
216
217 if rel == "AGENTS.md" {
222 bytes = render_project_template(&bytes, ctx).map_err(|e| {
223 CoreError::Validation(format!("Failed to render template {rel}: {e}"))
224 })?;
225 }
226
227 if mode == InstallMode::Update
232 && (rel == project_md_rel || rel == config_json_rel)
233 && project_root.join(rel).exists()
234 {
235 continue;
236 }
237
238 let target = project_root.join(rel);
239 write_one(&target, &bytes, mode, opts)?;
240 }
241
242 Ok(())
243}
244
245fn release_tag() -> String {
246 let version = option_env!("ITO_WORKSPACE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
247 if version.starts_with('v') {
248 return version.to_string();
249 }
250
251 format!("v{version}")
252}
253
254fn should_install_project_rel(rel: &str, tools: &BTreeSet<String>) -> bool {
255 if rel == "AGENTS.md" {
257 return true;
258 }
259 if rel.starts_with(".ito/") {
260 return true;
261 }
262
263 if rel == "CLAUDE.md" || rel.starts_with(".claude/") {
265 return tools.contains(TOOL_CLAUDE);
266 }
267 if rel.starts_with(".opencode/") {
268 return tools.contains(TOOL_OPENCODE);
269 }
270 if rel.starts_with(".github/") {
271 return tools.contains(TOOL_GITHUB_COPILOT);
272 }
273 if rel.starts_with(".codex/") {
274 return tools.contains(TOOL_CODEX);
275 }
276
277 false
279}
280
281fn write_one(
282 target: &Path,
283 rendered_bytes: &[u8],
284 mode: InstallMode,
285 opts: &InitOptions,
286) -> CoreResult<()> {
287 if let Some(parent) = target.parent() {
288 ito_common::io::create_dir_all_std(parent)
289 .map_err(|e| CoreError::io(format!("creating directory {}", parent.display()), e))?;
290 }
291
292 if let Ok(text) = std::str::from_utf8(rendered_bytes)
294 && let Some(block) = ito_templates::extract_managed_block(text)
295 {
296 if target.exists() {
297 if mode == InstallMode::Init && opts.force {
298 ito_common::io::write_std(target, rendered_bytes)
299 .map_err(|e| CoreError::io(format!("writing {}", target.display()), e))?;
300 return Ok(());
301 }
302
303 if mode == InstallMode::Init && !opts.force && !opts.update {
304 let existing = ito_common::io::read_to_string_or_default(target);
307 let has_start = existing.contains(ito_templates::ITO_START_MARKER);
308 let has_end = existing.contains(ito_templates::ITO_END_MARKER);
309 if !(has_start && has_end) {
310 return Err(CoreError::Validation(format!(
311 "Refusing to overwrite existing file without markers: {} (re-run with --force)",
312 target.display()
313 )));
314 }
315 }
316
317 update_file_with_markers(
318 target,
319 block,
320 ito_templates::ITO_START_MARKER,
321 ito_templates::ITO_END_MARKER,
322 )
323 .map_err(|e| match e {
324 markers::FsEditError::Io(io_err) => {
325 CoreError::io(format!("updating markers in {}", target.display()), io_err)
326 }
327 markers::FsEditError::Marker(marker_err) => CoreError::Validation(format!(
328 "Failed to update markers in {}: {}",
329 target.display(),
330 marker_err
331 )),
332 })?;
333 } else {
334 ito_common::io::write_std(target, rendered_bytes)
336 .map_err(|e| CoreError::io(format!("writing {}", target.display()), e))?;
337 }
338
339 return Ok(());
340 }
341
342 if mode == InstallMode::Init && target.exists() && !opts.force {
345 if opts.update {
346 return Ok(());
347 }
348 return Err(CoreError::Validation(format!(
349 "Refusing to overwrite existing file without markers: {} (re-run with --force)",
350 target.display()
351 )));
352 }
353
354 ito_common::io::write_std(target, rendered_bytes)
355 .map_err(|e| CoreError::io(format!("writing {}", target.display()), e))?;
356 Ok(())
357}
358
359fn install_adapter_files(
360 project_root: &Path,
361 _mode: InstallMode,
362 opts: &InitOptions,
363 worktree_ctx: Option<&WorktreeTemplateContext>,
364) -> CoreResult<()> {
365 for tool in &opts.tools {
366 match tool.as_str() {
367 TOOL_OPENCODE => {
368 let config_dir = project_root.join(".opencode");
369 let manifests = crate::distribution::opencode_manifests(&config_dir);
370 crate::distribution::install_manifests(&manifests, worktree_ctx)?;
371 }
372 TOOL_CLAUDE => {
373 let manifests = crate::distribution::claude_manifests(project_root);
374 crate::distribution::install_manifests(&manifests, worktree_ctx)?;
375 }
376 TOOL_CODEX => {
377 let manifests = crate::distribution::codex_manifests(project_root);
378 crate::distribution::install_manifests(&manifests, worktree_ctx)?;
379 }
380 TOOL_GITHUB_COPILOT => {
381 let manifests = crate::distribution::github_manifests(project_root);
382 crate::distribution::install_manifests(&manifests, worktree_ctx)?;
383 }
384 _ => {}
385 }
386 }
387
388 Ok(())
389}
390
391fn install_agent_templates(
393 project_root: &Path,
394 mode: InstallMode,
395 opts: &InitOptions,
396) -> CoreResult<()> {
397 use ito_templates::agents::{
398 AgentTier, Harness, default_agent_configs, get_agent_files, render_agent_template,
399 };
400
401 let configs = default_agent_configs();
402
403 let tool_harness_map = [
405 (TOOL_OPENCODE, Harness::OpenCode),
406 (TOOL_CLAUDE, Harness::ClaudeCode),
407 (TOOL_CODEX, Harness::Codex),
408 (TOOL_GITHUB_COPILOT, Harness::GitHubCopilot),
409 ];
410
411 for (tool_id, harness) in tool_harness_map {
412 if !opts.tools.contains(tool_id) {
413 continue;
414 }
415
416 let agent_dir = project_root.join(harness.project_agent_path());
417
418 let files = get_agent_files(harness);
420
421 for (rel_path, contents) in files {
422 let target = agent_dir.join(rel_path);
423
424 let tier = if rel_path.contains("ito-quick") || rel_path.contains("quick") {
426 Some(AgentTier::Quick)
427 } else if rel_path.contains("ito-general") || rel_path.contains("general") {
428 Some(AgentTier::General)
429 } else if rel_path.contains("ito-thinking") || rel_path.contains("thinking") {
430 Some(AgentTier::Thinking)
431 } else {
432 None
433 };
434
435 let config = tier.and_then(|t| configs.get(&(harness, t)));
437
438 match mode {
439 InstallMode::Init => {
440 if target.exists() {
441 if opts.update {
442 if let Some(cfg) = config {
444 update_agent_model_field(&target, &cfg.model)?;
445 }
446 continue;
447 }
448 if !opts.force {
449 continue;
451 }
452 }
453
454 let rendered = if let Some(cfg) = config {
456 if let Ok(template_str) = std::str::from_utf8(contents) {
457 render_agent_template(template_str, cfg).into_bytes()
458 } else {
459 contents.to_vec()
460 }
461 } else {
462 contents.to_vec()
463 };
464
465 if let Some(parent) = target.parent() {
467 ito_common::io::create_dir_all_std(parent).map_err(|e| {
468 CoreError::io(format!("creating directory {}", parent.display()), e)
469 })?;
470 }
471
472 ito_common::io::write_std(&target, rendered)
473 .map_err(|e| CoreError::io(format!("writing {}", target.display()), e))?;
474 }
475 InstallMode::Update => {
476 if !target.exists() {
478 let rendered = if let Some(cfg) = config {
480 if let Ok(template_str) = std::str::from_utf8(contents) {
481 render_agent_template(template_str, cfg).into_bytes()
482 } else {
483 contents.to_vec()
484 }
485 } else {
486 contents.to_vec()
487 };
488
489 if let Some(parent) = target.parent() {
490 ito_common::io::create_dir_all_std(parent).map_err(|e| {
491 CoreError::io(format!("creating directory {}", parent.display()), e)
492 })?;
493 }
494 ito_common::io::write_std(&target, rendered).map_err(|e| {
495 CoreError::io(format!("writing {}", target.display()), e)
496 })?;
497 } else if let Some(cfg) = config {
498 update_agent_model_field(&target, &cfg.model)?;
500 }
501 }
502 }
503 }
504 }
505
506 Ok(())
507}
508
509fn update_agent_model_field(path: &Path, new_model: &str) -> CoreResult<()> {
511 let content = ito_common::io::read_to_string_or_default(path);
512
513 if !content.starts_with("---") {
515 return Ok(());
516 }
517
518 let rest = &content[3..];
520 let Some(end_idx) = rest.find("\n---") else {
521 return Ok(());
522 };
523
524 let frontmatter = &rest[..end_idx];
525 let body = &rest[end_idx + 4..]; let updated_frontmatter = update_model_in_yaml(frontmatter, new_model);
529
530 let updated = format!("---{}\n---{}", updated_frontmatter, body);
532 ito_common::io::write_std(path, updated)
533 .map_err(|e| CoreError::io(format!("writing {}", path.display()), e))?;
534
535 Ok(())
536}
537
538fn update_model_in_yaml(yaml: &str, new_model: &str) -> String {
540 let mut lines: Vec<String> = yaml.lines().map(|l| l.to_string()).collect();
541 let mut found = false;
542
543 for line in &mut lines {
544 if line.trim_start().starts_with("model:") {
545 *line = format!("model: \"{}\"", new_model);
546 found = true;
547 break;
548 }
549 }
550
551 if !found {
553 lines.push(format!("model: \"{}\"", new_model));
554 }
555
556 lines.join("\n")
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562
563 #[test]
564 fn gitignore_created_when_missing() {
565 let td = tempfile::tempdir().unwrap();
566 ensure_repo_gitignore_ignores_session_json(td.path(), ".ito").unwrap();
567 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
568 assert_eq!(s, ".ito/session.json\n");
569 }
570
571 #[test]
572 fn gitignore_noop_when_already_present() {
573 let td = tempfile::tempdir().unwrap();
574 std::fs::write(td.path().join(".gitignore"), ".ito/session.json\n").unwrap();
575 ensure_repo_gitignore_ignores_session_json(td.path(), ".ito").unwrap();
576 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
577 assert_eq!(s, ".ito/session.json\n");
578 }
579
580 #[test]
581 fn gitignore_does_not_duplicate_on_repeated_calls() {
582 let td = tempfile::tempdir().unwrap();
583 std::fs::write(td.path().join(".gitignore"), "node_modules\n").unwrap();
584 ensure_repo_gitignore_ignores_session_json(td.path(), ".ito").unwrap();
585 ensure_repo_gitignore_ignores_session_json(td.path(), ".ito").unwrap();
586 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
587 assert_eq!(s, "node_modules\n.ito/session.json\n");
588 }
589
590 #[test]
591 fn gitignore_audit_session_added() {
592 let td = tempfile::tempdir().unwrap();
593 ensure_repo_gitignore_ignores_audit_session(td.path(), ".ito").unwrap();
594 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
595 assert!(s.contains(".ito/.state/audit/.session"));
596 }
597
598 #[test]
599 fn gitignore_both_session_entries() {
600 let td = tempfile::tempdir().unwrap();
601 ensure_repo_gitignore_ignores_session_json(td.path(), ".ito").unwrap();
602 ensure_repo_gitignore_ignores_audit_session(td.path(), ".ito").unwrap();
603 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
604 assert!(s.contains(".ito/session.json"));
605 assert!(s.contains(".ito/.state/audit/.session"));
606 }
607
608 #[test]
609 fn gitignore_preserves_existing_content_and_adds_newline_if_missing() {
610 let td = tempfile::tempdir().unwrap();
611 std::fs::write(td.path().join(".gitignore"), "node_modules").unwrap();
612 ensure_repo_gitignore_ignores_session_json(td.path(), ".ito").unwrap();
613 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
614 assert_eq!(s, "node_modules\n.ito/session.json\n");
615 }
616
617 #[test]
618 fn gitignore_audit_events_unignored() {
619 let td = tempfile::tempdir().unwrap();
620 ensure_repo_gitignore_unignores_audit_events(td.path(), ".ito").unwrap();
621 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
622 assert!(s.contains("!.ito/.state/audit/"));
623 }
624
625 #[test]
626 fn gitignore_full_audit_setup() {
627 let td = tempfile::tempdir().unwrap();
628 std::fs::write(td.path().join(".gitignore"), ".ito/.state/\n").unwrap();
630 ensure_repo_gitignore_ignores_audit_session(td.path(), ".ito").unwrap();
631 ensure_repo_gitignore_unignores_audit_events(td.path(), ".ito").unwrap();
632 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
633 assert!(s.contains(".ito/.state/audit/.session"));
634 assert!(s.contains("!.ito/.state/audit/"));
635 }
636
637 #[test]
638 fn gitignore_ignores_local_configs() {
639 let td = tempfile::tempdir().unwrap();
640 ensure_repo_gitignore_ignores_local_configs(td.path(), ".ito").unwrap();
641 let s = std::fs::read_to_string(td.path().join(".gitignore")).unwrap();
642 assert!(s.contains(".ito/config.local.json"));
643 assert!(s.contains(".local/ito/config.json"));
644 }
645
646 #[test]
647 fn gitignore_exact_line_matching_trims_whitespace() {
648 assert!(gitignore_has_exact_line(" foo \nbar\n", "foo"));
649 assert!(!gitignore_has_exact_line("foo\n", "bar"));
650 }
651
652 #[test]
653 fn should_install_project_rel_filters_by_tool_id() {
654 let mut tools = BTreeSet::new();
655 tools.insert(TOOL_OPENCODE.to_string());
656
657 assert!(should_install_project_rel("AGENTS.md", &tools));
658 assert!(should_install_project_rel(".ito/config.json", &tools));
659 assert!(should_install_project_rel(".opencode/config.json", &tools));
660 assert!(!should_install_project_rel(".claude/settings.json", &tools));
661 assert!(!should_install_project_rel(".codex/config.json", &tools));
662 assert!(!should_install_project_rel(
663 ".github/workflows/x.yml",
664 &tools
665 ));
666 }
667
668 #[test]
669 fn release_tag_is_prefixed_with_v() {
670 let tag = release_tag();
671 assert!(tag.starts_with('v'));
672 }
673
674 #[test]
675 fn update_model_in_yaml_replaces_or_inserts() {
676 let yaml = "name: test\nmodel: \"old\"\n";
677 let updated = update_model_in_yaml(yaml, "new");
678 assert!(updated.contains("model: \"new\""));
679
680 let yaml = "name: test\n";
681 let updated = update_model_in_yaml(yaml, "new");
682 assert!(updated.contains("model: \"new\""));
683 }
684
685 #[test]
686 fn update_agent_model_field_updates_frontmatter_when_present() {
687 let td = tempfile::tempdir().unwrap();
688 let path = td.path().join("agent.md");
689 std::fs::write(&path, "---\nname: test\nmodel: \"old\"\n---\nbody\n").unwrap();
690 update_agent_model_field(&path, "new").unwrap();
691 let s = std::fs::read_to_string(&path).unwrap();
692 assert!(s.contains("model: \"new\""));
693
694 let path = td.path().join("no-frontmatter.md");
695 std::fs::write(&path, "no frontmatter\n").unwrap();
696 update_agent_model_field(&path, "newer").unwrap();
697 let s = std::fs::read_to_string(&path).unwrap();
698 assert_eq!(s, "no frontmatter\n");
699 }
700
701 #[test]
702 fn write_one_non_marker_files_skip_on_init_update_mode() {
703 let td = tempfile::tempdir().unwrap();
704 let target = td.path().join("plain.txt");
705 std::fs::write(&target, "existing").unwrap();
706
707 let opts = InitOptions::new(BTreeSet::new(), false, true);
708 write_one(&target, b"new", InstallMode::Init, &opts).unwrap();
709 let s = std::fs::read_to_string(&target).unwrap();
710 assert_eq!(s, "existing");
711 }
712
713 #[test]
714 fn write_one_marker_managed_files_refuse_overwrite_without_markers() {
715 let td = tempfile::tempdir().unwrap();
716 let target = td.path().join("managed.md");
717 std::fs::write(&target, "existing without markers\n").unwrap();
718
719 let template = format!(
720 "before\n{}\nmanaged\n{}\nafter\n",
721 ito_templates::ITO_START_MARKER,
722 ito_templates::ITO_END_MARKER
723 );
724 let opts = InitOptions::new(BTreeSet::new(), false, false);
725 let err = write_one(&target, template.as_bytes(), InstallMode::Init, &opts).unwrap_err();
726 assert!(err.to_string().contains("Refusing to overwrite"));
727 }
728
729 #[test]
730 fn write_one_marker_managed_files_update_existing_markers() {
731 let td = tempfile::tempdir().unwrap();
732 let target = td.path().join("managed.md");
733 let existing = format!(
734 "before\n{}\nold\n{}\nafter\n",
735 ito_templates::ITO_START_MARKER,
736 ito_templates::ITO_END_MARKER
737 );
738 std::fs::write(&target, existing).unwrap();
739
740 let template = format!(
741 "before\n{}\nnew\n{}\nafter\n",
742 ito_templates::ITO_START_MARKER,
743 ito_templates::ITO_END_MARKER
744 );
745 let opts = InitOptions::new(BTreeSet::new(), false, false);
746 write_one(&target, template.as_bytes(), InstallMode::Init, &opts).unwrap();
747 let s = std::fs::read_to_string(&target).unwrap();
748 assert!(s.contains("new"));
749 assert!(!s.contains("old"));
750 }
751
752 #[test]
753 fn write_one_marker_managed_files_error_when_markers_missing_in_update_mode() {
754 let td = tempfile::tempdir().unwrap();
755 let target = td.path().join("managed.md");
756 std::fs::write(
758 &target,
759 format!(
760 "{}\nexisting without end marker\n",
761 ito_templates::ITO_START_MARKER
762 ),
763 )
764 .unwrap();
765
766 let template = format!(
767 "before\n{}\nmanaged\n{}\nafter\n",
768 ito_templates::ITO_START_MARKER,
769 ito_templates::ITO_END_MARKER
770 );
771 let opts = InitOptions::new(BTreeSet::new(), false, true);
772 let err = write_one(&target, template.as_bytes(), InstallMode::Init, &opts).unwrap_err();
773 assert!(err.to_string().contains("Failed to update markers"));
774 }
775}