1use std::collections::BTreeSet;
2
3use colored::Colorize;
4
5use super::{
6 InstallState, Status, TargetOutcome,
7 common::{canonical_target_key, resolve_difflore_binary, write_install_manifest},
8 diagnosis::client_name_for_surface,
9 manifest::{self, ManifestTarget},
10 registry::{self, AGENTS, AgentSpec, BlockKind},
11 snapshot::{collect_agent_statuses, installed_targets_from_agents},
12};
13use crate::style::{self, sym};
14
15fn successful_outcome_names(outcomes: &[TargetOutcome]) -> Vec<&'static str> {
16 outcomes
17 .iter()
18 .filter(|o| matches!(o.status, Status::Installed | Status::Updated))
19 .map(|o| o.name)
20 .collect()
21}
22
23pub(super) fn failed_outcome_names(outcomes: &[TargetOutcome]) -> Vec<&'static str> {
24 outcomes
25 .iter()
26 .filter(|o| matches!(o.status, Status::Error(_)))
27 .map(|o| o.name)
28 .collect()
29}
30
31pub(super) fn outcome_client_names(outcomes: &[TargetOutcome]) -> Vec<String> {
32 outcomes
33 .iter()
34 .filter(|o| matches!(o.status, Status::Installed | Status::Updated))
35 .map(|o| client_name_for_surface(o.name).to_owned())
36 .collect::<BTreeSet<_>>()
37 .into_iter()
38 .collect()
39}
40
41fn installed_surface_keys(bin: &str) -> BTreeSet<String> {
42 collect_agent_statuses(bin)
43 .into_iter()
44 .filter(|agent| matches!(agent.state, InstallState::Installed))
45 .map(|agent| canonical_target_key(agent.name))
46 .collect()
47}
48
49pub(super) fn outcome_already_installed(
50 outcome: &TargetOutcome,
51 installed_surfaces: &BTreeSet<String>,
52) -> bool {
53 installed_surfaces.contains(&canonical_target_key(outcome.name))
54 || skipped_because_already_installed(&outcome.status)
55}
56
57fn skipped_because_already_installed(status: &Status) -> bool {
58 matches!(status, Status::Skipped(reason) if reason.contains("already installed"))
59}
60
61pub(super) const fn install_outcome_verb(
62 status: &Status,
63 dry_run: bool,
64 already_installed: bool,
65) -> &'static str {
66 match status {
67 Status::Installed | Status::Updated | Status::Skipped(_)
68 if dry_run && already_installed =>
69 {
70 "already installed"
71 }
72 Status::Installed if dry_run => "would install",
73 Status::Updated if dry_run => "would update",
74 Status::Installed => "installed",
75 Status::Updated => "updated",
76 Status::Removed if dry_run => "would remove",
78 Status::Removed => "removed",
79 Status::Skipped(_) => "skipped",
80 Status::Error(_) => "error",
81 }
82}
83
84pub(super) const fn should_write_canonical_record(
85 dry_run: bool,
86 installed: &[&str],
87 failed: &[&str],
88) -> bool {
89 !dry_run && !installed.is_empty() && failed.is_empty()
90}
91
92pub fn install_all(dry_run: bool) {
95 let cli_bin = match resolve_difflore_binary() {
96 Ok(b) => b,
97 Err(e) => crate::commands::util::exit_err(&e),
98 };
99 let mcp_bin = cli_bin.clone();
100
101 let install_message = if dry_run {
102 "Checking DiffLore MCP install plan for every detected agent"
103 } else {
104 "Installing DiffLore MCP server to every detected agent"
105 };
106 let dry_tag = if dry_run {
107 format!(" {}", style::amber("(dry-run; no changes)"))
108 } else {
109 String::new()
110 };
111 println!(
112 "{} {}{dry_tag}",
113 style::emerald(sym::TIP),
114 style::pewter(install_message),
115 );
116 println!(
117 " {} {} {}",
118 style::pewter("mcp command:"),
119 style::emerald("difflore"),
120 style::emerald("mcp-server")
121 );
122 println!();
123
124 let outcomes = install_all_targets(&mcp_bin, &cli_bin, dry_run);
125 let installed_surfaces = if dry_run {
126 installed_surface_keys(&mcp_bin)
127 } else {
128 BTreeSet::new()
129 };
130 print_install_outcomes(&outcomes, dry_run, &installed_surfaces, &mcp_bin);
131
132 let installed = successful_outcome_names(&outcomes);
133 let has_detected_or_planned = if dry_run {
134 outcomes.iter().any(|o| {
135 matches!(o.status, Status::Installed | Status::Updated)
136 || outcome_already_installed(o, &installed_surfaces)
137 })
138 } else {
139 !installed.is_empty()
140 };
141
142 let failed = failed_outcome_names(&outcomes);
143 if should_write_canonical_record(dry_run, &installed, &failed) {
144 let agents = collect_agent_statuses(&mcp_bin);
145 let current_installed = installed_targets_from_agents(&agents);
146 let record_targets = if current_installed.is_empty() {
147 installed.as_slice()
148 } else {
149 current_installed.as_slice()
150 };
151 let prior = manifest::load();
157 let manifest_targets =
158 manifest::build_targets(record_targets, &mcp_bin, &cli_bin, prior.as_ref());
159 if let Err(e) = write_install_manifest(&mcp_bin, manifest_targets) {
160 eprintln!(
161 "{} failed to write canonical record: {e}",
162 style::warn("warning:")
163 );
164 }
165 } else if !dry_run && !installed.is_empty() && !failed.is_empty() {
166 eprintln!(
167 "{} partial MCP install: canonical record not updated because {} failed. Run {} after fixing those clients.",
168 style::warn("warning:"),
169 failed.join(", "),
170 style::cmd("difflore agents status"),
171 );
172 }
173
174 if !has_detected_or_planned {
175 println!();
176 println!(
177 "{} no agents were detected. Install a supported agent (Claude Code, Codex, Cursor, Gemini, Copilot CLI, Antigravity, Goose, Crush, Roo Code, Warp) and re-run.",
178 style::warn("!")
179 );
180 return;
181 }
182
183 print_post_install_help(dry_run, &outcomes);
184}
185
186fn install_all_targets(mcp_bin: &str, cli_bin: &str, dry_run: bool) -> Vec<TargetOutcome> {
191 AGENTS
192 .iter()
193 .filter(|spec| spec.name != "Claude Code hooks")
197 .map(|spec| registry::install(spec, mcp_bin, cli_bin, dry_run))
198 .collect()
199}
200
201fn print_install_outcomes(
202 outcomes: &[TargetOutcome],
203 dry_run: bool,
204 installed_surfaces: &BTreeSet<String>,
205 mcp_bin: &str,
206) {
207 let mut skipped_summary: Vec<&str> = Vec::new();
208 for o in outcomes {
209 let already_installed = dry_run && outcome_already_installed(o, installed_surfaces);
210 if dry_run && !already_installed && matches!(o.status, Status::Skipped(_)) {
211 skipped_summary.push(o.name);
212 continue;
213 }
214 let plain_verb = install_outcome_verb(&o.status, dry_run, already_installed);
215 let (mark, verb) = match &o.status {
216 Status::Installed | Status::Updated | Status::Skipped(_) if already_installed => {
217 (style::ok(sym::OK), style::emerald(plain_verb))
218 }
219 Status::Installed | Status::Updated if dry_run => {
220 (style::amber("·"), style::amber(plain_verb))
221 }
222 Status::Installed | Status::Updated | Status::Removed => {
225 (style::ok(sym::OK), style::emerald(plain_verb))
226 }
227 Status::Skipped(_) => (style::pewter("·"), style::pewter(plain_verb)),
228 Status::Error(_) => (style::err(sym::ERR), style::danger(plain_verb)),
229 };
230 println!(" {mark} {:<14} {verb}", o.name.bold());
231 let sub = match &o.status {
232 Status::Skipped(r) | Status::Error(r) => r.as_str(),
233 _ => o.detail.as_str(),
234 };
235 if !sub.is_empty() {
236 println!(
237 " {}",
238 style::pewter(&public_install_detail(sub, mcp_bin))
239 );
240 }
241 }
242 if !skipped_summary.is_empty() {
243 let (hooks, agents): (Vec<_>, Vec<_>) = skipped_summary
244 .into_iter()
245 .partition(|name| name.to_ascii_lowercase().contains("hooks"));
246 let mut agents = agents;
247 if hooks.contains(&"Windsurf hooks") && !agents.contains(&"Windsurf") {
248 agents.push("Windsurf");
249 }
250 if !agents.is_empty() {
251 println!(
252 " {} {}",
253 style::pewter("·"),
254 style::pewter(&format!(
255 "agents skipped/not detected: {}",
256 agents.join(", ")
257 ))
258 );
259 }
260 if !hooks.is_empty() {
261 println!(
262 " {} {}",
263 style::pewter("·"),
264 style::pewter(&format!("hooks skipped/not detected: {}", hooks.join(", ")))
265 );
266 }
267 }
268}
269
270fn public_install_detail(detail: &str, mcp_bin: &str) -> String {
271 let mut out = detail.replace(mcp_bin, "difflore");
272 let normalized = out.replace('\\', "/");
273 for (suffix, label) in [
274 ("/.github/copilot/mcp.json", "~/.github/copilot/mcp.json"),
275 (
276 "/.gemini/antigravity/mcp_config.json",
277 "~/.gemini/antigravity/mcp_config.json",
278 ),
279 ("/.config/crush/mcp.json", "~/.config/crush/mcp.json"),
280 ("/.roo/mcp.json", "./.roo/mcp.json"),
281 ("/.warp/mcp.json", "~/.warp/mcp.json"),
282 ] {
283 if normalized.ends_with(suffix) {
284 return label.to_owned();
285 }
286 if let Some(pos) = normalized.find(suffix) {
287 let before = &normalized[..pos];
288 let after = &normalized[pos + suffix.len()..];
289 out = format!(
290 "{}{}{}",
291 &normalized[..before.rfind(' ').map_or(0, |i| i + 1)],
292 label,
293 after
294 );
295 }
296 }
297 out
298}
299
300static MCP_TOOLS_HELP: &[(&str, &str)] = &[
301 (
302 "search_rules",
303 " — compact rule index (~80 tok/result), ids only",
304 ),
305 (
306 "get_rules",
307 " — fetch full rule bodies by ids (batch after search_rules)",
308 ),
309 (
310 "get_past_verdicts",
311 " — recall past PR review decisions",
312 ),
313 (
314 "remember_rule",
315 " — save \"remember this rule\" moments mid-chat",
316 ),
317];
318
319fn print_post_install_help(dry_run: bool, outcomes: &[TargetOutcome]) {
320 let clients = outcome_client_names(outcomes);
321 let restart_targets = if clients.is_empty() {
322 "any agent you use with DiffLore".to_owned()
323 } else {
324 clients.join(", ")
325 };
326 println!();
327 if dry_run {
328 println!(
329 "{} dry-run only: no MCP config or hooks were changed.",
330 style::emerald(sym::TIP)
331 );
332 println!(
333 " {} apply with {} when the plan looks right.",
334 style::pewter(sym::BULLET),
335 style::cmd("difflore agents install"),
336 );
337 } else {
338 println!(
339 "{} restart/reload {} so they pick up the new DiffLore memory server.",
340 style::emerald(sym::TIP),
341 if clients.is_empty() {
342 "Claude/Codex/Cursor/etc.".to_owned()
343 } else {
344 clients.join(", ")
345 }
346 );
347 }
348 println!(
349 " {} installed once; use {} later to refresh team review memory.",
350 style::pewter(sym::BULLET),
351 style::cmd("difflore cloud sync"),
352 );
353 println!();
354 println!(
355 "{} review-memory tools your local agent can now call:",
356 style::emerald(sym::TIP)
357 );
358 for (name, desc) in MCP_TOOLS_HELP {
359 println!(" • {}{desc}", style::ident(name));
360 }
361 println!();
362 println!(
363 " {} For large rule libraries prefer search_rules → get_rules (~10× fewer tokens on large libraries).",
364 style::pewter("ℹ")
365 );
366 println!();
367 println!("{} verification loop:", style::emerald(sym::TIP));
368 println!(
369 " {} run {} after applying to verify config, runtime startup, tool listing, and the built-in search_rules self-check.",
370 style::pewter(sym::BULLET),
371 style::cmd("difflore agents status"),
372 );
373 println!(
374 " {} restart/reload: {}.",
375 style::pewter(sym::BULLET),
376 style::ident(&restart_targets),
377 );
378 println!(
379 " {} in one restarted agent, call {} to verify DiffLore MCP can recall review memory.",
380 style::pewter(sym::BULLET),
381 style::cmd("search_rules"),
382 );
383}
384
385#[derive(Debug, Clone, PartialEq, Eq)]
391pub(super) enum UpdateAction {
392 UpToDate,
394 Upgrade { from: u32, to: u32 },
397 Adopt,
400 SkippedLocalEdits,
403 ForceOverwrite,
405 Gone,
407 ReissueCli { from: u32, to: u32 },
409 UpToDateExternal,
411}
412
413pub(super) fn plan_update_target(
418 is_external: bool,
419 recorded_hash: Option<&str>,
420 recorded_version: u32,
421 current_version: u32,
422 on_disk_hash: Option<&str>,
423 standard_render_hash: Option<&str>,
424) -> UpdateAction {
425 if is_external {
426 return if recorded_version < current_version {
427 UpdateAction::ReissueCli {
428 from: recorded_version,
429 to: current_version,
430 }
431 } else {
432 UpdateAction::UpToDateExternal
433 };
434 }
435
436 let Some(on_disk) = on_disk_hash else {
438 return UpdateAction::Gone;
439 };
440
441 match recorded_hash {
442 Some(recorded) if recorded == on_disk => {
444 if recorded_version < current_version {
446 UpdateAction::Upgrade {
447 from: recorded_version,
448 to: current_version,
449 }
450 } else {
451 UpdateAction::UpToDate
452 }
453 }
454 Some(_) => UpdateAction::SkippedLocalEdits,
455 None => {
458 if standard_render_hash == Some(on_disk) {
459 UpdateAction::Adopt
460 } else {
461 UpdateAction::SkippedLocalEdits
462 }
463 }
464 }
465}
466
467pub fn update_all(dry_run: bool, force: bool) {
468 let cli_bin = match resolve_difflore_binary() {
469 Ok(b) => b,
470 Err(e) => crate::commands::util::exit_err(&e),
471 };
472 let mcp_bin = cli_bin.clone();
473
474 let Some(mut manifest) = manifest::load() else {
475 println!(
476 "{} no DiffLore install manifest (~/.difflore/mcp.json) found.",
477 style::warn("!")
478 );
479 println!(
480 " {} run {} first to wire DiffLore into your agents.",
481 style::pewter(sym::BULLET),
482 style::cmd("difflore agents install"),
483 );
484 return;
485 };
486
487 if manifest.targets.is_empty() && !manifest.installed_targets.is_empty() {
492 manifest.targets = manifest::v1_provisional_targets(&manifest.installed_targets);
493 }
494
495 let message = if dry_run {
496 "Checking DiffLore block upgrade plan for every recorded target"
497 } else {
498 "Upgrading DiffLore blocks that are unchanged since DiffLore wrote them"
499 };
500 let dry_tag = if dry_run {
501 format!(" {}", style::amber("(dry-run; no changes)"))
502 } else {
503 String::new()
504 };
505 println!(
506 "{} {}{dry_tag}",
507 style::emerald(sym::TIP),
508 style::pewter(message),
509 );
510 if force {
511 println!(
512 " {} {}",
513 style::amber("·"),
514 style::amber("--force: locally-edited blocks will be overwritten"),
515 );
516 }
517 println!();
518
519 let mut any_changed = false;
520 let mut any_gone = false;
521 let mut any_skipped = false;
522 for idx in 0..manifest.targets.len() {
524 let target_name = manifest.targets[idx].name.clone();
525 let Some(spec) = registry::find_spec(&target_name) else {
526 continue;
529 };
530 let block_kind = registry::block_kind_of(spec);
531 let current_version = block_kind.current_version();
532 let is_external = block_kind == BlockKind::ExternalCli;
533
534 let on_disk_hash = if is_external {
535 None
536 } else {
537 manifest::on_disk_block_hash(spec, &cli_bin)
538 };
539 let standard_render_hash = if is_external {
540 None
541 } else {
542 manifest::render_block_hash(spec, &mcp_bin, &cli_bin)
543 };
544
545 let recorded_version = manifest.targets[idx].block_version;
546 let recorded_hash = manifest.targets[idx].block_hash.clone();
547
548 let mut action = plan_update_target(
549 is_external,
550 recorded_hash.as_deref(),
551 recorded_version,
552 current_version,
553 on_disk_hash.as_deref(),
554 standard_render_hash.as_deref(),
555 );
556 if force && matches!(action, UpdateAction::SkippedLocalEdits) {
560 action = UpdateAction::ForceOverwrite;
561 }
562
563 let changed = apply_update_action(
564 &action,
565 spec,
566 &mut manifest.targets[idx],
567 &mcp_bin,
568 &cli_bin,
569 current_version,
570 standard_render_hash.as_deref(),
571 on_disk_hash.as_deref(),
572 dry_run,
573 );
574 any_changed |= changed;
575 any_gone |= matches!(action, UpdateAction::Gone);
576 any_skipped |= matches!(action, UpdateAction::SkippedLocalEdits);
577 }
578
579 if !dry_run && any_changed {
582 manifest.manifest_version = manifest::MANIFEST_VERSION;
584 if let Err(e) = manifest::save(&manifest) {
585 eprintln!(
586 "{} failed to update install manifest: {e}",
587 style::warn("warning:")
588 );
589 }
590 }
591
592 print_update_footer(dry_run, force, any_changed, any_gone, any_skipped);
593}
594
595#[allow(clippy::too_many_arguments)]
599fn apply_update_action(
602 action: &UpdateAction,
603 spec: &'static AgentSpec,
604 target: &mut ManifestTarget,
605 mcp_bin: &str,
606 cli_bin: &str,
607 current_version: u32,
608 standard_render_hash: Option<&str>,
609 on_disk_hash: Option<&str>,
610 dry_run: bool,
611) -> bool {
612 let now = manifest::now_rfc3339();
613 match action {
614 UpdateAction::UpToDate => {
615 report_update_line(
616 spec.name,
617 style::pewter("·"),
618 style::pewter("up to date"),
619 "",
620 );
621 false
622 }
623 UpdateAction::UpToDateExternal => {
624 report_update_line(
625 spec.name,
626 style::pewter("·"),
627 style::pewter("up to date"),
628 &format!(
629 "managed by {} (no local block to upgrade)",
630 external_cli_label(spec)
631 ),
632 );
633 false
634 }
635 UpdateAction::Adopt => {
636 let verb = if dry_run { "would adopt" } else { "adopted" };
637 report_update_line(
638 spec.name,
639 style::ok(sym::OK),
640 style::emerald(verb),
641 "recognised the on-disk block as DiffLore's standard render",
642 );
643 if dry_run {
644 return false;
645 }
646 target.block_hash = standard_render_hash.or(on_disk_hash).map(ToOwned::to_owned);
647 target.block_version = current_version;
648 target.updated_at = now;
649 true
650 }
651 UpdateAction::Upgrade { from, to } => {
652 let verb = if dry_run {
653 format!("would upgrade v{from}→v{to}")
654 } else {
655 format!("upgraded v{from}→v{to}")
656 };
657 report_update_line(spec.name, style::ok(sym::OK), style::emerald(&verb), "");
658 if dry_run {
659 return false;
660 }
661 let render_spec = effective_install_spec(spec);
666 let outcome = registry::install(render_spec, mcp_bin, cli_bin, false);
667 if let Status::Error(e) = &outcome.status {
668 eprintln!(" {}", style::danger(e));
669 return false;
670 }
671 target.block_hash = standard_render_hash
673 .map(ToOwned::to_owned)
674 .or_else(|| manifest::on_disk_block_hash(spec, cli_bin));
675 target.block_version = current_version;
676 target.updated_at = now;
677 true
678 }
679 UpdateAction::ReissueCli { from, to } => {
680 let verb = if dry_run {
681 format!(
682 "would re-issue {} add (v{from}→v{to})",
683 external_cli_label(spec)
684 )
685 } else {
686 format!("re-issued {} add (v{from}→v{to})", external_cli_label(spec))
687 };
688 report_update_line(spec.name, style::ok(sym::OK), style::emerald(&verb), "");
689 if dry_run {
690 return false;
691 }
692 let outcome = registry::install(spec, mcp_bin, cli_bin, false);
694 if let Status::Error(e) = &outcome.status {
695 eprintln!(" {}", style::danger(e));
696 return false;
697 }
698 target.block_version = current_version;
699 target.updated_at = now;
700 true
701 }
702 UpdateAction::Gone => {
703 report_update_line(
704 spec.name,
705 style::warn("!"),
706 style::amber("gone"),
707 "no DiffLore block on disk — reinstall with `difflore agents install`",
708 );
709 false
710 }
711 UpdateAction::SkippedLocalEdits => {
712 report_update_line(
713 spec.name,
714 style::pewter("·"),
715 style::pewter("skipped: local edits since DiffLore wrote it"),
716 &format!(
717 "{} — re-run with --force to overwrite",
718 target.config_path.as_deref().map_or_else(
719 || spec.display.to_owned(),
720 |p| public_install_detail(p, mcp_bin)
721 ),
722 ),
723 );
724 false
725 }
726 UpdateAction::ForceOverwrite => {
727 let verb = if dry_run {
728 "would overwrite (--force)"
729 } else {
730 "overwrote (--force)"
731 };
732 report_update_line(
733 spec.name,
734 style::ok(sym::OK),
735 style::amber(verb),
736 "replaced the locally-edited block with DiffLore's current render",
737 );
738 if dry_run {
739 return false;
740 }
741 let render_spec = effective_install_spec(spec);
742 let outcome = registry::install(render_spec, mcp_bin, cli_bin, false);
743 if let Status::Error(e) = &outcome.status {
744 eprintln!(" {}", style::danger(e));
745 return false;
746 }
747 target.block_hash = standard_render_hash
748 .map(ToOwned::to_owned)
749 .or_else(|| manifest::on_disk_block_hash(spec, cli_bin));
750 target.block_version = current_version;
751 target.updated_at = now;
752 true
753 }
754 }
755}
756
757fn effective_install_spec(spec: &'static AgentSpec) -> &'static AgentSpec {
762 if spec.name == "Claude Code hooks"
763 && let Some(claude) = registry::find_spec("Claude Code")
764 {
765 return claude;
766 }
767 spec
768}
769
770fn external_cli_label(spec: &AgentSpec) -> &'static str {
771 match spec.name {
773 "Codex" => "codex",
774 _ => "claude",
775 }
776}
777
778#[allow(clippy::needless_pass_by_value)]
782fn report_update_line(
783 name: &str,
784 mark: colored::ColoredString,
785 verb: colored::ColoredString,
786 sub: &str,
787) {
788 println!(" {mark} {:<14} {verb}", name.bold());
789 if !sub.is_empty() {
790 println!(" {}", style::pewter(sub));
791 }
792}
793
794fn print_update_footer(dry_run: bool, force: bool, changed: bool, gone: bool, skipped: bool) {
795 println!();
796 if dry_run {
797 println!(
798 "{} dry-run only: no blocks were re-rendered and the manifest was not touched.",
799 style::emerald(sym::TIP)
800 );
801 println!(
802 " {} apply with {} when the plan looks right.",
803 style::pewter(sym::BULLET),
804 style::cmd("difflore agents update"),
805 );
806 return;
807 }
808 if changed {
809 println!(
810 "{} restart/reload the affected agents so they pick up the refreshed DiffLore blocks.",
811 style::emerald(sym::TIP),
812 );
813 } else {
814 println!(
815 "{} everything is already up to date — no blocks needed re-rendering.",
816 style::emerald(sym::TIP),
817 );
818 }
819 if skipped && !force {
820 println!(
821 " {} some blocks were skipped because they had local edits; re-run with {} to overwrite them.",
822 style::pewter(sym::BULLET),
823 style::cmd("difflore agents update --force"),
824 );
825 }
826 if gone {
827 println!(
828 " {} some recorded targets had no DiffLore block on disk; reinstall with {}.",
829 style::pewter(sym::BULLET),
830 style::cmd("difflore agents install"),
831 );
832 }
833}
834
835#[cfg(test)]
836mod update_tests {
837 use super::*;
838
839 #[test]
840 fn external_cli_reissues_only_on_version_bump() {
841 assert_eq!(
843 plan_update_target(true, None, 1, 2, None, None),
844 UpdateAction::ReissueCli { from: 1, to: 2 }
845 );
846 assert_eq!(
847 plan_update_target(true, None, 2, 2, None, None),
848 UpdateAction::UpToDateExternal
849 );
850 }
851
852 #[test]
853 fn unchanged_block_behind_version_upgrades() {
854 assert_eq!(
856 plan_update_target(
857 false,
858 Some("sha256:aa"),
859 1,
860 2,
861 Some("sha256:aa"),
862 Some("sha256:bb")
863 ),
864 UpdateAction::Upgrade { from: 1, to: 2 }
865 );
866 }
867
868 #[test]
869 fn unchanged_block_at_current_version_is_up_to_date() {
870 assert_eq!(
871 plan_update_target(
872 false,
873 Some("sha256:aa"),
874 1,
875 1,
876 Some("sha256:aa"),
877 Some("sha256:aa")
878 ),
879 UpdateAction::UpToDate
880 );
881 }
882
883 #[test]
884 fn edited_block_is_skipped_not_clobbered() {
885 assert_eq!(
887 plan_update_target(
888 false,
889 Some("sha256:aa"),
890 1,
891 2,
892 Some("sha256:zz"),
893 Some("sha256:bb")
894 ),
895 UpdateAction::SkippedLocalEdits
896 );
897 }
898
899 #[test]
900 fn missing_on_disk_block_is_gone() {
901 assert_eq!(
902 plan_update_target(false, Some("sha256:aa"), 1, 1, None, Some("sha256:bb")),
903 UpdateAction::Gone
904 );
905 }
906
907 #[test]
908 fn v1_record_adopts_standard_render_else_skips() {
909 assert_eq!(
911 plan_update_target(false, None, 1, 1, Some("sha256:std"), Some("sha256:std")),
912 UpdateAction::Adopt
913 );
914 assert_eq!(
916 plan_update_target(false, None, 1, 1, Some("sha256:edited"), Some("sha256:std")),
917 UpdateAction::SkippedLocalEdits
918 );
919 }
920}