lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 22787c02c6493165ba1c68b52e1d7688928206de
parent adc1b18196a5f45c2ee7f0da37561bec96e34ec3
Author: triesap <tyson@radroots.org>
Date:   Fri, 21 Aug 2026 03:23:24 +0000

test: restore release coverage gates

- add exact negative-path coverage for trade, transport, and FFI boundaries
- exercise release-artifact and source-lock policy drift independently
- preserve governed coverage thresholds and normalization semantics
- qualify the required crates before service build integration

Diffstat:
Mcrates/event_codec/src/authoring/mod.rs | 5+++++
Mcrates/event_codec/tests/event_ref.rs | 10++++++++++
Mcrates/event_codec/tests/parser_robustness_corpus.rs | 15+++++++++++++++
Mcrates/event_codec_wasm/src/lib.rs | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/mobile_ffi/src/dto.rs | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk_ffi/src/lib.rs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/trade/src/evidence_manifest.rs | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/trade/src/evidence_report.rs | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/transport_nostr/src/status.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/service_release_artifacts.rs | 600+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/service_source_lock_command.rs | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 1452 insertions(+), 3 deletions(-)

diff --git a/crates/event_codec/src/authoring/mod.rs b/crates/event_codec/src/authoring/mod.rs @@ -428,6 +428,11 @@ mod tests { } #[test] + fn plan_digest_parser_rejects_a_wrong_length_before_hex_decoding() { + assert_eq!(PlanDigest::parse_hex("a"), Err(PlanDigestError)); + } + + #[test] fn every_bound_authoring_field_changes_the_plan_digest() { let base = plan(ALICE, 1_700_000_000, Vec::new(), "hello"); let variants = [ diff --git a/crates/event_codec/tests/event_ref.rs b/crates/event_codec/tests/event_ref.rs @@ -65,6 +65,16 @@ fn parse_event_ref_tag_rejects_noncanonical_event_pointer_shapes() { let tag = vec![ "e".to_string(), hex_64('5'), + common::AUTHOR.to_string(), + KIND_POST.to_string(), + RELAY_PRIMARY_WSS.to_string(), + ]; + let err = parse_event_ref_tag(&tag, "e").unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("e"))); + + let tag = vec![ + "e".to_string(), + hex_64('5'), hex_64('6'), KIND_POST.to_string(), ]; diff --git a/crates/event_codec/tests/parser_robustness_corpus.rs b/crates/event_codec/tests/parser_robustness_corpus.rs @@ -46,6 +46,21 @@ fn corpus_rejects_oversized_input_before_json_allocation() { } #[test] +fn signed_event_decoder_accepts_valid_input_and_rejects_oversize_before_parsing() { + let encoded = encode(&valid_event()); + decode::signed_event(encoded.as_str()).expect("ID-valid signed event"); + + let raw = " ".repeat(DEFAULT_RAW_JSON_MAX_BYTES + 1); + assert_eq!( + decode::signed_event(raw.as_str()).expect_err("oversized signed event"), + DecodeError::InputTooLarge { + max: DEFAULT_RAW_JSON_MAX_BYTES, + actual: DEFAULT_RAW_JSON_MAX_BYTES + 1, + } + ); +} + +#[test] fn corpus_rejects_tag_count_overflow_before_id_verification() { let mut value = valid_event(); object(&mut value).insert( diff --git a/crates/event_codec_wasm/src/lib.rs b/crates/event_codec_wasm/src/lib.rs @@ -1057,6 +1057,7 @@ pub fn group_roles_tags(group_json: &str) -> Result<String, RadrootsJsValue> { #[cfg(test)] mod tests { use super::*; + use core::num::NonZeroU64; use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; use radroots_event::envelope::kind::{ KIND_FARM_FILE_METADATA, KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_SOURCE, @@ -1073,6 +1074,7 @@ mod tests { FarmWorkspaceRelayMode, RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION, RADROOTS_FARM_WORKSPACE_SCHEMA, }; + use radroots_event::id::TradeId; use radroots_event::knowledge::{ AddressableRef, KnowledgeCitationSpan, KnowledgeFieldContext, KnowledgeLocation, KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation, @@ -1097,6 +1099,12 @@ mod tests { ReportFileTarget, ReportType, SocialFarmAnchor, SocialLocation, SocialMediaDimensions, SocialTarget, }; + use radroots_trade::evidence::{ + RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidencePolicyDigestV1, + RadrootsTradeEvidenceScopePrerequisitesV1, RadrootsTradeEvidenceSourceCompletionV1, + RadrootsTradeEvidenceSourceIdV1, RadrootsTradeEvidenceSourceRequirementV1, + RadrootsTradeEvidenceSourceResultDigestV1, RadrootsTradeEvidenceSourceResultV1, + }; fn sample_listing() -> OperationalListing { let quantity = @@ -2490,5 +2498,110 @@ mod tests { let secret = "private-report-content"; let error = rhi_evidence_report_parse_json(secret).expect_err("malformed report"); assert!(!format!("{error:?}").contains(secret)); + + let decision_vectors: serde_json::Value = serde_json::from_str(include_str!( + "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json" + )) + .expect("RHI decision corpus"); + let superseding = decision_vectors["vectors"] + .as_array() + .expect("RHI decision vectors") + .iter() + .find(|entry| entry["id"] == "rhi_evidence_attestation_superseding_002") + .expect("superseding vector"); + let superseding_content = superseding["expected"]["canonical_event_content_utf8"] + .as_str() + .expect("superseding canonical content"); + let superseding_report: serde_json::Value = serde_json::from_str( + &rhi_evidence_report_parse_json(superseding_content) + .expect("superseding report projection"), + ) + .expect("superseding report JSON"); + assert_eq!(superseding_report["outcome"], "valid"); + assert_eq!( + superseding_report["supersession"]["report_id"], + "7777777777777777777777777777777777777777777777777777777777777777" + ); + assert_eq!( + superseding_report["supersession"]["event_id"], + "8888888888888888888888888888888888888888888888888888888888888888" + ); + } + + #[test] + fn evidence_manifest_projection_covers_every_admitted_vocabulary() { + assert_eq!( + coverage_name(RadrootsTradeEvidenceCoverageV1::Missing), + "missing" + ); + assert_eq!( + coverage_name(RadrootsTradeEvidenceCoverageV1::Partial), + "partial" + ); + assert_eq!( + coverage_name(RadrootsTradeEvidenceCoverageV1::ScopeSatisfied), + "scope_satisfied" + ); + assert_eq!( + coverage_name(RadrootsTradeEvidenceCoverageV1::Unsupported), + "unsupported" + ); + assert_eq!(outcome_name(RadrootsTradeEvidenceOutcomeV1::Valid), "valid"); + assert_eq!( + outcome_name(RadrootsTradeEvidenceOutcomeV1::Invalid), + "invalid" + ); + assert_eq!( + outcome_name(RadrootsTradeEvidenceOutcomeV1::Indeterminate), + "indeterminate" + ); + assert_eq!( + attestation_outcome_name(RadrootsRhiEvidenceAttestationOutcomeV1::Valid), + "valid" + ); + assert_eq!( + attestation_outcome_name(RadrootsRhiEvidenceAttestationOutcomeV1::Invalid), + "invalid" + ); + assert_eq!( + attestation_outcome_name(RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate), + "indeterminate" + ); + + let source = RadrootsTradeEvidenceManifestSourceResultV1::new( + RadrootsTradeEvidenceSourceIdV1::parse("typed_source").expect("source id"), + RadrootsTradeEvidenceSourceResultV1::new( + RadrootsTradeEvidenceSourceRequirementV1::Required, + RadrootsTradeEvidenceSourceCompletionV1::Complete, + 0, + ) + .expect("source result"), + RadrootsTradeEvidenceSourceResultDigestV1::from_bytes([0x33; 32]), + ); + let manifest = RadrootsTradeEvidenceManifestV1::new( + TradeId::from_bytes([0x11; 16]), + NonZeroU64::new(1).expect("nonzero generation"), + RadrootsTradeEvidencePolicyDigestV1::from_bytes([0x22; 32]), + 1_800_000_000, + RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied, + [source], + [], + ) + .expect("manifest"); + let canonical_hex = hex::encode(manifest.canonical_bytes()); + let projected: serde_json::Value = serde_json::from_str( + &trade_evidence_manifest_parse_json(&canonical_hex).expect("manifest projection"), + ) + .expect("manifest projection JSON"); + assert_eq!(projected["coverage"], "scope_satisfied"); + assert_eq!(projected["canonical_bytes_hex"], canonical_hex); + + assert!(trade_evidence_manifest_parse_json("0").is_err()); + assert!( + trade_evidence_manifest_parse_json(&"0".repeat( + radroots_trade::evidence::RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES * 2 + 2 + )) + .is_err() + ); } } diff --git a/crates/mobile_ffi/src/dto.rs b/crates/mobile_ffi/src/dto.rs @@ -2298,9 +2298,12 @@ fn require_schema(schema_version: u16) -> Result<(), RadrootsAppError> { #[cfg(test)] mod tests { + use core::num::NonZeroU64; use std::io::Write; use std::os::fd::AsRawFd; + use radroots_event::id::TradeId; + use super::*; fn rhi_attestation_fixture() -> serde_json::Value { @@ -2354,6 +2357,139 @@ mod tests { } #[test] + fn evidence_manifest_and_supersession_project_the_complete_vocabulary() { + use radroots_sdk::trade::{ + RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidenceManifestV1, + RadrootsTradeEvidencePolicyDigestV1, RadrootsTradeEvidenceScopePrerequisitesV1, + RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceSourceIdV1, + RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultDigestV1, + RadrootsTradeEvidenceSourceResultV1, + }; + + assert_eq!( + FfiTradeEvidenceCoverage::from( + radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing + ), + FfiTradeEvidenceCoverage::Missing + ); + assert_eq!( + FfiTradeEvidenceCoverage::from( + radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial + ), + FfiTradeEvidenceCoverage::Partial + ); + assert_eq!( + FfiTradeEvidenceCoverage::from( + radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied + ), + FfiTradeEvidenceCoverage::ScopeSatisfied + ); + assert_eq!( + FfiTradeEvidenceCoverage::from( + radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported + ), + FfiTradeEvidenceCoverage::Unsupported + ); + assert_eq!( + FfiTradeEvidenceOutcome::from( + radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid + ), + FfiTradeEvidenceOutcome::Valid + ); + assert_eq!( + FfiTradeEvidenceOutcome::from( + radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid + ), + FfiTradeEvidenceOutcome::Invalid + ); + assert_eq!( + FfiTradeEvidenceOutcome::from( + radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate + ), + FfiTradeEvidenceOutcome::Indeterminate + ); + + let source = RadrootsTradeEvidenceManifestSourceResultV1::new( + RadrootsTradeEvidenceSourceIdV1::parse("typed_source").expect("source id"), + RadrootsTradeEvidenceSourceResultV1::new( + RadrootsTradeEvidenceSourceRequirementV1::Required, + RadrootsTradeEvidenceSourceCompletionV1::Complete, + 0, + ) + .expect("source result"), + RadrootsTradeEvidenceSourceResultDigestV1::from_bytes([0x33; 32]), + ); + let manifest = RadrootsTradeEvidenceManifestV1::new( + TradeId::from_bytes([0x11; 16]), + NonZeroU64::new(1).expect("nonzero generation"), + RadrootsTradeEvidencePolicyDigestV1::from_bytes([0x22; 32]), + 1_800_000_000, + RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied, + [source], + [], + ) + .expect("manifest"); + let projected = parse_trade_evidence_manifest(manifest.canonical_bytes().to_vec()) + .expect("manifest projection"); + assert_eq!(projected.coverage, FfiTradeEvidenceCoverage::ScopeSatisfied); + assert_eq!( + projected.canonical_bytes_hex, + hex::encode(manifest.canonical_bytes()) + ); + + let decisions: serde_json::Value = serde_json::from_str(include_str!( + "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json" + )) + .expect("RHI decision corpus"); + let superseding = decisions["vectors"] + .as_array() + .expect("RHI decision vectors") + .iter() + .find(|entry| entry["id"] == "rhi_evidence_attestation_superseding_002") + .expect("superseding vector"); + let report = parse_rhi_evidence_report( + superseding["expected"]["canonical_event_content_utf8"] + .as_str() + .expect("canonical event content") + .to_owned(), + ) + .expect("superseding report"); + assert_eq!(report.outcome, FfiTradeEvidenceOutcome::Valid); + assert_eq!( + report.supersedes_report_id.as_deref(), + Some("7777777777777777777777777777777777777777777777777777777777777777") + ); + assert_eq!( + report.supersedes_event_id.as_deref(), + Some("8888888888888888888888888888888888888888888888888888888888888888") + ); + } + + #[test] + fn transport_policy_enums_map_every_closed_variant() { + assert_eq!( + FfiCancellationPolicy::PreservePublishedRequest.core(), + Phase1CancellationPolicy::PreservePublishedRequest + ); + assert_eq!( + FfiCancellationPolicy::LocalCooperative.core(), + Phase1CancellationPolicy::LocalCooperative + ); + assert_eq!( + radroots_sdk::transport::BlossomHostKind::from(FfiBlossomHostKind::Native), + radroots_sdk::transport::BlossomHostKind::Native + ); + assert_eq!( + radroots_sdk::transport::BlossomHostKind::from(FfiBlossomHostKind::Simulator), + radroots_sdk::transport::BlossomHostKind::Simulator + ); + assert_eq!( + radroots_sdk::transport::BlossomHostKind::from(FfiBlossomHostKind::PhysicalDevice), + radroots_sdk::transport::BlossomHostKind::PhysicalDevice + ); + } + + #[test] fn evidence_errors_do_not_render_untrusted_content() { let secret = "mobile-private-evidence"; let error = parse_rhi_evidence_report(secret.to_owned()).expect_err("malformed report"); diff --git a/crates/sdk_ffi/src/lib.rs b/crates/sdk_ffi/src/lib.rs @@ -439,10 +439,14 @@ pub mod v1 { #[cfg(test)] mod tests { + use core::num::NonZeroU64; + + use radroots_event::id::TradeId; + use super::v1::{ - CapabilityAvailability, Error, MobileClient, SignedEvent, TradeEvidenceOutcome, - parse_rhi_evidence_report, prepare_rhi_evidence_attestation, - validate_rhi_evidence_attestation, + CapabilityAvailability, Error, MobileClient, SignedEvent, TradeEvidenceCoverage, + TradeEvidenceOutcome, parse_rhi_evidence_report, parse_trade_evidence_manifest, + prepare_rhi_evidence_attestation, validate_rhi_evidence_attestation, }; fn assert_send_sync<T: Send + Sync>() {} @@ -530,6 +534,128 @@ mod tests { } #[test] + fn manifest_supersession_and_closed_evidence_vocabularies_cross_the_ffi() { + use radroots_sdk::trade::{ + RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidenceManifestV1, + RadrootsTradeEvidencePolicyDigestV1, RadrootsTradeEvidenceScopePrerequisitesV1, + RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceSourceIdV1, + RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultDigestV1, + RadrootsTradeEvidenceSourceResultV1, + }; + + for (source, expected) in [ + ( + radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing, + TradeEvidenceCoverage::Missing, + ), + ( + radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial, + TradeEvidenceCoverage::Partial, + ), + ( + radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied, + TradeEvidenceCoverage::ScopeSatisfied, + ), + ( + radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported, + TradeEvidenceCoverage::Unsupported, + ), + ] { + assert_eq!(TradeEvidenceCoverage::from(source), expected); + } + for (source, expected) in [ + ( + radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid, + TradeEvidenceOutcome::Valid, + ), + ( + radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid, + TradeEvidenceOutcome::Invalid, + ), + ( + radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate, + TradeEvidenceOutcome::Indeterminate, + ), + ] { + assert_eq!(TradeEvidenceOutcome::from(source), expected); + } + + let source = RadrootsTradeEvidenceManifestSourceResultV1::new( + RadrootsTradeEvidenceSourceIdV1::parse("typed_source").expect("source id"), + RadrootsTradeEvidenceSourceResultV1::new( + RadrootsTradeEvidenceSourceRequirementV1::Required, + RadrootsTradeEvidenceSourceCompletionV1::Complete, + 0, + ) + .expect("source result"), + RadrootsTradeEvidenceSourceResultDigestV1::from_bytes([0x33; 32]), + ); + let manifest = RadrootsTradeEvidenceManifestV1::new( + TradeId::from_bytes([0x11; 16]), + NonZeroU64::new(1).expect("nonzero generation"), + RadrootsTradeEvidencePolicyDigestV1::from_bytes([0x22; 32]), + 1_800_000_000, + RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied, + [source], + [], + ) + .expect("manifest"); + let projected = parse_trade_evidence_manifest(manifest.canonical_bytes().to_vec()) + .expect("manifest projection"); + assert_eq!(projected.coverage, TradeEvidenceCoverage::ScopeSatisfied); + assert_eq!( + projected.canonical_bytes_hex, + hex::encode(manifest.canonical_bytes()) + ); + assert!(parse_trade_evidence_manifest(Vec::new()).is_err()); + + let decisions: serde_json::Value = serde_json::from_str(include_str!( + "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json" + )) + .expect("RHI decision corpus"); + let superseding = decisions["vectors"] + .as_array() + .expect("RHI decision vectors") + .iter() + .find(|entry| entry["id"] == "rhi_evidence_attestation_superseding_002") + .expect("superseding vector"); + let report = parse_rhi_evidence_report( + superseding["expected"]["canonical_event_content_utf8"] + .as_str() + .expect("canonical event content") + .to_owned(), + ) + .expect("superseding report"); + assert_eq!(report.outcome, TradeEvidenceOutcome::Valid); + let supersession = report.supersession.expect("supersession"); + assert_eq!( + supersession.report_id, + "7777777777777777777777777777777777777777777777777777777777777777" + ); + assert_eq!( + supersession.event_id, + "8888888888888888888888888888888888888888888888888888888888888888" + ); + } + + #[test] + fn evidence_entry_points_classify_pre_plan_and_signed_event_failures() { + assert!(prepare_rhi_evidence_attestation("invalid".to_owned(), 0).is_err()); + assert!( + validate_rhi_evidence_attestation(SignedEvent { + id: String::new(), + author_pubkey: String::new(), + created_at_unix_s: 0, + kind: 0, + tags: Vec::new(), + content: String::new(), + signature: String::new(), + }) + .is_err() + ); + } + + #[test] fn ffi_contract_errors_are_bounded_and_secret_safe() { let secret = "private-report-content"; let error = parse_rhi_evidence_report(secret.to_owned()).expect_err("malformed report"); diff --git a/crates/trade/src/evidence_manifest.rs b/crates/trade/src/evidence_manifest.rs @@ -874,6 +874,55 @@ mod tests { } #[test] + fn manifest_components_expose_exact_typed_evidence() { + let manifest = manifest(); + let source = &manifest.sources()[0]; + let observation = &manifest.observations()[0]; + + assert_eq!(source.source_id().as_str(), "source-a"); + assert_eq!( + source.result().requirement(), + RadrootsTradeEvidenceSourceRequirementV1::Required + ); + assert_eq!( + source.result().completion(), + RadrootsTradeEvidenceSourceCompletionV1::Complete + ); + assert_eq!(source.result().admitted_event_count(), 1); + assert_eq!(source.result_digest().as_bytes(), &[0x41; 32]); + + assert_eq!(observation.source_id().as_str(), "source-a"); + assert_eq!(observation.mutation_id().as_bytes(), &[0x31; 32]); + assert_eq!(observation.event_id().as_bytes(), &[0x51; 32]); + assert_eq!(observation.signed_event_digest().as_bytes(), &[0x61; 32]); + assert_eq!(observation.provenance_digest().as_bytes(), &[0x71; 32]); + assert_eq!(manifest.digest().as_bytes().len(), 32); + + assert_eq!( + RadrootsTradeEvidencePolicyDigestV1::sha256(b"policy").as_bytes(), + RadrootsTradeEvidencePolicyDigestV1::from_bytes(Sha256::digest(b"policy").into()) + .as_bytes() + ); + assert_eq!( + RadrootsTradeEvidenceSourceResultDigestV1::sha256(b"result").as_bytes(), + RadrootsTradeEvidenceSourceResultDigestV1::from_bytes(Sha256::digest(b"result").into()) + .as_bytes() + ); + assert_eq!( + RadrootsTradeSignedEventDigestV1::sha256(b"event").as_bytes(), + RadrootsTradeSignedEventDigestV1::from_bytes(Sha256::digest(b"event").into()) + .as_bytes() + ); + assert_eq!( + RadrootsTradeEvidenceProvenanceDigestV1::sha256(b"provenance").as_bytes(), + RadrootsTradeEvidenceProvenanceDigestV1::from_bytes( + Sha256::digest(b"provenance").into() + ) + .as_bytes() + ); + } + + #[test] fn construction_is_permutation_invariant_and_parser_is_strict() { let first = manifest(); let second = RadrootsTradeEvidenceManifestV1::new( diff --git a/crates/trade/src/evidence_report.rs b/crates/trade/src/evidence_report.rs @@ -754,6 +754,66 @@ mod tests { } #[test] + fn report_accessors_preserve_the_exact_attestation_projection() { + let current = + RadrootsRhiEvidenceReportV1::from_canonical_content(CURRENT_REPORT.as_bytes()) + .expect("current report vector"); + + assert_eq!( + current.contract_id(), + RADROOTS_RHI_EVIDENCE_REPORT_CONTRACT_ID + ); + assert_eq!( + current.contract_version(), + RADROOTS_RHI_EVIDENCE_REPORT_CONTRACT_VERSION + ); + assert_eq!(current.issuer_public_key().to_hex(), "aa".repeat(32)); + assert_eq!(current.trade_id().to_hex(), "11".repeat(16)); + assert_eq!(current.claim_mutation_id().to_hex(), "22".repeat(32)); + assert_eq!( + current.outcome(), + RadrootsTradeEvidenceOutcomeV1::Indeterminate + ); + assert_eq!( + current.reason_codes()[0].as_str(), + "required_source_incomplete" + ); + assert_eq!(current.projection_digest().as_bytes(), &[0x66; 32]); + assert_eq!(current.evidence_manifest_digest().as_bytes(), &[0x44; 32]); + assert_eq!(current.evidence_policy_digest().as_bytes(), &[0x55; 32]); + assert_eq!(current.observed_at_unix_s(), 1_800_000_000); + assert_eq!(current.trade_generation(), NonZeroU64::new(7).unwrap()); + assert_eq!( + current.reducer_contract_id(), + RADROOTS_TRADE_REDUCER_CONTRACT_ID + ); + assert_eq!( + current.reducer_contract_version(), + RADROOTS_TRADE_REDUCER_VERSION + ); + assert_eq!( + current.attestation_method(), + RADROOTS_RHI_EVIDENCE_ATTESTATION_METHOD + ); + assert_eq!(current.statement_digest().as_bytes().len(), 32); + + let superseding = + RadrootsRhiEvidenceReportV1::from_canonical_content(SUPERSEDING_REPORT.as_bytes()) + .expect("superseding report vector"); + let supersession = superseding.supersession().expect("supersession"); + assert_eq!(supersession.report_id().as_bytes(), &[0x77; 32]); + assert_eq!(supersession.event_id().as_bytes(), &[0x88; 32]); + + assert_eq!( + RadrootsTradeEvidenceProjectionDigestV1::sha256(b"projection").as_bytes(), + RadrootsTradeEvidenceProjectionDigestV1::from_bytes( + Sha256::digest(b"projection").into() + ) + .as_bytes() + ); + } + + #[test] fn construction_is_canonical_manifest_bound_and_permutation_invariant() { let manifest = manifest(RadrootsTradeEvidenceCoverageV1::ScopeSatisfied); let a = RadrootsRhiEvidenceReportV1::new( diff --git a/crates/transport_nostr/src/status.rs b/crates/transport_nostr/src/status.rs @@ -858,4 +858,57 @@ mod tests { FetchTargetState::FailedRetryable ); } + + #[test] + fn capability_evidence_rejects_unsupported_and_stale_observations() { + let backoff = ReconnectBackoff::new(10, 40).expect("backoff"); + + let mut unsupported = MutableEvidence::unsupported(); + unsupported.begin(10); + unsupported.record(true, false, 10, backoff); + assert_eq!(unsupported.public.state(), RelayEvidenceState::Unsupported); + assert!(!unsupported.may_attempt(u64::MAX)); + + let mut evidence = MutableEvidence::unobserved(); + evidence.begin(10); + evidence.begin(9); + assert_eq!(evidence.public.last_attempt_unix_ms(), Some(10)); + + evidence.record(false, true, 10, backoff); + evidence.record(true, false, 9, backoff); + assert_eq!(evidence.public.state(), RelayEvidenceState::Unavailable); + assert!(!evidence.may_attempt(19)); + assert!(evidence.may_attempt(20)); + + let (read_only, canonical, writable) = tracker(); + read_only.record_read(&canonical, true, false, 1); + read_only.record_read(&writable, true, false, 1); + let report = read_only.report(); + assert_eq!(report.state(), RelayAggregateState::ReadOnly); + assert_eq!(report.read_availability(), Availability::Available); + assert_eq!(report.write_availability(), Availability::Unavailable); + + let read_only_profile = crate::RelayProfile::explicit( + crate::RelayProfileKind::Public, + [crate::RelayEndpoint::new( + "wss://read-only.example", + crate::RelayUrlPolicy::Public, + crate::RelayAccess::ReadOnly, + ) + .expect("read-only endpoint")], + ) + .expect("read-only profile"); + let read_only_config = Config::from_profile(read_only_profile); + let read_only_relay = read_only_config.relays()[0].clone(); + let read_only_tracker = StatusTracker::new(&read_only_config); + read_only_tracker.record_read(&read_only_relay, true, false, 1); + let report = read_only_tracker.report(); + assert_eq!(report.state(), RelayAggregateState::ReadOnly); + assert_eq!(report.read_availability(), Availability::Available); + assert_eq!(report.write_availability(), Availability::Unavailable); + assert_eq!( + report.relays()[0].write().state(), + RelayEvidenceState::Unsupported + ); + } } diff --git a/tools/xtask/src/service_release_artifacts.rs b/tools/xtask/src/service_release_artifacts.rs @@ -1593,6 +1593,10 @@ fn validate_contract_inner(workspace_root: &Path) -> Result<(), ReleaseArtifactE )?; let decision = serde_json::from_slice::<ReleaseDecision>(&bytes) .map_err(|_| ReleaseArtifactError::InvalidContract)?; + validate_decision(&decision) +} + +fn validate_decision(decision: &ReleaseDecision) -> Result<(), ReleaseArtifactError> { let errors = [ ReleaseArtifactError::InvalidContract, ReleaseArtifactError::InvalidServiceRoot, @@ -1890,6 +1894,37 @@ version = "0.1.0-alpha" } } + fn sample_cargo_metadata() -> CargoMetadata { + let root_id = "path+file:///fixture#fixture-service@0.1.0-alpha"; + let dependency_id = "registry+https://example.invalid#index@0.1.0-alpha"; + CargoMetadata { + packages: vec![ + package(root_id, "fixture-service", None, None, Some("MIT"), true), + package( + dependency_id, + "dependency", + Some("registry+https://github.com/rust-lang/crates.io-index"), + Some(&"a".repeat(64)), + Some("Apache-2.0"), + false, + ), + ], + workspace_members: vec![root_id.to_owned()], + resolve: Some(CargoResolve { + nodes: vec![ + CargoNode { + id: dependency_id.to_owned(), + dependencies: Vec::new(), + }, + CargoNode { + id: root_id.to_owned(), + dependencies: vec![dependency_id.to_owned()], + }, + ], + }), + } + } + #[test] fn contract_matches_the_checked_in_decision() { let root = Path::new(env!("CARGO_MANIFEST_DIR")) @@ -1900,6 +1935,60 @@ version = "0.1.0-alpha" } #[test] + fn contract_rejects_every_independent_governed_field_drift() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("workspace root"); + let bytes = fs::read(root.join(CONTRACT_RELATIVE)).expect("decision"); + let canonical = serde_json::from_slice::<serde_json::Value>(&bytes).expect("decision json"); + for (pointer, replacement) in [ + ("/schema", serde_json::json!("other")), + ("/contract_version", serde_json::json!(2)), + ("/decision_state", serde_json::json!("draft")), + ("/command", serde_json::json!("other")), + ("/modes", serde_json::json!([])), + ("/required_arguments", serde_json::json!([])), + ("/service_metadata_path", serde_json::json!("other")), + ("/service_metadata_fields", serde_json::json!([])), + ("/supported_targets", serde_json::json!([])), + ("/input_inventory", serde_json::json!([])), + ("/excluded_parent_owned_inputs", serde_json::json!([])), + ("/service_root_inventory", serde_json::json!([])), + ("/output_inventory", serde_json::json!([])), + ("/canonical_json", serde_json::json!("other")), + ("/checksum_format", serde_json::json!("other")), + ("/sbom_format", serde_json::json!("other")), + ("/provenance_posture", serde_json::json!("other")), + ("/protected_material_scan_scope", serde_json::json!("other")), + ("/source_cleanliness", serde_json::json!("other")), + ("/revision_stability", serde_json::json!("other")), + ("/no_protected_material", serde_json::json!(false)), + ("/maximums/text_input_bytes", serde_json::json!(1)), + ("/maximums/generated_document_bytes", serde_json::json!(1)), + ("/maximums/service_cargo_lock_bytes", serde_json::json!(1)), + ("/maximums/service_flake_lock_bytes", serde_json::json!(1)), + ("/maximums/binary_bytes", serde_json::json!(1)), + ("/maximums/source_bundle_bytes", serde_json::json!(1)), + ("/maximums/oci_bytes", serde_json::json!(1)), + ("/maximums/cargo_metadata_bytes", serde_json::json!(1)), + ("/maximums/packages", serde_json::json!(1)), + ("/maximums/workspace_packages", serde_json::json!(1)), + ("/negative_error_codes", serde_json::json!([])), + ] { + let mut drifted = canonical.clone(); + *drifted.pointer_mut(pointer).expect("governed field") = replacement; + let decision = serde_json::from_value::<ReleaseDecision>(drifted) + .expect("structurally valid drift"); + assert_eq!( + validate_decision(&decision), + Err(ReleaseArtifactError::InvalidContract), + "accepted drift at {pointer}" + ); + } + } + + #[test] fn exact_inventory_and_limits_are_literal() { assert_eq!(INPUT_NAMES.len(), 8); assert_eq!(OUTPUT_NAMES.len(), 18); @@ -2007,6 +2096,517 @@ version = "0.1.0-alpha" } #[test] + fn package_metadata_rejects_each_independent_field_drift() { + let invalid = |package: CargoPackage, workspace_member| { + assert_eq!( + validate_metadata_package(&package, workspace_member), + Err(ReleaseArtifactError::InvalidPackageInventory) + ); + }; + for field in ["name", "version", "id"] { + for value in [ + String::new(), + "x".repeat(MAX_TEXT_FIELD_BYTES + 1), + "x\ny".into(), + ] { + let mut candidate = + package("root", "fixture-service", None, None, Some("MIT"), true); + match field { + "name" => candidate.name = value, + "version" => candidate.version = value, + "id" => candidate.id = value, + _ => unreachable!(), + } + invalid(candidate, true); + } + } + + for license in [ + Some(""), + Some("bad\nlicense"), + Some("-----BEGIN PRIVATE KEY-----"), + ] { + invalid( + package("root", "fixture-service", None, None, license, true), + true, + ); + } + for source in [ + Some(""), + Some("path+file:///private"), + Some("git+ssh://git@github.com/private/repo"), + ] { + invalid( + package( + "dep", + "dep", + source, + Some(&"a".repeat(64)), + Some("MIT"), + false, + ), + false, + ); + } + invalid( + package( + "dep", + "dep", + None, + Some(&"a".repeat(64)), + Some("MIT"), + false, + ), + false, + ); + invalid( + package( + "dep", + "dep", + Some("registry+https://github.com/rust-lang/crates.io-index"), + Some(&"a".repeat(64)), + None, + false, + ), + false, + ); + for checksum in ["a".repeat(63), "g".repeat(64)] { + invalid( + package( + "root", + "fixture-service", + None, + Some(&checksum), + Some("MIT"), + true, + ), + true, + ); + } + } + + #[test] + fn supply_chain_graph_rejects_each_independent_structural_drift() { + let invalid = |cargo| { + assert!(matches!( + build_supply_chain_documents(&sample_metadata(), cargo), + Err(ReleaseArtifactError::InvalidPackageInventory) + )); + }; + + let mut cargo = sample_cargo_metadata(); + cargo.packages.clear(); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.workspace_members.clear(); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.packages[0].version = "0.2.0".into(); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.packages[0].targets.clear(); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.packages[0].name = "other".into(); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.packages.push(package( + "second-root", + "fixture-service", + None, + None, + Some("MIT"), + true, + )); + cargo.workspace_members.push("second-root".into()); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.packages[1].id = cargo.packages[0].id.clone(); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.workspace_members.push("missing".into()); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.resolve = None; + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.resolve.as_mut().expect("resolve").nodes.clear(); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + let duplicate = cargo.resolve.as_ref().expect("resolve").nodes[0].id.clone(); + cargo.resolve.as_mut().expect("resolve").nodes[1].id = duplicate; + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.resolve.as_mut().expect("resolve").nodes[0].id = "missing".into(); + invalid(cargo); + let mut cargo = sample_cargo_metadata(); + cargo.resolve.as_mut().expect("resolve").nodes[1] + .dependencies + .push("missing".into()); + invalid(cargo); + + let root_id = "path+file:///fixture#fixture-service@0.1.0-alpha"; + let root_only = CargoMetadata { + packages: vec![package( + root_id, + "fixture-service", + None, + None, + Some("MIT"), + true, + )], + workspace_members: vec![root_id.into()], + resolve: Some(CargoResolve { + nodes: vec![CargoNode { + id: root_id.into(), + dependencies: Vec::new(), + }], + }), + }; + let (_, notices) = + build_supply_chain_documents(&sample_metadata(), root_only).expect("root-only graph"); + assert!(notices.contains("No third-party Cargo packages are present.")); + } + + #[test] + fn file_admission_predicates_reject_each_independent_drift() { + let root = TempDir::new().expect("file fixture"); + let regular = root.path().join("regular"); + write_file(&regular, b"bytes"); + let empty = root.path().join("empty"); + write_file(&empty, b""); + let oversized = root.path().join("oversized"); + fs::File::create(&oversized) + .and_then(|file| file.set_len(6)) + .expect("sparse file"); + assert!(matches!( + hash_regular(root.path(), 5), + Err(ReleaseArtifactError::InvalidInputArtifact) + )); + assert!(matches!( + hash_regular(&oversized, 5), + Err(ReleaseArtifactError::InvalidInputArtifact) + )); + assert!(matches!( + validate_regular_input(&empty, 5), + Err(ReleaseArtifactError::InvalidInputArtifact) + )); + assert!(matches!( + validate_regular_input(root.path(), 5), + Err(ReleaseArtifactError::InvalidInputArtifact) + )); + assert!(matches!( + validate_regular_input(&oversized, 5), + Err(ReleaseArtifactError::InvalidInputArtifact) + )); + assert_eq!( + validate_absolute_directory(&regular, ReleaseArtifactError::InvalidInputRoot), + Err(ReleaseArtifactError::InvalidInputRoot) + ); + assert_eq!( + validate_absolute_directory( + Path::new("relative"), + ReleaseArtifactError::InvalidInputRoot + ), + Err(ReleaseArtifactError::InvalidInputRoot) + ); + assert_eq!( + output_maximum("unknown"), + Err(ReleaseArtifactError::GenerationFailure) + ); + + #[cfg(unix)] + { + let symlink = root.path().join("symlink"); + std::os::unix::fs::symlink(&regular, &symlink).expect("symlink"); + assert!(matches!( + hash_regular(&symlink, 5), + Err(ReleaseArtifactError::InvalidInputArtifact) + )); + assert!(matches!( + validate_regular_input(&symlink, 5), + Err(ReleaseArtifactError::InvalidInputArtifact) + )); + assert_eq!( + validate_absolute_directory(&symlink, ReleaseArtifactError::InvalidInputRoot), + Err(ReleaseArtifactError::InvalidInputRoot) + ); + } + } + + #[test] + fn release_metadata_and_text_admission_reject_each_field_drift() { + let root = TempDir::new().expect("metadata fixture"); + let canonical = r#"[workspace.metadata.radroots.service_release] +service = "fixture_service" +service_package = "fixture-service" +binary_name = "fixture-service" +version = "0.1.0-alpha" +"#; + for (from, to) in [ + ("fixture_service", "Fixture"), + ( + "service_package = \"fixture-service\"", + "service_package = \"fixture--service\"", + ), + ( + "binary_name = \"fixture-service\"", + "binary_name = \"fixture--service\"", + ), + ("version = \"0.1.0-alpha\"", "version = \"invalid\""), + ] { + write_file( + &root.path().join("Cargo.toml"), + canonical.replacen(from, to, 1).as_bytes(), + ); + assert!(matches!( + read_release_metadata(root.path()), + Err(ReleaseArtifactError::InvalidServiceMetadata) + )); + } + write_file( + &root.path().join("Cargo.toml"), + canonical + .replacen("0.1.0-alpha", &"a".repeat(129), 1) + .as_bytes(), + ); + assert!(matches!( + read_release_metadata(root.path()), + Err(ReleaseArtifactError::InvalidServiceMetadata) + )); + + for (name, bytes) in [ + ("plain.txt", b"contains\0nul".as_slice()), + ("config.schema.json", b"not json".as_slice()), + ("config.example.toml", b"not = [toml".as_slice()), + ] { + let path = root.path().join(name); + write_file(&path, bytes); + assert_eq!( + validate_text_artifact(&path, name), + Err(ReleaseArtifactError::InvalidInputArtifact) + ); + } + assert_eq!( + write_generated(&root.path().join("empty-generated"), b""), + Err(ReleaseArtifactError::GenerationFailure) + ); + } + + #[test] + fn output_scope_remote_and_inventory_reject_each_drift() { + let root = TempDir::new().expect("output fixture"); + let service = root.path().join("service"); + let input = root.path().join("input"); + let output_parent = root.path().join("output"); + fs::create_dir(&service).expect("service"); + fs::create_dir(&input).expect("input"); + fs::create_dir(&output_parent).expect("output"); + assert_eq!( + validate_output_parent(Path::new("relative"), &service, &input), + Err(ReleaseArtifactError::InvalidOutputRoot) + ); + assert_eq!( + validate_output_parent(root.path(), &service, &input), + Err(ReleaseArtifactError::InvalidOutputRoot) + ); + let file_output = output_parent.join("file"); + write_file(&file_output, b"file"); + assert_eq!( + validate_output_parent(&file_output, &service, &input), + Err(ReleaseArtifactError::InvalidOutputRoot) + ); + + let inventory = root.path().join("inventory"); + fs::create_dir(&inventory).expect("inventory"); + fs::create_dir(inventory.join("directory-entry")).expect("directory entry"); + assert_eq!( + directory_inventory(&inventory, ReleaseArtifactError::InvalidInputRoot), + Err(ReleaseArtifactError::InvalidInputRoot) + ); + fs::remove_dir(inventory.join("directory-entry")).expect("remove entry"); + for index in 0..=OUTPUT_NAMES.len() { + write_file(&inventory.join(format!("file-{index}")), b"x"); + } + assert_eq!( + directory_inventory(&inventory, ReleaseArtifactError::InvalidInputRoot), + Err(ReleaseArtifactError::InvalidInputRoot) + ); + + write_file(&service.join("fixture"), b"fixture"); + initialize_git(&service, "https://example.invalid/service"); + assert_eq!( + git_remote(&service), + Err(ReleaseArtifactError::InvalidServiceRoot) + ); + git( + &service, + &[ + "remote", + "set-url", + "origin", + "ssh://git@github.com/user/repo\nbad", + ], + ); + assert_eq!( + git_remote(&service), + Err(ReleaseArtifactError::InvalidServiceRoot) + ); + git( + &service, + &[ + "remote", + "set-url", + "origin", + &format!("https://github.com/{}", "a".repeat(MAX_TEXT_FIELD_BYTES)), + ], + ); + assert_eq!( + git_remote(&service), + Err(ReleaseArtifactError::InvalidServiceRoot) + ); + } + + #[test] + fn remaining_release_boundaries_fail_closed() { + let fixture = ReleaseFixture::new(); + assert_eq!( + fixture.check(&fixture.output_a), + Err(ReleaseArtifactError::StaleOutput) + ); + + let source_lock = ServiceSourceLockV1::from_canonical_bytes( + &fs::read(fixture.service.join(LOCK_FILENAME)).expect("source lock"), + ) + .expect("source lock"); + fs::write(fixture.service.join("flake.lock"), b"different").expect("flake drift"); + assert_eq!( + validate_source_lock_files(&fixture.service, &source_lock), + Err(ReleaseArtifactError::InvalidSourceLock) + ); + + let mut packages = sample_cargo_metadata(); + packages.packages = (0..=MAX_PACKAGES) + .map(|index| { + package( + &format!("id-{index}"), + "dep", + None, + None, + Some("MIT"), + false, + ) + }) + .collect(); + assert!(matches!( + build_supply_chain_documents(&sample_metadata(), packages), + Err(ReleaseArtifactError::InvalidPackageInventory) + )); + let mut workspace = sample_cargo_metadata(); + workspace.workspace_members = (0..=MAX_WORKSPACE_PACKAGES) + .map(|index| format!("member-{index}")) + .collect(); + assert!(matches!( + build_supply_chain_documents(&sample_metadata(), workspace), + Err(ReleaseArtifactError::InvalidPackageInventory) + )); + for (target_name, kind) in [("other", "bin"), ("fixture-service", "lib")] { + let mut cargo = sample_cargo_metadata(); + cargo.packages[0].targets = vec![CargoTarget { + name: target_name.into(), + kind: vec![kind.into()], + }]; + assert!(matches!( + build_supply_chain_documents(&sample_metadata(), cargo), + Err(ReleaseArtifactError::InvalidPackageInventory) + )); + } + let mut outside_workspace = sample_cargo_metadata(); + outside_workspace.workspace_members = vec!["other".into()]; + assert!(matches!( + build_supply_chain_documents(&sample_metadata(), outside_workspace), + Err(ReleaseArtifactError::InvalidPackageInventory) + )); + + let scope = TempDir::new().expect("scope fixture"); + let service = scope.path().join("service"); + let input_parent = TempDir::new().expect("input scope"); + let input = input_parent.path().join("input"); + fs::create_dir(&service).expect("service"); + fs::create_dir(&input).expect("input"); + assert_eq!( + validate_output_parent(&scope.path().join("bad\\name"), &service, &input), + Err(ReleaseArtifactError::InvalidOutputRoot) + ); + assert_eq!( + validate_output_parent(input_parent.path(), &service, &input), + Err(ReleaseArtifactError::InvalidOutputRoot) + ); + #[cfg(unix)] + { + let foreign = scope.path().join("foreign"); + fs::create_dir(&foreign).expect("foreign"); + let output = scope.path().join("output-link"); + std::os::unix::fs::symlink(&foreign, &output).expect("output symlink"); + assert_eq!( + validate_output_parent(&output, &service, &input), + Err(ReleaseArtifactError::InvalidOutputRoot) + ); + } + + write_file(&service.join("tracked"), b"tracked"); + initialize_git(&service, "https://github.com/radrootslabs/service"); + let child = service.join("child"); + fs::create_dir(&child).expect("child"); + assert_eq!( + validate_git_root(&child), + Err(ReleaseArtifactError::InvalidServiceRoot) + ); + write_file(&service.join("untracked"), b"dirty"); + assert_eq!( + validate_clean_git(&service), + Err(ReleaseArtifactError::DirtyServiceSource) + ); + + assert_eq!( + verify_bundle( + &fixture.input.join("service-source.bundle"), + &"a".repeat(40) + ), + Err(ReleaseArtifactError::InvalidSourceBundle) + ); + assert_eq!( + write_generated( + &scope.path().join("oversized-generated"), + &vec![b'x'; MAX_GENERATED_DOCUMENT_BYTES as usize + 1] + ), + Err(ReleaseArtifactError::GenerationFailure) + ); + } + + #[test] + fn identifier_predicates_reject_each_independent_boundary() { + for value in ["", "1service", "service_", "service__name", "service-name"] { + assert!(!valid_snake_identifier(value), "{value}"); + } + assert!(!valid_snake_identifier(&"a".repeat(129))); + for value in ["", "1service", "service-", "service--name", "service_name"] { + assert!(!valid_kebab_identifier(value), "{value}"); + } + assert!(!valid_kebab_identifier(&"a".repeat(129))); + assert!(!valid_metadata_text("")); + assert!(!valid_metadata_text(&"a".repeat(MAX_TEXT_FIELD_BYTES + 1))); + assert!(!valid_metadata_text("bad\rvalue")); + assert!(!valid_metadata_text("-----BEGIN PRIVATE KEY-----")); + assert!(!valid_public_source("registry+http://example.invalid")); + assert!(!valid_public_source( + "registry+https://user@github.com/private" + )); + assert!(!valid_lower_hex("a", 2)); + assert!(!valid_lower_hex("ag", 2)); + } + + #[test] fn binary_archive_is_reproducible_and_metadata_is_fixed() { let root = TempDir::new().expect("archive fixture"); let source = root.path().join("service"); diff --git a/tools/xtask/src/service_source_lock_command.rs b/tools/xtask/src/service_source_lock_command.rs @@ -1344,6 +1344,288 @@ radroots_service_host = {{ git = "{LIB_REPOSITORY}", rev = "{revision}", version } #[test] + fn flake_lock_rejects_each_independent_fixed_field_drift() { + let fixture = Fixture::new(); + let bytes = fs::read(fixture.service.join(FLAKE_LOCK)).expect("flake lock"); + let canonical = serde_json::from_slice::<serde_json::Value>(&bytes).expect("flake json"); + for (pointer, replacement) in [ + ("/version", serde_json::json!(6)), + ("/nodes/root/inputs/lib", serde_json::json!("other")), + ("/nodes/lib/locked/type", serde_json::json!("git")), + ("/nodes/lib/locked/owner", serde_json::json!("other")), + ("/nodes/lib/locked/repo", serde_json::json!("other")), + ("/nodes/lib/locked/rev", serde_json::json!("other")), + ( + "/nodes/lib/locked/narHash", + serde_json::json!("sha256-invalid"), + ), + ("/nodes/lib/original/type", serde_json::json!("git")), + ("/nodes/lib/original/owner", serde_json::json!("other")), + ("/nodes/lib/original/repo", serde_json::json!("other")), + ("/nodes/lib/original/rev", serde_json::json!("other")), + ] { + let mut drifted = canonical.clone(); + *drifted.pointer_mut(pointer).expect("governed field") = replacement; + assert_eq!( + validate_flake_lock( + &serde_json::to_vec(&drifted).expect("flake json"), + &fixture.revision + ), + Err(CommandError::InvalidFlakeLock), + "accepted drift at {pointer}" + ); + } + + for (section, field) in [ + ("locked", "extra"), + ("original", "extra"), + ("original", "ref"), + ] { + let mut drifted = canonical.clone(); + drifted["nodes"]["lib"][section] + .as_object_mut() + .expect("flake section") + .insert(field.into(), serde_json::json!("unexpected")); + assert_eq!( + validate_flake_lock( + &serde_json::to_vec(&drifted).expect("flake json"), + &fixture.revision + ), + Err(CommandError::InvalidFlakeLock), + "accepted {section}.{field}" + ); + } + + let mut duplicate = canonical.clone(); + duplicate["nodes"]["lib2"] = duplicate["nodes"]["lib"].clone(); + assert_eq!( + validate_flake_lock( + &serde_json::to_vec(&duplicate).expect("flake json"), + &fixture.revision + ), + Err(CommandError::InvalidFlakeLock) + ); + } + + #[test] + fn service_metadata_and_catalog_reject_independent_field_drift() { + let fixture = Fixture::new(); + let bytes = fs::read(fixture.service.join(CARGO_MANIFEST)).expect("manifest"); + let canonical = parse_toml(&bytes, CommandError::InvalidCargoManifest).expect("manifest"); + for (field, replacement) in [ + ("service", toml::Value::Integer(1)), + ("host_feature_profile", toml::Value::String("other".into())), + ("config_contract_version", toml::Value::Integer(0)), + ("state_contract_version", toml::Value::Integer(0)), + ("admin_contract_version", toml::Value::Integer(0)), + ("status_contract_version", toml::Value::Integer(0)), + ("provider_contract_version", toml::Value::Integer(0)), + ] { + let mut drifted = canonical.clone(); + drifted["workspace"]["metadata"]["radroots"]["service_source_lock"][field] = + replacement; + assert_eq!( + parse_service_metadata(&drifted), + Err(CommandError::InvalidServiceMetadata), + "accepted metadata drift at {field}" + ); + } + let mut extra = canonical; + extra["workspace"]["metadata"]["radroots"]["service_source_lock"] + .as_table_mut() + .expect("metadata table") + .insert("extra".into(), toml::Value::Integer(1)); + assert_eq!( + parse_service_metadata(&extra), + Err(CommandError::InvalidServiceMetadata) + ); + + let catalog = br#"schema = "radroots.workspace.catalog.v2" +architecture = "radroots.crates.release.v2" +version = "0.1.0-alpha" +package_count = 2 + +[[package]] +name = "radroots_core" + +[[package]] +name = "radroots_service_host" +"#; + let canonical = parse_toml(catalog, CommandError::InvalidSourceArchive).expect("catalog"); + for (field, replacement) in [ + ("schema", toml::Value::String("other".into())), + ("architecture", toml::Value::String("other".into())), + ("version", toml::Value::String("other".into())), + ("package_count", toml::Value::Integer(1)), + ] { + let mut drifted = canonical.clone(); + drifted[field] = replacement; + assert_eq!( + catalog_package_names(toml::to_string(&drifted).expect("catalog").as_bytes()), + Err(CommandError::InvalidSourceArchive), + "accepted catalog drift at {field}" + ); + } + let mut duplicate = canonical.clone(); + duplicate["package"][1]["name"] = toml::Value::String("radroots_core".into()); + assert_eq!( + catalog_package_names(toml::to_string(&duplicate).expect("catalog").as_bytes()), + Err(CommandError::InvalidSourceArchive) + ); + let mut missing_host = canonical; + missing_host["package"][1]["name"] = toml::Value::String("radroots_other".into()); + assert_eq!( + catalog_package_names(toml::to_string(&missing_host).expect("catalog").as_bytes()), + Err(CommandError::InvalidSourceArchive) + ); + } + + #[test] + fn manifest_and_cargo_lock_reject_each_independent_dependency_drift() { + let revision = "a".repeat(40); + let canonical = format!( + "[dependencies]\nradroots_service_host = {{ git = \"{LIB_REPOSITORY}\", rev = \"{revision}\", version = \"{LIB_VERSION_REQUIREMENT}\" }}\n" + ); + for (from, to) in [ + (LIB_REPOSITORY, "https://example.invalid/lib"), + (LIB_VERSION_REQUIREMENT, "=0.2.0"), + (revision.as_str(), "invalid"), + ] { + let value = + toml::from_str::<toml::Value>(&canonical.replacen(from, to, 1)).expect("manifest"); + assert_eq!( + validate_manifest_node( + &value, + None, + false, + false, + None, + &mut ManifestState::default() + ), + Err(CommandError::InvalidCargoManifest) + ); + } + for forbidden in ["branch", "tag", "path"] { + let mutated = canonical.replacen( + "version = \"=0.1.0-alpha\"", + &format!("version = \"=0.1.0-alpha\", {forbidden} = \"forbidden\""), + 1, + ); + let value = toml::from_str::<toml::Value>(&mutated).expect("manifest"); + assert_eq!( + validate_manifest_node( + &value, + None, + false, + false, + None, + &mut ManifestState::default() + ), + Err(CommandError::InvalidCargoManifest), + "accepted {forbidden}" + ); + } + let value = toml::from_str::<toml::Value>(&canonical).expect("manifest"); + assert_eq!( + validate_manifest_node( + &value, + None, + true, + false, + None, + &mut ManifestState::default() + ), + Err(CommandError::InvalidCargoManifest) + ); + let mut state = ManifestState { + revision: Some("b".repeat(40)), + ..ManifestState::default() + }; + assert_eq!( + validate_manifest_node(&value, None, false, false, None, &mut state), + Err(CommandError::InvalidCargoManifest) + ); + + let packages = BTreeSet::from([HOST_PACKAGE.to_owned(), "radroots_core".to_owned()]); + let expected = format!("git+{LIB_REPOSITORY}?rev={revision}#{revision}"); + for document in [ + format!( + "version = 4\n\n[[package]]\nname = \"{HOST_PACKAGE}\"\nversion = \"0.1.0-alpha\"\n" + ), + format!( + "version = 4\n\n[[package]]\nname = \"{HOST_PACKAGE}\"\nversion = \"0.1.0-alpha\"\nsource = \"other\"\n" + ), + format!( + "version = 4\n\n[[package]]\nname = \"{HOST_PACKAGE}\"\nversion = \"0.2.0\"\nsource = \"{expected}\"\n" + ), + format!( + "version = 4\n\n[[package]]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha\"\nsource = \"{expected}\"\n" + ), + "version = 4\n".to_owned(), + ] { + assert_eq!( + validate_cargo_lock(document.as_bytes(), &revision, &packages), + Err(CommandError::InvalidCargoLock) + ); + } + } + + #[test] + fn archive_and_manifest_filesystem_admission_is_fail_closed() { + let root = TempDir::new().expect("filesystem fixture"); + let empty = root.path().join("empty"); + fs::write(&empty, b"").expect("empty file"); + assert!(matches!( + validate_archive(Path::new("relative"), &"a".repeat(40)), + Err(CommandError::InvalidSourceArchive) + )); + assert!(matches!( + validate_archive(root.path(), &"a".repeat(40)), + Err(CommandError::InvalidSourceArchive) + )); + assert!(matches!( + validate_archive(&empty, &"a".repeat(40)), + Err(CommandError::InvalidSourceArchive) + )); + + let no_manifest = root.path().join("no-manifest"); + fs::create_dir(&no_manifest).expect("empty tree"); + assert_eq!( + validate_cargo_manifests(&no_manifest, None), + Err(CommandError::InvalidCargoManifest) + ); + let no_dependency = root.path().join("no-dependency"); + fs::create_dir(&no_dependency).expect("manifest tree"); + fs::write( + no_dependency.join(CARGO_MANIFEST), + "[workspace]\nresolver = \"3\"\n", + ) + .expect("manifest"); + assert_eq!( + validate_cargo_manifests(&no_dependency, None), + Err(CommandError::InvalidCargoManifest) + ); + + #[cfg(unix)] + { + let archive_link = root.path().join("archive-link"); + std::os::unix::fs::symlink(&empty, &archive_link).expect("archive symlink"); + assert!(matches!( + validate_archive(&archive_link, &"a".repeat(40)), + Err(CommandError::InvalidSourceArchive) + )); + let manifest_link_root = root.path().join("manifest-link"); + fs::create_dir(&manifest_link_root).expect("manifest symlink tree"); + std::os::unix::fs::symlink(&empty, manifest_link_root.join(CARGO_MANIFEST)) + .expect("manifest symlink"); + assert_eq!( + validate_cargo_manifests(&manifest_link_root, None), + Err(CommandError::InvalidCargoManifest) + ); + } + } + + #[test] fn source_lock_output_rejects_symlink_replacement() { let fixture = Fixture::new(); let target = fixture.root.path().join("foreign");