diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 55daf34..3981bb5 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -417,6 +417,14 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { return Err(MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: ruling.0.clone() }); } } + for dependency in &spec.depends_on { + let names_a_cut = docs.of_kind(K::CutSpec).any(|(_, document)| { + matches!(document, D::CutSpec(other) if other.campaign == spec.campaign && other.cut.0 == dependency.0) + }); + if !names_a_cut { + return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); + } + } let predecessor = D::CutSpec(epiphany_pipeline::PipelineCutSpec { revision: spec.revision.wrapping_sub(1), ..spec.clone() }); revision_rule(docs, K::CutSpec, key, spec.revision, &predecessor) } @@ -1902,6 +1910,41 @@ mod tests { ); } + #[test] + fn depends_on_names_cuts() { + let depending = |cut: &str, on: &[&str]| { + let mut spec = cut_spec(cut, 1); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + let mut mind = seeded(); + // An unknown label, and a spec id (a revision, not a cut), are refused. + assert_eq!( + refusal(admit(&mut mind, vec![depending("2", &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![depending("2", &[&id("cut_spec", "cut-1.r1")])])), + MindRefusal::UnknownDependency { cut: id("cut_spec", "cut-1.r1") } + ); + // The same batch carries the cut it depends on. + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), depending("2", &["1"])])); + // The image carries it, and any revision of the cut names it. + committed(admit(&mut mind, vec![depending("3", &["1", "2"])])); + // A cut of another campaign is not this campaign's cut. + let mut other = cut_spec("9", 1); + other.campaign = slug("elsewhere"); + let mut elsewhere = campaign(&[REPO]); + if let D::Campaign(campaign) = &mut elsewhere { + campaign.slug = slug("elsewhere"); + } + committed(admit(&mut mind, vec![elsewhere, D::CutSpec(other)])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("4", &["9"])])), + MindRefusal::UnknownDependency { cut: "9".into() } + ); + } + #[test] fn a_report_head_may_spell_its_commit_short_or_full() { let full = "5f98228d0123456789abcdef0123456789abcdef"; diff --git a/crates/huginn-mind/src/refusal.rs b/crates/huginn-mind/src/refusal.rs index 8e0e058..1a33ac7 100644 --- a/crates/huginn-mind/src/refusal.rs +++ b/crates/huginn-mind/src/refusal.rs @@ -43,6 +43,7 @@ pub enum MindRefusal { QuoteWithoutOperator { ruling: String }, EmptyRepos { campaign: String }, RepoNotInCampaign { repo: String }, + UnknownDependency { cut: String }, CutReportWithoutSpec { report: String }, SpecMismatch { field: String }, RangeOutsideCommits { head: String },