1use std::io;
2
3use callisto_model::{
4 ComposePrBodyReport, InitReport, PublishAttemptResult, PublishPlan, PublishReport, SnapshotReport, StatusReport,
5 TagReport, ValidateReport, VersionReport,
6};
7
8pub mod attribution;
9pub mod diff;
10
11pub fn render_diagnostics<W: io::Write>(diagnostics: &[callisto_model::Diagnostic], w: &mut W) -> io::Result<()> {
12 if !diagnostics.is_empty() {
13 writeln!(w, "\nDiagnostics:")?;
14 for d in diagnostics {
15 writeln!(w, " [{:?}] {}", d.severity, d.message)?;
16 }
17 }
18 Ok(())
19}
20
21pub fn render_status<W: io::Write>(report: &StatusReport, w: &mut W) -> io::Result<()> {
22 writeln!(w, "Status (schema v{}):", report.schema_version)?;
23 for pkg in &report.packages {
24 let severity = pkg
25 .pending_severity
26 .map(|s| s.to_string())
27 .unwrap_or_else(|| "none".to_string());
28 writeln!(
29 w,
30 " {} {} (pending: {})",
31 pkg.package.display_name(),
32 pkg.current_version.raw(),
33 severity
34 )?;
35 }
36 render_diagnostics(&report.diagnostics, w)
37}
38
39pub fn render_version<W: io::Write>(report: &VersionReport, w: &mut W) -> io::Result<()> {
40 writeln!(w, "Version Plan (schema v{}):", report.schema_version)?;
41 for bump in &report.bumps {
42 writeln!(
43 w,
44 " {} {} → {}",
45 bump.package.display_name(),
46 bump.from.raw(),
47 bump.to.raw()
48 )?;
49 }
50 render_diagnostics(&report.diagnostics, w)
51}
52
53pub fn render_publish<W: io::Write>(report: &PublishPlan, w: &mut W) -> io::Result<()> {
54 let total_packages = report.rust_crates.len()
55 + report.npm_platform_packages.len()
56 + report.npm_main_packages.len()
57 + report.pypi_packages.len();
58 writeln!(w, "Publish Plan (schema v{}):", report.schema_version)?;
59 for rel in &report.releases {
60 writeln!(w, " Tag: {} (sha: {})", rel.tag_name, rel.sha.as_str())?;
61 }
62 if total_packages == 0 && report.releases.is_empty() {
63 writeln!(w, " No packages to publish.")?;
64 render_diagnostics(&report.diagnostics, w)?;
65 return Ok(());
66 }
67 if total_packages == 0 {
68 render_diagnostics(&report.diagnostics, w)?;
69 return Ok(());
70 }
71 if !report.rust_crates.is_empty() {
72 writeln!(w, " Crates ({}):", report.rust_crates.len())?;
73 for pkg in &report.rust_crates {
74 writeln!(w, " {} {}", pkg.name, pkg.version.raw())?;
75 }
76 }
77 if !report.npm_main_packages.is_empty() {
78 writeln!(w, " npm packages ({}):", report.npm_main_packages.len())?;
79 for pkg in &report.npm_main_packages {
80 writeln!(w, " {} {}", pkg.name, pkg.version.raw())?;
81 }
82 }
83 if !report.npm_platform_packages.is_empty() {
84 writeln!(w, " npm platform packages ({}):", report.npm_platform_packages.len())?;
85 for pkg in &report.npm_platform_packages {
86 writeln!(w, " {} {}", pkg.name, pkg.version.raw())?;
87 }
88 }
89 if !report.pypi_packages.is_empty() {
90 writeln!(w, " PyPI packages ({}):", report.pypi_packages.len())?;
91 for pkg in &report.pypi_packages {
92 writeln!(w, " {} {}", pkg.name, pkg.version.raw())?;
93 }
94 }
95 render_diagnostics(&report.diagnostics, w)?;
96 Ok(())
97}
98
99pub fn render_publish_report<W: io::Write>(report: &PublishReport, w: &mut W) -> io::Result<()> {
100 writeln!(w, "Publish Report (schema v{}):", report.schema_version)?;
101 for attempt in &report.attempts {
102 let status = match &attempt.result {
103 PublishAttemptResult::Published => "published".to_string(),
104 PublishAttemptResult::AlreadyPublished => "already published".to_string(),
105 PublishAttemptResult::Failed { kind, error } => format!("FAILED [{kind}]: {error}"),
106 };
107 writeln!(
108 w,
109 " {} {} — {}",
110 attempt.package.display_name(),
111 attempt.version.raw(),
112 status
113 )?;
114 }
115 render_diagnostics(&report.diagnostics, w)
116}
117
118pub fn render_snapshot<W: io::Write>(report: &SnapshotReport, w: &mut W) -> io::Result<()> {
119 writeln!(w, "Snapshot Tag: {}", report.snapshot_tag)?;
120 for bump in &report.bumps {
121 writeln!(
122 w,
123 " {} {} → {}",
124 bump.package.display_name(),
125 bump.from.raw(),
126 bump.to.raw()
127 )?;
128 }
129 Ok(())
130}
131
132pub fn render_validate<W: io::Write>(report: &ValidateReport, w: &mut W) -> io::Result<()> {
133 if report.ok {
134 writeln!(w, "Validation passed.")?;
135 } else {
136 writeln!(w, "Validation failed with diagnostics:")?;
137 for diag in &report.diagnostics {
138 writeln!(w, " [{:?}] {}", diag.severity, diag.message)?;
139 }
140 }
141 Ok(())
142}
143
144pub fn render_tag<W: io::Write>(report: &TagReport, dry_run: bool, w: &mut W) -> io::Result<()> {
145 if dry_run {
146 writeln!(w, "Would create tags:")?;
147 } else {
148 writeln!(w, "Created Tags:")?;
149 }
150 for tag in &report.tags {
151 writeln!(w, " {} ({})", tag.tag_name, tag.sha.as_str())?;
152 }
153 Ok(())
154}
155
156pub fn render_compose_pr_body<W: io::Write>(report: &ComposePrBodyReport, w: &mut W) -> io::Result<()> {
157 write!(w, "{}", report.body)?;
158 Ok(())
159}
160
161pub fn render_init<W: io::Write>(report: &InitReport, w: &mut W) -> io::Result<()> {
162 if report.initialized {
163 writeln!(
164 w,
165 "Initialized callisto configuration at {}",
166 report.config_path.display()
167 )?;
168 } else if report.diff.new_ecosystems.is_empty() {
169 writeln!(
170 w,
171 "callisto configuration at {} is up to date; nothing to reconcile",
172 report.config_path.display()
173 )?;
174 } else {
175 let names: Vec<&str> = report.diff.new_ecosystems.iter().map(|e| e.prefix()).collect();
176 if report.diff.applied {
177 writeln!(
178 w,
179 "Reconciled {}: added newly-detected ecosystem(s) {}",
180 report.config_path.display(),
181 names.join(", ")
182 )?;
183 } else {
184 writeln!(
185 w,
186 "Drift detected in {}: newly-detected ecosystem(s) {} — re-run with --yes to apply",
187 report.config_path.display(),
188 names.join(", ")
189 )?;
190 }
191 }
192 Ok(())
193}
194
195pub fn render_matrix<W: io::Write>(report: &callisto_model::MatrixReport, w: &mut W) -> io::Result<()> {
196 writeln!(w, "Matrix (schema v{}):", report.schema_version)?;
197
198 if report.platform_targets.is_empty() && report.runtime_versions.is_empty() {
199 writeln!(w, " (no platform targets or runtime-version constraints declared)")?;
200 }
201
202 for (pkg, group) in &report.platform_targets {
203 writeln!(w, " {pkg} [{:?} <- {}]:", group.kind, group.source)?;
204 for t in &group.targets {
205 writeln!(
206 w,
207 " {:<32} abi={:<8} runner={:<14} cross={:<5} artifact={}",
208 t.triple,
209 t.abi.as_deref().unwrap_or("-"),
210 t.host_runner,
211 t.use_cross,
212 t.artifact_name
213 )?;
214 }
215 }
216
217 for (pkg, entries) in &report.runtime_versions {
218 for e in entries {
219 writeln!(w, " {pkg} [{:?}] {} = {}", e.ecosystem, e.field, e.range)?;
220 }
221 }
222
223 render_diagnostics(&report.diagnostics, w)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use callisto_model::{
230 BumpRecord, CreatedTag, Ecosystem, PackageId, PublishAttempt, ReleaseTrigger, Severity, StatusPackageRecord,
231 Version, VersionGrammar,
232 };
233
234 fn v1() -> Version {
235 Version::parse("1.0.0", VersionGrammar::SemVer).unwrap()
236 }
237
238 fn pkg(name: &str) -> PackageId {
239 PackageId::Prefixed {
240 ecosystem: Ecosystem::Cargo,
241 name: name.to_string(),
242 }
243 }
244
245 fn mixed_report() -> PublishReport {
246 PublishReport {
247 schema_version: callisto_model::SCHEMA_VERSION,
248 attempts: vec![
249 PublishAttempt {
250 package: pkg("crate-a"),
251 version: v1(),
252 result: PublishAttemptResult::Published,
253 },
254 PublishAttempt {
255 package: pkg("crate-b"),
256 version: v1(),
257 result: PublishAttemptResult::AlreadyPublished,
258 },
259 PublishAttempt {
260 package: pkg("crate-c"),
261 version: v1(),
262 result: PublishAttemptResult::Failed {
263 kind: "authFailed".to_string(),
264 error: "auth failed: bad token".to_string(),
265 },
266 },
267 ],
268 diagnostics: vec![],
269 }
270 }
271
272 fn status_pkg(name: &str, severity: Option<Severity>, changesets: Vec<&str>) -> StatusPackageRecord {
273 StatusPackageRecord {
274 package: pkg(name),
275 current_version: v1(),
276 last_tag: None,
277 last_released_version: None,
278 pending_severity: severity,
279 changed_since_last_tag: false,
280 release_trigger: ReleaseTrigger::Changeset,
281 pending_changesets: changesets.into_iter().map(|s| s.to_string()).collect(),
282 }
283 }
284
285 #[test]
287 fn render_status_no_some_wrapper_in_output() {
288 let report = StatusReport {
289 schema_version: callisto_model::SCHEMA_VERSION,
290 has_changesets: true,
291 packages: vec![status_pkg("crate-a", Some(Severity::Minor), vec!["cs-001"])],
292 diagnostics: vec![],
293 };
294 let mut out = Vec::new();
295 render_status(&report, &mut out).unwrap();
296 let text = String::from_utf8(out).unwrap();
297 assert!(
298 !text.contains("Some("),
299 "render_status output must not contain 'Some('; got: {text}"
300 );
301 assert!(
302 text.contains("minor"),
303 "render_status output should contain severity 'minor'; got: {text}"
304 );
305 }
306
307 fn full_plan() -> PublishPlan {
308 use callisto_model::{
309 CratePublish, NpmMainPublish, NpmPublish, PypiPublish, RegistryKey, Version, SCHEMA_VERSION,
310 };
311 let v = Version::parse("1.0.0", callisto_model::VersionGrammar::SemVer).unwrap();
312 PublishPlan {
313 schema_version: SCHEMA_VERSION,
314 rust_crates: vec![CratePublish {
315 name: "my-crate".to_string(),
316 version: v.clone(),
317 publish_to: RegistryKey(RegistryKey::CRATES_IO.to_string()),
318 registry: None,
319 package_dir: None,
320 }],
321 npm_main_packages: vec![NpmMainPublish {
322 name: "@scope/main-pkg".to_string(),
323 version: v.clone(),
324 publish_to: RegistryKey(RegistryKey::NPM.to_string()),
325 registry: None,
326 tag: None,
327 access: None,
328 depends_on_platforms: vec![],
329 package_dir: std::path::PathBuf::new(),
330 }],
331 npm_platform_packages: vec![NpmPublish {
332 name: "@scope/main-pkg-linux-x64-gnu".to_string(),
333 version: v.clone(),
334 publish_to: RegistryKey(RegistryKey::NPM.to_string()),
335 registry: None,
336 tag: None,
337 access: None,
338 package_dir: std::path::PathBuf::new(),
339 }],
340 pypi_packages: vec![PypiPublish {
341 name: "my-pypi-pkg".to_string(),
342 version: v,
343 publish_to: RegistryKey(RegistryKey::PYPI.to_string()),
344 index: None,
345 package_dir: std::path::PathBuf::new(),
346 }],
347 releases: vec![],
348 diagnostics: vec![],
349 }
350 }
351
352 #[test]
353 fn render_publish_lists_all_four_package_types() {
354 let plan = full_plan();
355 let mut out = Vec::new();
356 render_publish(&plan, &mut out).unwrap();
357 let text = String::from_utf8(out).unwrap();
358 assert!(
359 text.contains("my-crate"),
360 "render_publish must list rust crates; got:\n{text}"
361 );
362 assert!(
363 text.contains("@scope/main-pkg"),
364 "render_publish must list npm main packages; got:\n{text}"
365 );
366 assert!(
367 text.contains("@scope/main-pkg-linux-x64-gnu"),
368 "render_publish must list npm platform packages; got:\n{text}"
369 );
370 assert!(
371 text.contains("my-pypi-pkg"),
372 "render_publish must list pypi packages; got:\n{text}"
373 );
374 }
375
376 #[test]
378 fn render_publish_empty_plan_shows_nothing_to_publish() {
379 let plan = PublishPlan {
380 schema_version: callisto_model::SCHEMA_VERSION,
381 rust_crates: vec![],
382 npm_platform_packages: vec![],
383 npm_main_packages: vec![],
384 pypi_packages: vec![],
385 releases: vec![],
386 diagnostics: vec![],
387 };
388 let mut out = Vec::new();
389 render_publish(&plan, &mut out).unwrap();
390 let text = String::from_utf8(out).unwrap().to_lowercase();
391 assert!(
392 text.contains("no packages") || text.contains("nothing to publish"),
393 "render_publish empty plan must mention 'no packages' or 'nothing to publish'; got: {text}"
394 );
395 }
396
397 #[test]
402 fn render_publish_surfaces_plan_diagnostics() {
403 use callisto_model::{Diagnostic, DiagnosticCode, DiagnosticSeverity};
404
405 let mut plan = PublishPlan {
406 schema_version: callisto_model::SCHEMA_VERSION,
407 rust_crates: vec![],
408 npm_platform_packages: vec![],
409 npm_main_packages: vec![],
410 pypi_packages: vec![],
411 releases: vec![],
412 diagnostics: vec![Diagnostic {
413 code: DiagnosticCode::GitDiscoveryFailed,
414 severity: DiagnosticSeverity::Warning,
415 message: "could not discover git repository: not a git repo".to_string(),
416 package: None,
417 path: None,
418 escalated_by: None,
419 governed_by: None,
420 }],
421 };
422
423 use callisto_model::{CratePublish, RegistryKey};
424
425 plan.rust_crates.push(CratePublish {
427 name: "my-crate".to_string(),
428 version: v1(),
429 publish_to: RegistryKey(RegistryKey::CRATES_IO.to_string()),
430 registry: None,
431 package_dir: None,
432 });
433 let mut out = Vec::new();
434 render_publish(&plan, &mut out).unwrap();
435 let text = String::from_utf8(out).unwrap();
436 assert!(
437 text.to_ascii_lowercase().contains("git") || text.contains("GitDiscoveryFailed"),
438 "render_publish must include diagnostic text; got:\n{text}"
439 );
440
441 plan.rust_crates.clear();
443 let mut out2 = Vec::new();
444 render_publish(&plan, &mut out2).unwrap();
445 let text2 = String::from_utf8(out2).unwrap();
446 assert!(
447 text2.to_ascii_lowercase().contains("git") || text2.contains("GitDiscoveryFailed"),
448 "render_publish must include diagnostic text even for empty plan; got:\n{text2}"
449 );
450 }
451
452 #[test]
453 fn render_publish_report_text_distinguishes_per_package_outcomes() {
454 let mut out = Vec::new();
455 render_publish_report(&mixed_report(), &mut out).unwrap();
456 let text = String::from_utf8(out).unwrap();
457
458 assert!(text.contains("crate-a") && text.contains("published"));
459 assert!(text.contains("crate-b") && text.contains("already published"));
460 assert!(text.contains("crate-c") && text.contains("FAILED [authFailed]: auth failed: bad token"));
461 }
462
463 #[test]
468 fn render_publish_report_failed_includes_error_kind() {
469 use callisto_model::{PublishAttempt, PublishReport, SCHEMA_VERSION};
470
471 let report = PublishReport {
472 schema_version: SCHEMA_VERSION,
473 attempts: vec![
474 PublishAttempt {
475 package: pkg("pkg-a"),
476 version: v1(),
477 result: callisto_model::PublishAttemptResult::Failed {
478 kind: "authFailed".to_string(),
479 error: "invalid token".to_string(),
480 },
481 },
482 PublishAttempt {
483 package: pkg("pkg-b"),
484 version: v1(),
485 result: callisto_model::PublishAttemptResult::Failed {
486 kind: "rateLimited".to_string(),
487 error: "try again in 60s".to_string(),
488 },
489 },
490 ],
491 diagnostics: vec![],
492 };
493
494 let mut out = Vec::new();
495 render_publish_report(&report, &mut out).unwrap();
496 let text = String::from_utf8(out).unwrap();
497
498 assert!(
499 text.contains("authFailed"),
500 "text output must include the error kind 'authFailed' so operators \
501 can distinguish it from transient failures; got:\n{text}"
502 );
503 assert!(
504 text.contains("rateLimited"),
505 "text output must include the error kind 'rateLimited'; got:\n{text}"
506 );
507 }
508
509 #[test]
510 fn publish_report_json_distinguishes_per_package_outcomes() {
511 let json = serde_json::to_string(&mixed_report()).unwrap();
512
513 assert!(json.contains("\"status\":\"published\""));
514 assert!(json.contains("\"status\":\"alreadyPublished\""));
515 assert!(json.contains("\"status\":\"failed\"") && json.contains("auth failed: bad token"));
516 }
517
518 #[test]
522 fn render_matrix_produces_non_json_non_empty_output() {
523 use callisto_model::{
524 MatrixReport, PlatformTarget, PlatformTargetGroup, PlatformTargetKind, RuntimeEcosystem,
525 RuntimeVersionEntry,
526 };
527 use std::collections::BTreeMap;
528
529 let mut platform_targets = BTreeMap::new();
530 platform_targets.insert(
531 "native-mod".to_string(),
532 PlatformTargetGroup {
533 kind: PlatformTargetKind::Napi,
534 source: "napi.targets".to_string(),
535 targets: vec![PlatformTarget {
536 triple: "aarch64-apple-darwin".to_string(),
537 platform: "darwin".to_string(),
538 arch: "arm64".to_string(),
539 abi: None,
540 host_runner: "macos-latest".to_string(),
541 use_cross: false,
542 artifact_name: "native-mod-darwin-arm64".to_string(),
543 package_dir: "native-mod".to_string(),
544 package_name: "native-mod".to_string(),
545 }],
546 },
547 );
548 let mut runtime_versions = BTreeMap::new();
549 runtime_versions.insert(
550 "native-mod".to_string(),
551 vec![RuntimeVersionEntry {
552 ecosystem: RuntimeEcosystem::Npm,
553 field: "engines.node".to_string(),
554 range: ">=20.0.0".to_string(),
555 }],
556 );
557 let report = MatrixReport {
558 schema_version: 1,
559 platform_targets,
560 runtime_versions,
561 diagnostics: vec![],
562 };
563
564 let mut buf = Vec::new();
565 render_matrix(&report, &mut buf).unwrap();
566 let text = String::from_utf8(buf).unwrap();
567
568 assert!(!text.is_empty(), "table output must not be empty");
569 assert!(
570 serde_json::from_str::<serde_json::Value>(&text).is_err(),
571 "table output must not itself parse as JSON: {text}"
572 );
573 assert!(
574 text.contains("native-mod"),
575 "table must mention the package name: {text}"
576 );
577 assert!(
578 text.contains("aarch64-apple-darwin"),
579 "table must mention the triple: {text}"
580 );
581 }
582
583 #[test]
586 fn render_matrix_renders_diagnostics_for_unrecognised_triple() {
587 use callisto_model::{Diagnostic, DiagnosticCode, DiagnosticSeverity, MatrixReport, PackageId};
588 use std::collections::BTreeMap;
589
590 let report = MatrixReport {
591 schema_version: 1,
592 platform_targets: BTreeMap::new(),
593 runtime_versions: BTreeMap::new(),
594 diagnostics: vec![Diagnostic {
595 code: DiagnosticCode::UnrecognisedPlatformTriple,
596 severity: DiagnosticSeverity::Warning,
597 message: "package `native-mod` declares unrecognised platform triple `sparc64-unknown-linux-gnu` in `napi.targets`".to_string(),
598 package: Some(PackageId::Bare("native-mod".to_string())),
599 path: None,
600 escalated_by: None,
601 governed_by: None,
602 }],
603 };
604
605 let mut buf = Vec::new();
606 render_matrix(&report, &mut buf).unwrap();
607 let text = String::from_utf8(buf).unwrap();
608
609 assert!(
610 text.contains("sparc64-unknown-linux-gnu"),
611 "table output must mention the unrecognised triple: {text}"
612 );
613 assert!(
614 text.contains("native-mod"),
615 "table output must mention the offending package: {text}"
616 );
617 }
618
619 #[test]
620 fn render_snapshot_lists_snapshot_tag_and_bumps() {
621 let report = SnapshotReport {
622 schema_version: callisto_model::SCHEMA_VERSION,
623 snapshot_tag: "0.0.0-canary-abc1234".to_string(),
624 bumps: vec![BumpRecord {
625 package: pkg("crate-a"),
626 from: v1(),
627 to: Version::parse("0.0.0-canary-abc1234", VersionGrammar::SemVer).unwrap(),
628 severity: Severity::Patch,
629 governed_by: None,
630 reason: None,
631 }],
632 diagnostics: vec![],
633 };
634 let mut out = Vec::new();
635 render_snapshot(&report, &mut out).unwrap();
636 let text = String::from_utf8(out).unwrap();
637 assert!(text.contains("0.0.0-canary-abc1234"), "got: {text}");
638 assert!(text.contains("crate-a"), "got: {text}");
639 }
640
641 #[test]
642 fn render_validate_ok_reports_pass() {
643 let report = ValidateReport {
644 schema_version: callisto_model::SCHEMA_VERSION,
645 ok: true,
646 diagnostics: vec![],
647 };
648 let mut out = Vec::new();
649 render_validate(&report, &mut out).unwrap();
650 let text = String::from_utf8(out).unwrap();
651 assert!(text.contains("Validation passed"), "got: {text}");
652 }
653
654 #[test]
655 fn render_validate_failure_lists_diagnostics() {
656 use callisto_model::{Diagnostic, DiagnosticCode, DiagnosticSeverity};
657
658 let report = ValidateReport {
659 schema_version: callisto_model::SCHEMA_VERSION,
660 ok: false,
661 diagnostics: vec![Diagnostic {
662 code: DiagnosticCode::UnrecognisedPlatformTriple,
663 severity: DiagnosticSeverity::Error,
664 message: "something is wrong".to_string(),
665 package: None,
666 path: None,
667 escalated_by: None,
668 governed_by: None,
669 }],
670 };
671 let mut out = Vec::new();
672 render_validate(&report, &mut out).unwrap();
673 let text = String::from_utf8(out).unwrap();
674 assert!(text.contains("Validation failed"), "got: {text}");
675 assert!(text.contains("something is wrong"), "got: {text}");
676 }
677
678 #[test]
679 fn render_tag_dry_run_vs_real_use_distinct_headers() {
680 use callisto_model::{CommitSha, TagName};
681
682 let report = TagReport {
683 schema_version: callisto_model::SCHEMA_VERSION,
684 tags: vec![CreatedTag {
685 package: pkg("crate-a"),
686 tag_name: TagName("crate-a@1.0.0".to_string()),
687 sha: CommitSha::parse(&"a".repeat(40)).unwrap(),
688 already_existed: false,
689 }],
690 diagnostics: vec![],
691 };
692
693 let mut dry_run_out = Vec::new();
694 render_tag(&report, true, &mut dry_run_out).unwrap();
695 let dry_run_text = String::from_utf8(dry_run_out).unwrap();
696 assert!(dry_run_text.contains("Would create tags"), "got: {dry_run_text}");
697 assert!(dry_run_text.contains("crate-a@1.0.0"), "got: {dry_run_text}");
698
699 let mut real_out = Vec::new();
700 render_tag(&report, false, &mut real_out).unwrap();
701 let real_text = String::from_utf8(real_out).unwrap();
702 assert!(real_text.contains("Created Tags"), "got: {real_text}");
703 assert!(!real_text.contains("Would create"), "got: {real_text}");
704 }
705
706 #[test]
707 fn render_init_up_to_date_reports_nothing_to_reconcile() {
708 let report = InitReport {
709 schema_version: callisto_model::SCHEMA_VERSION,
710 initialized: false,
711 config_path: std::path::PathBuf::from("callisto.toml"),
712 diff: callisto_model::InitDiff {
713 new_ecosystems: vec![],
714 applied: false,
715 },
716 diagnostics: vec![],
717 };
718 let mut out = Vec::new();
719 render_init(&report, &mut out).unwrap();
720 let text = String::from_utf8(out).unwrap();
721 assert!(text.contains("up to date"), "got: {text}");
722 }
723
724 #[test]
725 fn render_init_applied_drift_reports_reconciled() {
726 let report = InitReport {
727 schema_version: callisto_model::SCHEMA_VERSION,
728 initialized: false,
729 config_path: std::path::PathBuf::from("callisto.toml"),
730 diff: callisto_model::InitDiff {
731 new_ecosystems: vec![Ecosystem::Npm],
732 applied: true,
733 },
734 diagnostics: vec![],
735 };
736 let mut out = Vec::new();
737 render_init(&report, &mut out).unwrap();
738 let text = String::from_utf8(out).unwrap();
739 assert!(text.contains("Reconciled"), "got: {text}");
740 assert!(text.contains("npm"), "got: {text}");
741 }
742
743 #[test]
744 fn render_init_unapplied_drift_reports_needs_yes_flag() {
745 let report = InitReport {
746 schema_version: callisto_model::SCHEMA_VERSION,
747 initialized: false,
748 config_path: std::path::PathBuf::from("callisto.toml"),
749 diff: callisto_model::InitDiff {
750 new_ecosystems: vec![Ecosystem::Npm],
751 applied: false,
752 },
753 diagnostics: vec![],
754 };
755 let mut out = Vec::new();
756 render_init(&report, &mut out).unwrap();
757 let text = String::from_utf8(out).unwrap();
758 assert!(text.contains("Drift detected"), "got: {text}");
759 assert!(text.contains("--yes"), "got: {text}");
760 }
761}