1include!(concat!(env!("OUT_DIR"), "/bundled_agents.rs"));
15
16use std::path::Path;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum AgentAction {
22 Install,
24 Update {
26 from: String,
28 },
29 Modified,
32 UpToDate,
34}
35
36impl AgentAction {
37 pub fn is_change(&self) -> bool {
39 !matches!(self, Self::UpToDate)
40 }
41
42 pub fn preselect(&self) -> bool {
49 matches!(self, Self::Install | Self::Update { .. })
50 }
51
52 pub fn label(&self, to: &str) -> String {
54 match self {
55 Self::Install => format!("install {to}"),
56 Self::Update { from } => format!("update {from} → {to}"),
57 Self::Modified => format!("{to}, edited locally - reinstall overwrites"),
58 Self::UpToDate => "up to date".to_string(),
59 }
60 }
61}
62
63pub fn installed_version(agents_dir: &Path, name: &str) -> Option<String> {
71 let manifest = std::fs::read_to_string(agents_dir.join(name).join("agent.leviath")).ok()?;
72 leviath_core::manifest::parse_manifest(&manifest)
73 .ok()
74 .map(|bp| bp.version)
75}
76
77fn matches_bundled(agent: &BundledAgent, agents_dir: &Path) -> bool {
88 let dest = agents_dir.join(agent.name);
89 for (rel, contents) in agent.files {
90 match std::fs::read_to_string(dest.join(rel)) {
91 Ok(on_disk) if on_disk == *contents => {}
92 _ => return false,
93 }
94 }
95 installed_file_count(&dest) == agent.files.len()
98}
99
100fn installed_file_count(dir: &Path) -> usize {
106 let Ok(entries) = std::fs::read_dir(dir) else {
107 return 0;
108 };
109 entries
110 .map(|entry| match entry.map(|e| e.path()) {
111 Ok(path) if path.is_dir() => installed_file_count(&path),
112 _ => 1,
113 })
114 .sum()
115}
116
117pub fn plan_agent_actions(agents_dir: &Path) -> Vec<(&'static BundledAgent, AgentAction)> {
132 BUNDLED_AGENTS
133 .iter()
134 .map(|agent| {
135 let action = match installed_version(agents_dir, agent.name) {
136 None => AgentAction::Install,
137 Some(v) if v != agent.version => AgentAction::Update { from: v },
138 Some(_) if matches_bundled(agent, agents_dir) => AgentAction::UpToDate,
139 Some(_) => AgentAction::Modified,
140 };
141 (agent, action)
142 })
143 .collect()
144}
145
146pub fn stale_install_note(
158 manifest_path: &Path,
159 blueprint: &leviath_core::Blueprint,
160 agents_dir: Option<&Path>,
161) -> Option<String> {
162 let installed = agents_dir?.join(&blueprint.name);
163 if !manifest_path.starts_with(&installed) {
164 return None;
165 }
166 let bundled = BUNDLED_AGENTS.iter().find(|a| a.name == blueprint.name)?;
167 if bundled.version == blueprint.version {
168 return None;
169 }
170 Some(format!(
171 "note: '{}' is installed at {}, and this build ships {}. \
172 Run `lev setup` to update it.",
173 blueprint.name, blueprint.version, bundled.version
174 ))
175}
176
177pub fn install_bundled(agent: &BundledAgent, agents_dir: &Path) -> anyhow::Result<()> {
185 let dest = agents_dir.join(agent.name);
186 if dest.exists() {
187 std::fs::remove_dir_all(&dest)?;
188 }
189 for (rel, contents) in agent.files {
190 let parent = match rel.rsplit_once('/') {
196 Some((dir, _)) => dest.join(dir),
197 None => dest.clone(),
198 };
199 std::fs::create_dir_all(&parent)?;
200 std::fs::write(dest.join(rel), contents)?;
201 }
202 Ok(())
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
213 fn every_bundled_agent_has_a_name_version_and_manifest() {
214 assert!(
215 !BUNDLED_AGENTS.is_empty(),
216 "the binary shipped with no blueprints -- build.rs found no agents/ directory"
217 );
218 for agent in BUNDLED_AGENTS {
219 assert!(!agent.name.is_empty(), "a bundled agent has an empty name");
220 assert!(
221 !agent.version.is_empty(),
222 "bundled agent {} has an empty version",
223 agent.name
224 );
225 assert!(
226 agent.files.iter().any(|(rel, _)| *rel == "agent.leviath"),
227 "bundled agent {} has no agent.leviath",
228 agent.name
229 );
230 for (rel, contents) in agent.files {
231 assert!(
232 !rel.is_empty(),
233 "bundled agent {} has an empty path",
234 agent.name
235 );
236 assert!(
237 !contents.is_empty(),
238 "bundled agent {} has an empty file {rel}",
239 agent.name
240 );
241 }
242 }
243 }
244
245 #[test]
259 fn a_tool_script_shared_by_several_agents_is_identical_in_all_of_them() {
260 use std::collections::HashMap;
261
262 let mut first_seen: HashMap<&str, (&str, &str)> = HashMap::new();
264 for agent in BUNDLED_AGENTS {
265 for (rel, contents) in agent.files {
266 let Some(filename) = rel.strip_prefix("tools/") else {
267 continue;
268 };
269 match first_seen.get(filename) {
270 Some((other, expected)) => assert!(
271 expected == contents,
272 "tools/{filename} differs between bundled agents {other} and {} - \
273 a change to one copy was not applied to the others",
274 agent.name
275 ),
276 None => {
277 first_seen.insert(filename, (agent.name, contents));
278 }
279 }
280 }
281 }
282 assert!(
285 !first_seen.is_empty(),
286 "no bundled agent ships a tools/ script - this invariant is not being tested"
287 );
288 }
289
290 #[test]
291 fn every_bundled_manifest_parses_and_agrees_with_its_recorded_version() {
292 for agent in BUNDLED_AGENTS {
295 let manifest = agent
296 .files
297 .iter()
298 .find(|(rel, _)| *rel == "agent.leviath")
299 .map(|(_, c)| *c)
300 .expect("checked above");
301 let parsed = leviath_core::manifest::parse_manifest(manifest);
307 assert!(
308 parsed.is_ok(),
309 "bundled agent {} does not parse",
310 agent.name
311 );
312 let blueprint = parsed.expect("asserted Ok just above");
313 assert_eq!(blueprint.version, agent.version);
314 assert_eq!(blueprint.name, agent.name);
315 }
316 }
317
318 #[test]
331 fn every_bundled_agent_ends_by_handing_something_back() {
332 for agent in BUNDLED_AGENTS {
333 let manifest = agent
334 .files
335 .iter()
336 .find(|(rel, _)| *rel == "agent.leviath")
337 .map(|(_, c)| *c)
338 .expect("checked above");
339 let blueprint = leviath_core::manifest::parse_manifest(manifest)
340 .expect("checked by every_bundled_manifest_parses");
341
342 let outputs: Vec<&leviath_core::Stage> = blueprint
343 .stages
344 .iter()
345 .filter(|s| s.mode == leviath_core::blueprint::StageMode::Output)
346 .collect();
347 assert!(
348 !outputs.is_empty(),
349 "bundled agent {} has no output stage, so a run of it hands back nothing",
350 agent.name
351 );
352
353 for stage in &outputs {
354 assert!(stage.require_output, "{} output stage", agent.name);
357 assert!(
358 stage
359 .available_tools
360 .iter()
361 .any(|t| t == leviath_core::blueprint::SUBMIT_OUTPUT_TOOL),
362 "{} output stage cannot submit",
363 agent.name
364 );
365 assert!(
367 !stage.available_tools.iter().any(|t| {
368 leviath_core::blueprint::MODIFYING_TOOLS
369 .contains(&leviath_tools::canonical_tool_name(t))
370 }),
371 "{} output stage can modify files",
372 agent.name
373 );
374 }
375
376 for stage in &blueprint.stages {
377 assert!(
378 !stage.allow_complete
379 || stage.mode == leviath_core::blueprint::StageMode::Output,
380 "bundled agent {}: stage '{}' may end the run, skipping the output stage",
381 agent.name,
382 stage.name
383 );
384 }
385 }
386 }
387
388 const SETUP_PROVIDERS: &[&str] = &["anthropic", "openai", "google", "openrouter", "ollama"];
391
392 const BLUEPRINT_SCHEMA: &str = include_str!("../../../docs/schema/blueprint.schema.json");
398
399 fn schema_problems(
405 validator: &jsonschema::Validator,
406 value: &serde_json::Value,
407 ) -> Vec<String> {
408 validator
409 .iter_errors(value)
410 .map(|e| format!("{}: {e}", e.instance_path()))
411 .collect()
412 }
413
414 fn toml_to_json(value: &toml::Value) -> serde_json::Value {
416 match value {
417 toml::Value::String(s) => serde_json::Value::String(s.clone()),
418 toml::Value::Integer(i) => serde_json::Value::from(*i),
419 toml::Value::Float(f) => serde_json::Value::from(*f),
420 toml::Value::Boolean(b) => serde_json::Value::Bool(*b),
421 toml::Value::Datetime(d) => serde_json::Value::String(d.to_string()),
425 toml::Value::Array(items) => {
426 serde_json::Value::Array(items.iter().map(toml_to_json).collect())
427 }
428 toml::Value::Table(table) => serde_json::Value::Object(
429 table
430 .iter()
431 .map(|(k, v)| (k.clone(), toml_to_json(v)))
432 .collect(),
433 ),
434 }
435 }
436
437 #[test]
438 fn toml_converts_to_json_for_every_value_kind() {
439 let source = concat!(
444 "s = \"text\"\n",
445 "i = 7\n",
446 "f = 0.5\n",
447 "b = true\n",
448 "d = 1979-05-27T07:32:00Z\n",
449 "a = [1, \"two\"]\n",
450 "[t]\n",
451 "nested = 1\n"
452 );
453 let parsed: toml::Value = toml::from_str(source).expect("valid TOML");
454 let json = toml_to_json(&parsed);
455 assert_eq!(json["s"], serde_json::json!("text"));
456 assert_eq!(json["i"], serde_json::json!(7));
457 assert_eq!(json["f"], serde_json::json!(0.5));
458 assert_eq!(json["b"], serde_json::json!(true));
459 assert!(json["d"].is_string());
461 assert_eq!(json["a"], serde_json::json!([1, "two"]));
462 assert_eq!(json["t"]["nested"], serde_json::json!(1));
463 }
464
465 #[test]
466 fn every_bundled_blueprint_validates_against_the_published_schema() {
467 let schema: serde_json::Value =
472 serde_json::from_str(BLUEPRINT_SCHEMA).expect("the schema is valid JSON");
473 let validator = jsonschema::validator_for(&schema).expect("the schema compiles");
474
475 for agent in BUNDLED_AGENTS {
476 let manifest = agent
477 .files
478 .iter()
479 .find(|(rel, _)| *rel == "agent.leviath")
480 .map(|(_, c)| *c)
481 .expect("every bundled agent has a manifest");
482 let parsed: toml::Value = toml::from_str(manifest).expect("the manifest is valid TOML");
483 let json = toml_to_json(&parsed);
484
485 assert_eq!(
486 schema_problems(&validator, &json),
487 Vec::<String>::new(),
488 "{} does not match blueprint.schema.json",
489 agent.name
490 );
491 }
492 }
493
494 #[test]
495 fn the_blueprint_schema_rejects_what_the_parser_rejects() {
496 let schema: serde_json::Value =
500 serde_json::from_str(BLUEPRINT_SCHEMA).expect("the schema is valid JSON");
501 let validator = jsonschema::validator_for(&schema).expect("the schema compiles");
502 let rejects = |manifest: &str| {
506 let parsed: toml::Value = toml::from_str(manifest).expect("valid TOML");
507 !schema_problems(&validator, &toml_to_json(&parsed)).is_empty()
508 };
509
510 assert!(
511 rejects("[stages.main]\nmode = \"autonomous\"\n"),
512 "no [agent]"
513 );
514 assert!(
515 rejects("[agent]\nname = \"a\"\n\n[context.regions]\nx = { kind = \"nonsense\" }\n"),
516 "unknown region kind"
517 );
518 assert!(
519 rejects(
520 "[agent]\nname = \"a\"\n\n[stages.main.transitions.other]\ncondition = \"whenever\"\n"
521 ),
522 "unknown transition condition"
523 );
524 assert!(
525 rejects("[agent]\nname = \"a\"\n\n[stages.main]\nmax_iteratoins = 5\n"),
526 "a typo'd stage key"
527 );
528 assert!(
529 rejects("[agent]\nname = \"a\"\n\n[tool_permissions]\nshell = \"maybe\"\n"),
530 "an invalid tool policy"
531 );
532 assert!(!rejects("[agent]\nname = \"a\"\n"), "a minimal manifest");
535 }
536
537 #[test]
538 fn every_bundled_stage_offers_every_provider_setup_can_configure() {
539 for agent in BUNDLED_AGENTS {
546 let manifest = agent
547 .files
548 .iter()
549 .find(|(rel, _)| *rel == "agent.leviath")
550 .map(|(_, c)| *c)
551 .expect("every bundled agent has a manifest");
552 let blueprint =
553 leviath_core::manifest::parse_manifest(manifest).expect("manifest parses");
554
555 for stage in &blueprint.stages {
556 let stage_name = &stage.name;
557 let listed: Vec<&str> = stage
558 .model
559 .models
560 .iter()
561 .map(|entry| entry.provider.as_str())
562 .collect();
563 for provider in SETUP_PROVIDERS {
564 assert!(
565 listed.contains(provider),
566 "{}/{} omits provider {}",
567 agent.name,
568 stage_name,
569 provider
570 );
571 }
572 assert_eq!(
576 listed.last().copied(),
577 Some("ollama"),
578 "{}/{} must list ollama last",
579 agent.name,
580 stage_name
581 );
582 }
583 }
584 }
585
586 fn lint_env_for(agent: &BundledAgent) -> crate::lint::LintEnv {
593 let mut known_tools: std::collections::HashSet<String> = leviath_tools::BuiltinTools::new(
594 leviath_tools::ToolContext::new(std::path::PathBuf::from(".")),
595 )
596 .names()
597 .into_iter()
598 .collect();
599 known_tools.extend(leviath_tools::BuiltinTools::subagent_tool_names());
600 known_tools.extend(
601 agent
602 .files
603 .iter()
604 .filter_map(|(rel, _)| rel.strip_prefix("tools/"))
605 .filter_map(|f| f.strip_suffix(".rhai"))
606 .map(str::to_string),
607 );
608 crate::lint::LintEnv {
609 known_tools,
610 known_models: crate::commands::models::closed_catalog_models(),
611 available_providers: None,
612 read_paths: None,
613 safe_commands_granted: None,
614 }
615 }
616
617 #[test]
629 fn no_bundled_agent_has_a_lint_error() {
630 for agent in BUNDLED_AGENTS {
631 let manifest = agent
632 .files
633 .iter()
634 .find(|(rel, _)| *rel == "agent.leviath")
635 .map(|(_, c)| *c)
636 .expect("every bundled agent has a manifest");
637 let parsed = leviath_core::manifest::parse_manifest(manifest);
638 assert!(
639 parsed.is_ok(),
640 "bundled agent {} does not parse",
641 agent.name
642 );
643 let blueprint = parsed.expect("asserted Ok just above");
644 let rendered: Vec<(bool, String)> =
651 crate::lint::lint_manifest(manifest, &blueprint, &lint_env_for(agent))
652 .iter()
653 .map(|f| (f.is_error(), format!("{} [{}]", f.one_line(), f.code)))
654 .collect();
655 let error_count = rendered.iter().filter(|(is_error, _)| *is_error).count();
656 assert_eq!(
657 error_count, 0,
658 "bundled agent {} has lint errors, among {rendered:?}",
659 agent.name
660 );
661 }
662 }
663
664 #[test]
667 fn the_lint_invariant_catches_a_typo_and_an_orphan_permission() {
668 let manifest = r#"
669[agent]
670name = "x"
671version = "0.1.0"
672description = "x"
673
674[stages.only]
675mode = "autonomous"
676model = { provider = "anthropic", model = "claude-sonnet-5" }
677max_iterations = 5
678available_tools = ["read_file", "raed_file"]
679
680[stages.only.tool_permissions]
681write_file = "allow"
682"#;
683 let bp = leviath_core::manifest::parse_manifest(manifest)
684 .expect("the fixture parses; it is the lint that should object");
685 let env = lint_env_for(&BundledAgent {
687 name: "x",
688 version: "0.1.0",
689 files: &[],
690 });
691 let codes: Vec<&str> = crate::lint::lint_manifest(manifest, &bp, &env)
692 .iter()
693 .filter(|f| f.is_error())
694 .map(|f| f.code)
695 .collect();
696 assert_eq!(codes, ["unknown-tool", "orphan-stage-permission"]);
697 }
698
699 #[test]
700 fn bundled_agent_names_are_unique() {
701 let mut names: Vec<&str> = BUNDLED_AGENTS.iter().map(|a| a.name).collect();
702 names.sort_unstable();
703 let count = names.len();
704 names.dedup();
705 assert_eq!(count, names.len(), "duplicate bundled agent names");
706 }
707
708 #[test]
711 fn installed_version_reads_a_manifest() {
712 let dir = tempfile::tempdir().unwrap();
713 let agent = &BUNDLED_AGENTS[0];
714 install_bundled(agent, dir.path()).unwrap();
715
716 assert_eq!(
717 installed_version(dir.path(), agent.name).as_deref(),
718 Some(agent.version)
719 );
720 }
721
722 #[test]
723 fn installed_version_is_none_when_nothing_is_installed() {
724 let dir = tempfile::tempdir().unwrap();
725 assert!(installed_version(dir.path(), "not-installed").is_none());
726 }
727
728 #[test]
729 fn installed_version_is_none_for_an_unparseable_manifest() {
730 let dir = tempfile::tempdir().unwrap();
733 std::fs::create_dir_all(dir.path().join("broken")).unwrap();
734 std::fs::write(
735 dir.path().join("broken/agent.leviath"),
736 "not valid toml {{{",
737 )
738 .unwrap();
739
740 assert!(installed_version(dir.path(), "broken").is_none());
741 }
742
743 #[test]
746 fn plan_offers_to_install_everything_into_an_empty_dir() {
747 let dir = tempfile::tempdir().unwrap();
748
749 let plan = plan_agent_actions(dir.path());
750
751 assert_eq!(plan.len(), BUNDLED_AGENTS.len());
752 for (agent, action) in &plan {
753 assert_eq!(*action, AgentAction::Install);
754 assert!(action.is_change());
755 assert_eq!(
756 action.label(agent.version),
757 format!("install {}", agent.version)
758 );
759 }
760 }
761
762 #[test]
763 fn plan_reports_up_to_date_after_installing() {
764 let dir = tempfile::tempdir().unwrap();
765 for agent in BUNDLED_AGENTS {
766 install_bundled(agent, dir.path()).unwrap();
767 }
768
769 let plan = plan_agent_actions(dir.path());
770
771 for (agent, action) in &plan {
772 assert_eq!(*action, AgentAction::UpToDate, "{}", agent.name);
773 assert!(!action.is_change());
774 assert_eq!(action.label(agent.version), "up to date");
775 }
776 }
777
778 #[test]
779 fn plan_reports_an_update_when_the_installed_version_differs() {
780 let dir = tempfile::tempdir().unwrap();
781 let agent = &BUNDLED_AGENTS[0];
782 install_bundled(agent, dir.path()).unwrap();
783 let manifest_path = dir.path().join(agent.name).join("agent.leviath");
785 let manifest = std::fs::read_to_string(&manifest_path).unwrap();
786 let bumped = manifest.replacen(
787 &format!("version = \"{}\"", agent.version),
788 "version = \"9.9.9\"",
789 1,
790 );
791 std::fs::write(&manifest_path, bumped).unwrap();
792
793 let plan = plan_agent_actions(dir.path());
794 let (_, action) = plan
795 .iter()
796 .find(|(a, _)| a.name == agent.name)
797 .expect("the bundled agent is in the plan");
798
799 assert_eq!(
800 *action,
801 AgentAction::Update {
802 from: "9.9.9".to_string()
803 }
804 );
805 assert!(action.is_change());
806 assert_eq!(
807 action.label(agent.version),
808 format!("update 9.9.9 → {}", agent.version)
809 );
810 }
811
812 #[test]
816 fn plan_reports_an_edited_install_as_modified() {
817 let dir = tempfile::tempdir().unwrap();
818 let agent = &BUNDLED_AGENTS[0];
819 install_bundled(agent, dir.path()).unwrap();
820 let manifest_path = dir.path().join(agent.name).join("agent.leviath");
821 let manifest = std::fs::read_to_string(&manifest_path).unwrap();
822 std::fs::write(&manifest_path, manifest + "\n# a local edit\n").unwrap();
823
824 let action = action_for(&plan_agent_actions(dir.path()), agent.name);
825 assert_eq!(action, AgentAction::Modified);
826 assert!(action.is_change());
829 assert!(!action.preselect());
830 let label = action.label(agent.version);
831 assert!(label.contains("edited locally"), "{label}");
832 }
833
834 #[test]
838 fn a_file_the_user_added_or_removed_counts_as_modified() {
839 let agent = &BUNDLED_AGENTS[0];
840
841 let added = tempfile::tempdir().unwrap();
842 install_bundled(agent, added.path()).unwrap();
843 std::fs::write(added.path().join(agent.name).join("notes.md"), "mine").unwrap();
844 assert_eq!(
845 action_for(&plan_agent_actions(added.path()), agent.name),
846 AgentAction::Modified
847 );
848
849 let multi = BUNDLED_AGENTS
853 .iter()
854 .find(|a| a.files.len() > 1)
855 .expect("some bundled blueprint ships more than its manifest");
856 let removed = tempfile::tempdir().unwrap();
857 install_bundled(multi, removed.path()).unwrap();
858 let extra = multi
859 .files
860 .iter()
861 .map(|(rel, _)| *rel)
862 .find(|rel| *rel != "agent.leviath")
863 .expect("a file other than the manifest");
864 std::fs::remove_file(removed.path().join(multi.name).join(extra)).unwrap();
865 assert_eq!(
866 action_for(&plan_agent_actions(removed.path()), multi.name),
867 AgentAction::Modified
868 );
869 }
870
871 #[test]
874 fn an_unreadable_tree_is_not_up_to_date() {
875 assert_eq!(installed_file_count(Path::new("/no/such/dir")), 0);
876 let dir = tempfile::tempdir().unwrap();
877 assert!(!matches_bundled(&BUNDLED_AGENTS[0], dir.path()));
878 }
879
880 #[test]
881 fn installed_file_count_walks_nested_directories() {
882 let dir = tempfile::tempdir().unwrap();
883 std::fs::create_dir_all(dir.path().join("a/b")).unwrap();
884 std::fs::write(dir.path().join("top.txt"), "x").unwrap();
885 std::fs::write(dir.path().join("a/mid.txt"), "x").unwrap();
886 std::fs::write(dir.path().join("a/b/leaf.txt"), "x").unwrap();
887 assert_eq!(installed_file_count(dir.path()), 3);
888 }
889
890 fn action_for(plan: &[(&'static BundledAgent, AgentAction)], name: &str) -> AgentAction {
891 plan.iter()
892 .find(|(a, _)| a.name == name)
893 .expect("the bundled agent is in the plan")
894 .1
895 .clone()
896 }
897
898 #[test]
903 fn a_stale_install_is_named_when_the_run_starts() {
904 let dir = tempfile::tempdir().unwrap();
905 let agent = &BUNDLED_AGENTS[0];
906 install_bundled(agent, dir.path()).unwrap();
907 let manifest = dir.path().join(agent.name).join("agent.leviath");
908 let mut blueprint =
909 leviath_core::manifest::parse_manifest(&std::fs::read_to_string(&manifest).unwrap())
910 .unwrap();
911
912 assert_eq!(
914 stale_install_note(&manifest, &blueprint, Some(dir.path())),
915 None
916 );
917
918 blueprint.version = "0.0.1".to_string();
919 let note = stale_install_note(&manifest, &blueprint, Some(dir.path()))
920 .expect("a behind install is named");
921 assert!(note.contains("0.0.1"), "{note}");
922 assert!(note.contains(agent.version), "{note}");
923 assert!(note.contains("lev setup"), "{note}");
924 }
925
926 #[test]
930 fn a_blueprint_that_is_not_the_installed_copy_is_left_alone() {
931 let dir = tempfile::tempdir().unwrap();
932 let agent = &BUNDLED_AGENTS[0];
933 install_bundled(agent, dir.path()).unwrap();
934 let manifest = dir.path().join(agent.name).join("agent.leviath");
935 let mut blueprint =
936 leviath_core::manifest::parse_manifest(&std::fs::read_to_string(&manifest).unwrap())
937 .unwrap();
938 blueprint.version = "0.0.1".to_string();
939
940 let elsewhere = tempfile::tempdir().unwrap();
942 let copy = elsewhere.path().join(agent.name).join("agent.leviath");
943 assert_eq!(
944 stale_install_note(©, &blueprint, Some(dir.path())),
945 None,
946 "not the installed copy"
947 );
948
949 assert_eq!(stale_install_note(&manifest, &blueprint, None), None);
951
952 blueprint.name = "not-a-bundled-agent".to_string();
954 assert_eq!(
955 stale_install_note(
956 &dir.path().join("not-a-bundled-agent").join("agent.leviath"),
957 &blueprint,
958 Some(dir.path())
959 ),
960 None
961 );
962 }
963
964 #[test]
967 fn install_writes_every_file_including_nested_ones() {
968 let dir = tempfile::tempdir().unwrap();
969 for agent in BUNDLED_AGENTS {
974 install_bundled(agent, dir.path()).unwrap();
975 for (rel, contents) in agent.files {
976 let written = std::fs::read_to_string(dir.path().join(agent.name).join(rel));
977 assert!(written.is_ok(), "{}/{rel} was not written", agent.name);
978 assert_eq!(written.expect("asserted Ok just above"), *contents);
979 }
980 }
981 assert!(
982 BUNDLED_AGENTS
983 .iter()
984 .any(|a| a.files.iter().any(|(rel, _)| rel.contains('/'))),
985 "no bundled blueprint has a nested file, so install's mkdir path is untested"
986 );
987 }
988
989 #[test]
990 fn install_replaces_an_existing_tree_and_drops_stale_files() {
991 let dir = tempfile::tempdir().unwrap();
992 let agent = &BUNDLED_AGENTS[0];
993 install_bundled(agent, dir.path()).unwrap();
994 let stale = dir
995 .path()
996 .join(agent.name)
997 .join("stale-from-an-older-version");
998 std::fs::write(&stale, "leftover").unwrap();
999
1000 install_bundled(agent, dir.path()).unwrap();
1001
1002 assert!(
1003 !stale.exists(),
1004 "a reinstall must not leave files from the previous version behind"
1005 );
1006 assert!(dir.path().join(agent.name).join("agent.leviath").exists());
1007 }
1008
1009 #[test]
1010 fn install_surfaces_a_directory_creation_failure() {
1011 let dir = tempfile::tempdir().unwrap();
1014 let blocked = dir.path().join("not-a-dir");
1015 std::fs::write(&blocked, "").unwrap();
1016
1017 let result = install_bundled(&BUNDLED_AGENTS[0], &blocked);
1018
1019 assert!(result.is_err());
1020 }
1021
1022 #[test]
1023 fn install_surfaces_a_file_write_failure() {
1024 let agent = BundledAgent {
1031 name: "collides-with-its-own-directory",
1032 version: "0.0.1",
1033 files: &[("tools/a.rhai", "nested first"), ("tools", "then the dir")],
1034 };
1035 let dir = tempfile::tempdir().unwrap();
1036
1037 let result = install_bundled(&agent, dir.path());
1038
1039 assert!(result.is_err());
1040 }
1041
1042 #[test]
1043 fn install_surfaces_a_remove_failure() {
1044 let dir = tempfile::tempdir().unwrap();
1047 let agent = &BUNDLED_AGENTS[0];
1048 std::fs::write(dir.path().join(agent.name), "").unwrap();
1049
1050 let result = install_bundled(agent, dir.path());
1051
1052 assert!(result.is_err());
1053 }
1054}