1use std::path::Path;
25
26use crate::capability::CapabilityRegistry;
27use crate::index::Index;
28use crate::model::{Diagnostic, Severity};
29use crate::repository::Repository;
30use crate::share::Share;
31
32use super::cache::{fingerprint_of, Cache, TOOLCHAIN_LIFETIME};
33use super::{
34 services, toolchain, vcs, workflows, FieldSource, KindCount, LayerSummary, ProjectIdentity,
35 ProjectionState, ProviderState, ReasoningSummary, RepositoryEnvironment, RepositoryIdentity,
36 Resolution, TierState, ToolchainAvailability, VcsState,
37};
38
39const LAYER_REFUSALS_NAMED: usize = 3;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct EnvironmentQuery {
46 pub resolution: Resolution,
48 pub probe_services: bool,
51 pub use_cache: bool,
53 pub write_cache: bool,
55}
56
57impl EnvironmentQuery {
58 pub fn fast() -> Self {
61 EnvironmentQuery {
62 resolution: Resolution::Fast,
63 probe_services: true,
64 use_cache: true,
65 write_cache: true,
66 }
67 }
68
69 pub fn full() -> Self {
71 EnvironmentQuery {
72 resolution: Resolution::Full,
73 probe_services: true,
74 use_cache: false,
75 write_cache: true,
76 }
77 }
78
79 pub fn sealed(self) -> Self {
82 EnvironmentQuery {
83 probe_services: false,
84 use_cache: false,
85 write_cache: false,
86 ..self
87 }
88 }
89}
90
91pub struct Inputs<'a> {
94 pub repository: &'a Repository,
96 pub share: Option<&'a Share>,
98 pub index: Option<&'a Index>,
100 pub registry: Option<&'a CapabilityRegistry>,
102 pub policy: Option<&'a crate::policy::LoadedPolicy>,
111}
112
113pub fn resolve(inputs: &Inputs<'_>, query: &EnvironmentQuery) -> RepositoryEnvironment {
119 let root = inputs.repository.root();
120 let local_half = inputs.repository.local_path();
121 let mut diagnostics = Vec::new();
122 let mut provenance = Vec::new();
123
124 let project = ProjectIdentity::of_this_build();
125 provenance.push(FieldSource::exact(
126 "project.version",
127 Some(project.version.clone()),
128 "the crate manifest, at compile time (CARGO_PKG_VERSION)",
129 "environment::resolve",
130 ));
131 provenance.push(FieldSource::exact(
132 "project.summary",
133 Some(project.summary.clone()),
134 "majordomus_cli::about::SUMMARY",
135 "environment::resolve",
136 ));
137 provenance.push(FieldSource::exact(
138 "project.commit",
139 Some(project.commit.clone()),
140 "build.rs, at compile time (MAJORDOMUS_COMMIT)",
141 "environment::resolve",
142 ));
143
144 let repository = repository_identity(inputs.repository);
145 provenance.push(FieldSource::exact(
146 "repository.name",
147 Some(repository.name.clone()),
148 "the base name of the repository root",
149 "environment::resolve",
150 ));
151 provenance.push(FieldSource::exact(
152 "repository.sections",
153 Some(format!("{} section(s)", repository.sections.len())),
154 crate::repository::MANIFEST,
155 "environment::resolve",
156 ));
157
158 let vcs = vcs::inspect(root);
160 match &vcs {
161 VcsState::Git(tree) => {
162 provenance.push(FieldSource::exact(
163 "vcs.branch",
164 tree.branch.clone(),
165 vcs::SOURCE,
166 "environment::vcs",
167 ));
168 provenance.push(FieldSource::exact(
169 "vcs.clean",
170 Some(tree.clean.to_string()),
171 vcs::SOURCE,
172 "environment::vcs",
173 ));
174 provenance.push(match (tree.ahead, tree.behind) {
175 (Some(a), Some(b)) => FieldSource::exact(
176 "vcs.ahead_behind",
177 Some(format!("{a}/{b}")),
178 vcs::SOURCE,
179 "environment::vcs",
180 ),
181 _ => FieldSource::unknown(
182 "vcs.ahead_behind",
183 "the branch tracks no upstream",
184 "environment::vcs",
185 ),
186 });
187 }
188 VcsState::Unavailable { reason } => {
189 diagnostics.push(Diagnostic::warning(
190 "vcs_unavailable",
191 None,
192 format!("version control could not be read: {reason}"),
193 ));
194 provenance.push(FieldSource::unknown(
195 "vcs.branch",
196 format!("{}: {reason}", vcs::SOURCE),
197 "environment::vcs",
198 ));
199 }
200 }
201
202 let mut cache = if query.use_cache || query.write_cache {
203 Cache::load(root, &local_half)
204 } else {
205 Cache::default()
206 };
207
208 let layer_fingerprint = layer_fingerprint(inputs.repository, &vcs);
210 let layer = match (inputs.index, inputs.registry) {
211 (Some(index), Some(registry)) => {
212 let summary = summarise(index, registry);
213 provenance.push(FieldSource::exact(
214 "layer.objects",
215 summary.objects.map(|n| n.to_string()),
216 "the index this process built",
217 "environment::resolve",
218 ));
219 summary
220 }
221 _ => {
222 let cached = query
223 .use_cache
224 .then_some(cache.tiers.layer.as_ref())
225 .flatten()
226 .and_then(|e| e.fresh(&layer_fingerprint, None));
227 match cached {
228 Some(summary) => {
229 let mut summary = summary.clone();
230 summary.state = TierState::Cached;
231 provenance.push(FieldSource::cached(
232 "layer.objects",
233 summary.objects.map(|n| n.to_string()),
234 format!(
235 "{}, written by the last full resolution",
236 super::cache::CACHE_PATH
237 ),
238 "environment::cache",
239 ));
240 summary
241 }
242 None => {
243 diagnostics.push(Diagnostic::info(
244 "environment_layer_not_counted",
245 None,
246 "what the layer holds is not in the cache for this state of the repository; `majordomus env status` counts it and writes the cache",
247 ));
248 provenance.push(FieldSource::unknown(
249 "layer.objects",
250 "a fast resolution does not build the index, and no cache entry matched",
251 "environment::cache",
252 ));
253 LayerSummary::unavailable()
254 }
255 }
256 }
257 };
258 if layer.degraded == Some(true) {
259 let named: Vec<String> = layer
264 .refused
265 .iter()
266 .take(LAYER_REFUSALS_NAMED)
267 .map(|d| match &d.path {
268 Some(p) => format!("{p} — {}", d.message),
269 None => d.message.clone(),
270 })
271 .collect();
272 let total = layer.invalid.unwrap_or(layer.refused.len());
273 let rest = total.saturating_sub(named.len());
274 let list = match (named.is_empty(), rest) {
275 (true, _) => String::new(),
276 (false, 0) => format!(": {}", named.join("; ")),
277 (false, n) => format!(": {}; and {n} more", named.join("; ")),
278 };
279 diagnostics.push(Diagnostic::warning(
280 "layer_degraded",
281 None,
282 format!("{total} declared file(s) did not become objects{list}"),
283 ));
284 }
285
286 let workflow_fingerprint = workflow_fingerprint(root);
288 let cached_workflows = query
289 .use_cache
290 .then_some(cache.tiers.workflows.as_ref())
291 .flatten()
292 .and_then(|e| e.fresh(&workflow_fingerprint, None));
293 let workflows = match cached_workflows {
294 Some(catalogue) => {
295 let mut catalogue = catalogue.clone();
296 catalogue.state = TierState::Cached;
297 provenance.push(FieldSource::cached(
298 "workflows",
299 Some(format!("{} workflow(s)", catalogue.workflows.len())),
300 format!("{}, keyed by the workflow files", super::cache::CACHE_PATH),
301 "environment::cache",
302 ));
303 catalogue
304 }
305 None => {
306 let catalogue = workflows::resolve(root);
309 provenance.push(match catalogue.state {
310 TierState::Unavailable => FieldSource::unknown(
311 "workflows",
312 "no workflow runner answered for this repository",
313 "environment::workflows",
314 ),
315 _ => FieldSource::exact(
316 "workflows",
317 Some(format!("{} workflow(s)", catalogue.workflows.len())),
318 workflows::SOURCE,
319 "environment::workflows",
320 ),
321 });
322 catalogue
323 }
324 };
325
326 let declared = toolchain::declared(root);
328 let toolchain_fingerprint = fingerprint_of(
329 root,
330 &declared
331 .iter()
332 .map(|t| t.declared_by.clone())
333 .collect::<Vec<_>>(),
334 &["toolchains"],
335 );
336 let toolchains = if query.resolution == Resolution::Full {
337 let resolved = toolchain::with_installed(declared);
338 provenance.push(FieldSource::exact(
339 "toolchains",
340 Some(format!("{} declared", resolved.len())),
341 "each toolchain's own --version, bounded",
342 "environment::toolchain",
343 ));
344 resolved
345 } else {
346 let cached = query
347 .use_cache
348 .then_some(cache.tiers.toolchains.as_ref())
349 .flatten()
350 .and_then(|e| e.fresh(&toolchain_fingerprint, Some(TOOLCHAIN_LIFETIME)))
351 .cloned();
352 match cached {
353 Some(resolved) => {
354 provenance.push(FieldSource::cached(
355 "toolchains",
356 Some(format!("{} declared", resolved.len())),
357 format!("{}, within its lifetime", super::cache::CACHE_PATH),
358 "environment::cache",
359 ));
360 resolved
361 }
362 None => {
363 provenance.push(FieldSource::exact(
364 "toolchains",
365 Some(format!("{} declared", declared.len())),
366 "the file that declares each one; no version was asked",
367 "environment::toolchain",
368 ));
369 declared
370 }
371 }
372 };
373 for missing in toolchains
374 .iter()
375 .filter(|t| t.availability == ToolchainAvailability::Missing)
376 {
377 diagnostics.push(Diagnostic::warning(
378 "toolchain_missing",
379 Some(missing.declared_by.clone()),
380 format!("{} is declared here and is not installed", missing.title),
381 ));
382 }
383 for mismatch in toolchains
384 .iter()
385 .filter(|t| t.availability == ToolchainAvailability::Mismatch)
386 {
387 diagnostics.push(Diagnostic::warning(
388 "toolchain_mismatch",
389 Some(mismatch.declared_by.clone()),
390 format!(
391 "{} {} is installed and this file declares {}",
392 mismatch.title,
393 mismatch.installed.as_deref().unwrap_or("?"),
394 mismatch.declared.as_deref().unwrap_or("?"),
395 ),
396 ));
397 }
398
399 let (providers, provider_source) =
401 provider_states(inputs.repository, inputs.share, inputs.policy);
402 provenance.push(FieldSource::exact(
403 "providers",
404 Some(format!("{} projection(s)", providers.len())),
405 provider_source,
406 "environment::resolve",
407 ));
408 for stale in providers
409 .iter()
410 .filter(|p| matches!(p.state, ProjectionState::Stale | ProjectionState::Absent))
411 {
412 diagnostics.push(Diagnostic::warning(
413 "projection_stale",
414 Some(stale.target.clone()),
415 format!(
416 "{} is {} against the policy that renders it; `majordomus generate` rewrites it",
417 stale.target,
418 match stale.state {
419 ProjectionState::Absent => "missing",
420 _ => "stale",
421 }
422 ),
423 ));
424 }
425
426 let services = services::resolve(root, &local_half, query.probe_services);
428 provenance.push(match services.first().and_then(|s| s.url.clone()) {
429 Some(_) => FieldSource::exact(
430 "services.url",
431 services.first().and_then(|s| s.url.clone()),
432 format!("{}/{}", local_half, crate::lease::LEASE_PATH),
433 "environment::services",
434 ),
435 None => FieldSource::unknown(
436 "services.url",
437 format!(
438 "no server holds the lease at {}/{}",
439 local_half,
440 crate::lease::LEASE_PATH
441 ),
442 "environment::services",
443 ),
444 });
445
446 let reasoning = inputs.share.and_then(|share| {
450 let declarations = share.providers().ok()?;
451 let situation =
452 crate::reasoning::Situation::resolve(root, Some(share.dir()), &declarations, &[])
453 .ok()?;
454 if situation.catalogue.advisors.is_empty() {
455 return None;
456 }
457 Some(ReasoningSummary {
458 operational: true,
459 mode: situation.mode.mode.as_str().to_string(),
460 unavailable: situation.states.len() - situation.available(),
461 available: situation
462 .states
463 .iter()
464 .filter(|a| a.status == crate::reasoning::AdvisorStatus::Available)
465 .map(|a| a.id.clone())
466 .collect(),
467 })
468 });
469
470 let environment = RepositoryEnvironment {
471 schema: RepositoryEnvironment::schema_id(),
472 generated_at: crate::peers::rfc3339(std::time::SystemTime::now()),
473 resolution: query.resolution,
474 project,
475 repository,
476 vcs,
477 toolchains,
478 layer,
479 workflows,
480 providers,
481 services,
482 reasoning,
483 diagnostics,
484 provenance,
485 };
486
487 if query.write_cache {
488 if environment.layer.state == TierState::Resolved {
492 cache.tiers.layer = Some(Cache::entry(layer_fingerprint, environment.layer.clone()));
493 }
494 if environment.workflows.state == TierState::Resolved {
495 cache.tiers.workflows = Some(Cache::entry(
496 workflow_fingerprint,
497 environment.workflows.clone(),
498 ));
499 }
500 if environment
501 .toolchains
502 .iter()
503 .any(|t| t.availability != ToolchainAvailability::Unknown)
504 {
505 cache.tiers.toolchains = Some(Cache::entry(
506 toolchain_fingerprint,
507 environment.toolchains.clone(),
508 ));
509 }
510 if let Err(e) = cache.store(root, &local_half) {
511 tracing::debug!(error = %e, "the environment cache could not be written");
512 }
513 }
514
515 environment
516}
517
518fn repository_identity(repository: &Repository) -> RepositoryIdentity {
520 let root = repository.root();
521 RepositoryIdentity {
522 name: root
523 .file_name()
524 .map(|n| n.to_string_lossy().into_owned())
525 .unwrap_or_else(|| root.display().to_string()),
526 root: root.display().to_string(),
527 layer_schema: repository.manifest().schema.clone(),
528 sections: repository
529 .manifest()
530 .sections
531 .iter()
532 .map(|(name, path)| (name.clone(), format!(".ai/{path}")))
533 .collect(),
534 local_path: repository.local_path(),
535 linked_worktree: root.join(".git").is_file(),
537 }
538}
539
540fn summarise(index: &Index, registry: &CapabilityRegistry) -> LayerSummary {
542 let refused: Vec<crate::model::Diagnostic> = index
545 .diagnostics
546 .iter()
547 .filter(|d| d.severity == Severity::Error)
548 .cloned()
549 .collect();
550 LayerSummary {
551 state: TierState::Resolved,
552 kinds: index
553 .kinds()
554 .into_iter()
555 .map(|(kind, count)| KindCount {
556 kind: kind.to_string(),
557 count,
558 })
559 .collect(),
560 objects: Some(index.objects.len()),
561 capabilities: Some(registry.summary().total),
562 invalid: Some(refused.len()),
563 refused,
564 degraded: Some(index.state == crate::index::State::Degraded),
565 }
566}
567
568fn provider_states(
570 repository: &Repository,
571 share: Option<&Share>,
572 already: Option<&crate::policy::LoadedPolicy>,
573) -> (Vec<ProviderState>, String) {
574 let source = format!(
575 "{}, projections[]",
576 repository
577 .section_path("policy")
578 .unwrap_or_else(|| ".ai/repo/policy.yaml".into())
579 );
580 let read;
581 let policy = match already {
582 Some(policy) => policy,
583 None => {
584 let Ok(loaded) = crate::policy::LoadedPolicy::load(repository) else {
585 return (Vec::new(), format!("{source} (unreadable)"));
586 };
587 read = loaded;
588 &read
589 }
590 };
591 let rendered = share.and_then(|share| {
595 crate::providers::artifacts(repository, share, policy)
596 .ok()
597 .map(|artifacts| {
598 artifacts
599 .into_iter()
600 .map(|a| {
601 let state = crate::providers::state_of(repository.root(), &a);
602 (a.path, state)
603 })
604 .collect::<Vec<_>>()
605 })
606 });
607 let states = policy
608 .policy
609 .projections
610 .iter()
611 .map(|p| ProviderState {
612 id: p.provider.clone(),
613 target: p.target.clone(),
614 always_loaded: p.always_loaded,
615 state: match rendered
616 .as_ref()
617 .and_then(|r| r.iter().find(|(path, _)| *path == p.target))
618 {
619 Some((_, crate::providers::TargetState::Current)) => ProjectionState::Current,
620 Some((_, crate::providers::TargetState::Stale)) => ProjectionState::Stale,
621 Some((_, crate::providers::TargetState::Absent)) => ProjectionState::Absent,
622 None => ProjectionState::Unknown,
623 },
624 })
625 .collect();
626 (states, source)
627}
628
629fn layer_fingerprint(repository: &Repository, vcs: &VcsState) -> String {
638 let mut paths: Vec<String> = vec![
639 crate::repository::MANIFEST.to_string(),
640 ".ai/repo/knowledge/sources.yaml".to_string(),
641 ".ai/repo/knowledge/kinds.yaml".to_string(),
642 "share/kinds.yaml".to_string(),
643 ];
644 if let Some(policy) = repository.section_path("policy") {
645 paths.push(policy);
646 }
647 if let Some(scope) = repository.section_path("scope") {
648 paths.push(scope);
649 }
650 let head = vcs
651 .tree()
652 .and_then(|t| t.head.clone())
653 .unwrap_or_else(|| "unborn".into());
654 if let Some(tree) = vcs.tree() {
655 paths.extend(
658 tree.changed_paths
659 .iter()
660 .filter(|p| is_layer_path(repository, p))
661 .cloned(),
662 );
663 }
664 fingerprint_of(repository.root(), &paths, &["layer", &head])
665}
666
667fn is_layer_path(repository: &Repository, path: &str) -> bool {
670 if path.starts_with(&repository.local_path()) {
671 return false;
672 }
673 path.starts_with(".ai/")
674 || path.starts_with("docs/")
675 || path.starts_with("share/")
676 || path.starts_with("lib/")
677 || path.starts_with("test/cases/")
678 || path.starts_with("bin/")
679}
680
681fn workflow_fingerprint(root: &Path) -> String {
683 let mut paths = vec!["justfile".to_string(), ".justfile".to_string()];
684 if let Ok(entries) = std::fs::read_dir(root.join(".just")) {
688 let mut names: Vec<String> = entries
689 .flatten()
690 .filter_map(|e| e.file_name().into_string().ok())
691 .map(|name| format!(".just/{name}"))
692 .collect();
693 crate::order::canonical(&mut names);
694 paths.extend(names);
695 } else {
696 paths.push(".just".to_string());
699 }
700 fingerprint_of(root, &paths, &["workflows"])
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use crate::synthetic::{Shape, SyntheticRepository};
707
708 fn repository() -> SyntheticRepository {
709 SyntheticRepository::new(Shape {
710 rules: 3,
711 ..Shape::default()
712 })
713 .expect("a synthetic repository")
714 }
715
716 fn full(repo: &SyntheticRepository) -> RepositoryEnvironment {
717 let index = repo.index().expect("an index");
718 let registry = CapabilityRegistry::builder()
719 .with_modules(crate::capability::builtin::modules())
720 .with_index(&index)
721 .build()
722 .expect("a registry");
723 let repository = Repository::open(repo.root()).expect("a repository");
724 resolve(
725 &Inputs {
726 repository: &repository,
727 share: None,
728 index: Some(&index),
729 registry: Some(®istry),
730 policy: None,
731 },
732 &EnvironmentQuery::full().sealed(),
733 )
734 }
735
736 fn fast(repo: &SyntheticRepository, query: EnvironmentQuery) -> RepositoryEnvironment {
737 let repository = Repository::open(repo.root()).expect("a repository");
738 resolve(
739 &Inputs {
740 repository: &repository,
741 share: None,
742 index: None,
743 registry: None,
744 policy: None,
745 },
746 &query,
747 )
748 }
749
750 #[test]
751 fn a_full_resolution_counts_what_the_layer_holds() {
752 let repo = repository();
753 let env = full(&repo);
754 assert_eq!(env.resolution, Resolution::Full);
755 assert_eq!(env.layer.state, TierState::Resolved);
756 assert!(env.layer.objects.unwrap_or(0) > 0);
757 assert_eq!(env.layer.kind("rule"), Some(3));
758 }
759
760 #[test]
763 fn a_fast_resolution_with_a_cold_cache_reports_unknown_and_never_zero() {
764 let repo = repository();
765 let env = fast(&repo, EnvironmentQuery::fast().sealed());
766 assert_eq!(env.layer.state, TierState::Unavailable);
767 assert_eq!(env.layer.objects, None);
768 assert_eq!(env.layer.kind("rule"), None);
769 assert!(env
770 .diagnostics
771 .iter()
772 .any(|d| d.code == "environment_layer_not_counted"));
773 assert_eq!(
774 env.explain("layer.objects").map(|p| p.confidence),
775 Some(super::super::Confidence::Unknown)
776 );
777 }
778
779 #[test]
780 fn a_full_resolution_warms_the_cache_a_fast_one_then_reads() {
781 let repo = repository();
782 let index = repo.index().expect("an index");
783 let registry = CapabilityRegistry::builder()
784 .with_modules(crate::capability::builtin::modules())
785 .with_index(&index)
786 .build()
787 .expect("a registry");
788 let repository = Repository::open(repo.root()).expect("a repository");
789 let warm = resolve(
790 &Inputs {
791 repository: &repository,
792 share: None,
793 index: Some(&index),
794 registry: Some(®istry),
795 policy: None,
796 },
797 &EnvironmentQuery {
798 probe_services: false,
799 ..EnvironmentQuery::full()
800 },
801 );
802 assert_eq!(warm.layer.state, TierState::Resolved);
803
804 let then = fast(
805 &repo,
806 EnvironmentQuery {
807 probe_services: false,
808 write_cache: false,
809 ..EnvironmentQuery::fast()
810 },
811 );
812 assert_eq!(then.layer.state, TierState::Cached);
813 assert_eq!(then.layer.objects, warm.layer.objects);
814 assert_eq!(then.layer.kinds, warm.layer.kinds);
815 assert_eq!(
816 then.explain("layer.objects").map(|p| p.confidence),
817 Some(super::super::Confidence::Cached)
818 );
819 }
820
821 #[test]
824 fn fast_and_full_agree_on_everything_fast_can_see() {
825 let repo = repository();
826 let full = full(&repo);
827 let fast = fast(&repo, EnvironmentQuery::fast().sealed());
828 assert_eq!(fast.project, full.project);
829 assert_eq!(fast.repository, full.repository);
830 assert_eq!(fast.vcs, full.vcs);
831 assert_eq!(fast.providers, full.providers);
832 let ids = |e: &RepositoryEnvironment| -> Vec<String> {
833 e.services.iter().map(|s| s.id.clone()).collect()
834 };
835 assert_eq!(ids(&fast), ids(&full));
836 assert_eq!(
837 fast.toolchains
838 .iter()
839 .map(|t| t.id.clone())
840 .collect::<Vec<_>>(),
841 full.toolchains
842 .iter()
843 .map(|t| t.id.clone())
844 .collect::<Vec<_>>()
845 );
846 }
847
848 #[test]
852 fn a_snapshot_of_an_unchanged_repository_is_deterministic() {
853 let repo = repository();
854 let a = full(&repo);
855 let b = full(&repo);
856 assert_eq!(a.digest(), b.digest());
857 let mut a_json = serde_json::to_value(&a).expect("json");
858 let mut b_json = serde_json::to_value(&b).expect("json");
859 for v in [&mut a_json, &mut b_json] {
860 v["generated_at"] = serde_json::Value::Null;
861 }
862 assert_eq!(a_json, b_json);
863 }
864
865 #[test]
870 fn the_digest_follows_what_the_snapshot_says_and_not_every_byte() {
871 let repo = repository();
872 let before = full(&repo).digest();
873
874 repo.touch_rule(0, "-changed").expect("a changed rule");
875 assert_eq!(
876 before,
877 full(&repo).digest(),
878 "editing a rule's body changes no count and so is not news"
879 );
880
881 std::fs::write(
882 repo.root().join(".ai/repo/rules/project/rule-99.v1.md"),
883 "---\nid: project.rule-99\nversion: 1\nkind: rule\ntitle: T\ndescription: D\nstatement: S\nstatus: active\nclass: advisory\n---\n\n# Rationale\n\nx\n",
884 )
885 .expect("a new rule");
886 assert_ne!(
887 before,
888 full(&repo).digest(),
889 "a rule that did not exist before is news"
890 );
891 }
892
893 #[test]
897 fn the_digest_ignores_the_moment_and_the_resolution() {
898 let repo = repository();
899 let mut a = full(&repo);
900 let mut b = a.clone();
901 b.generated_at = "1999-01-01T00:00:00Z".into();
902 b.resolution = Resolution::Fast;
903 b.provenance.clear();
904 assert_eq!(a.digest(), b.digest());
905 a.repository.name = "renamed".into();
906 assert_ne!(a.digest(), b.digest());
907 }
908
909 #[test]
910 fn a_repository_that_is_not_a_work_tree_still_produces_a_snapshot() {
911 let repo = repository();
912 let env = fast(&repo, EnvironmentQuery::fast().sealed());
913 if matches!(env.vcs, VcsState::Unavailable { .. }) {
916 assert!(env.diagnostics.iter().any(|d| d.code == "vcs_unavailable"));
917 }
918 assert!(!env.schema.is_empty());
919 assert_eq!(env.project.version, crate::VERSION);
920 }
921
922 #[test]
923 fn every_named_field_of_the_snapshot_can_say_where_it_came_from() {
924 let repo = repository();
925 let env = full(&repo);
926 for field in [
927 "project.version",
928 "project.summary",
929 "project.commit",
930 "repository.name",
931 "repository.sections",
932 "layer.objects",
933 "workflows",
934 "toolchains",
935 "providers",
936 "services.url",
937 ] {
938 let source = env
939 .explain(field)
940 .unwrap_or_else(|| panic!("{field} has no provenance"));
941 assert!(!source.source.is_empty(), "{field} names no source");
942 assert!(!source.resolver.is_empty(), "{field} names no resolver");
943 }
944 }
945}