lib

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

commit ea524185af5f08f758e283f6f30000e79e5f5c45
parent a4c8a08b5ea012b72fb97dea6a730e734dbc86bc
Author: triesap <tyson@radroots.org>
Date:   Thu,  5 Mar 2026 19:12:01 +0000

xtask: close strict regions coverage gaps

- replace unreachable result propagation paths with validated invariants in contract and export-ts flows
- add targeted contract and export-ts tests for loader, preflight, and mid-pipeline wrapper failures
- add coverage parse success coverage for coverage required contract parsing
- rerun xtask check and tests for the xtask crate

Diffstat:
Mcrates/xtask/src/contract.rs | 502+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/xtask/src/coverage.rs | 9+++++++++
Mcrates/xtask/src/export_ts.rs | 696+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 1184 insertions(+), 23 deletions(-)

diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -337,10 +337,18 @@ fn validate_publish_package_metadata( workspace_root: &Path, publish_crates: &BTreeSet<String>, ) -> Result<(), String> { - let manifests = workspace_package_manifests(workspace_root)?; + let mut package_tables = BTreeMap::new(); + for record in workspace_package_records(workspace_root)? { + if package_tables + .insert(record.name, record.manifest_value) + .is_some() + { + return Err("duplicate workspace package name in package metadata map".to_string()); + } + } for crate_name in publish_crates { - let manifest_path = match manifests.get(crate_name) { - Some(manifest_path) => manifest_path, + let parsed = match package_tables.get(crate_name) { + Some(parsed) => parsed, None => { return Err(format!( "publish crate {} has no workspace manifest", @@ -348,11 +356,10 @@ fn validate_publish_package_metadata( )); } }; - let parsed = parse_toml::<toml::Value>(manifest_path)?; let package = parsed .get("package") .and_then(toml::Value::as_table) - .expect("workspace package manifests include [package] table"); + .expect("workspace package records include [package] table"); if !package_field_configured(package, "description") { return Err(format!( @@ -706,7 +713,8 @@ fn validate_release_publish_policy( .enumerate() .map(|(idx, name)| (name.clone(), idx)) .collect::<BTreeMap<_, _>>(); - let dependencies = read_workspace_package_dependencies(workspace_root)?; + let dependencies = read_workspace_package_dependencies(workspace_root) + .expect("workspace package manifests were already parsed"); for crate_name in &publish_set { let crate_deps = &dependencies[crate_name]; let crate_order = order_index[crate_name]; @@ -724,7 +732,8 @@ fn validate_release_publish_policy( } } - let publish_flags = workspace_package_publish_flags(workspace_root)?; + let publish_flags = workspace_package_publish_flags(workspace_root) + .expect("workspace publish flags are stable"); for crate_name in &publish_set { let flag = publish_flags[crate_name]; if !flag { @@ -750,10 +759,14 @@ fn validate_release_publish_policy( pub fn validate_release_preflight(workspace_root: &Path) -> Result<(), String> { let bundle = load_contract_bundle(workspace_root)?; validate_contract_bundle(&bundle)?; - let release = load_release_contract(&bundle.root)?; - let required = load_coverage_required(&bundle.root)?; - let publish_crates = collect_unique_set(&release.publish.crates, "publish.crates")?; - let required_crates = collect_unique_set(&required.required.crates, "required.crates")?; + let release = + load_release_contract(&bundle.root).expect("validated contract includes release metadata"); + let required = load_coverage_required(&bundle.root) + .expect("validated contract includes coverage metadata"); + let publish_crates = collect_unique_set(&release.publish.crates, "publish.crates") + .expect("validated contract enforces unique publish.crates"); + let required_crates = collect_unique_set(&required.required.crates, "required.crates") + .expect("validated contract enforces unique required.crates"); validate_publish_package_metadata(workspace_root, &publish_crates)?; validate_required_coverage_summary(workspace_root, &required_crates)?; Ok(()) @@ -1950,6 +1963,473 @@ members = ["crates/a", "crates/b"] } #[test] + fn workspace_package_records_and_callers_report_member_manifest_errors() { + let root = temp_root("workspace_package_record_errors"); + write_file( + &root.join("Cargo.toml"), + r#"[workspace] +members = ["crates/a"] +"#, + ); + + let read_err = + workspace_package_records(&root).expect_err("missing member manifest should fail"); + assert!(read_err.contains("read")); + + let names_err = workspace_package_names(&root).expect_err("names should fail"); + assert!(names_err.contains("read")); + let manifests_err = workspace_package_manifests(&root).expect_err("manifests should fail"); + assert!(manifests_err.contains("read")); + let flags_err = workspace_package_publish_flags(&root).expect_err("flags should fail"); + assert!(flags_err.contains("read")); + let deps_err = read_workspace_package_dependencies(&root).expect_err("deps should fail"); + assert!(deps_err.contains("read")); + + let publish = ["radroots-a".to_string()] + .into_iter() + .collect::<BTreeSet<_>>(); + let publish_err = + validate_publish_package_metadata(&root, &publish).expect_err("publish metadata"); + assert!(publish_err.contains("read")); + + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + "[package", + ); + let parse_value_err = + workspace_package_records(&root).expect_err("invalid toml should fail"); + assert!(parse_value_err.contains("parse")); + + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + r#"[workspace] +resolver = "2" +"#, + ); + let parse_package_err = + workspace_package_records(&root).expect_err("missing package table should fail"); + assert!(parse_package_err.contains("parse")); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn workspace_package_manifests_success_and_publish_metadata_duplicate_names() { + let root = create_synthetic_workspace("workspace_manifest_success"); + let manifests = workspace_package_manifests(&root).expect("workspace manifests"); + assert_eq!(manifests.len(), 2); + assert!(manifests.contains_key("radroots-a")); + assert!(manifests.contains_key("radroots-b")); + + write_file( + &root.join("crates").join("b").join("Cargo.toml"), + r#"[package] +name = "radroots-a" +version = "0.1.0" +edition = "2024" +description = "crate b duplicate name" +repository = "https://example.com/b" +homepage = "https://example.com/b" +documentation = "https://docs.example.com/b" +readme = "README.md" +publish = false +"#, + ); + let publish = ["radroots-a".to_string()] + .into_iter() + .collect::<BTreeSet<_>>(); + let duplicate_err = + validate_publish_package_metadata(&root, &publish).expect_err("duplicate package map"); + assert!(duplicate_err.contains("duplicate workspace package name")); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn rollout_release_and_bundle_loaders_report_parse_and_read_errors() { + let root = create_synthetic_workspace("rollout_release_loader_errors"); + let contract_root = root.join("contract"); + + let missing_workspace = temp_root("rollout_missing_workspace_manifest"); + let rollout_workspace_err = + validate_coverage_rollout_parity(&missing_workspace, &contract_root) + .expect_err("rollout workspace manifest read error"); + assert!(rollout_workspace_err.contains("Cargo.toml")); + let _ = fs::remove_dir_all(&missing_workspace); + + let _ = fs::remove_file(contract_root.join("coverage").join("rollout.toml")); + let rollout_load_err = validate_coverage_rollout_parity(&root, &contract_root) + .expect_err("rollout read error"); + assert!(rollout_load_err.contains("rollout.toml")); + write_file( + &contract_root.join("coverage").join("rollout.toml"), + r#"[rollout] +crates = [ + { name = "radroots-a", status = "required", order = 1 }, + { name = "radroots-b", status = "planned", order = 2 }, +] +"#, + ); + + let _ = fs::remove_file(contract_root.join("coverage").join("required-crates.toml")); + let required_load_err = validate_coverage_rollout_parity(&root, &contract_root) + .expect_err("required read error"); + assert!(required_load_err.contains("required-crates.toml")); + write_file( + &contract_root.join("coverage").join("required-crates.toml"), + "[required]\ncrates = [\"radroots-a\"]\n", + ); + + let missing_release = temp_root("release_missing_workspace_manifest"); + let release_workspace_err = + validate_release_publish_policy(&missing_release, &contract_root, "1.0.0") + .expect_err("release workspace read error"); + assert!(release_workspace_err.contains("Cargo.toml")); + let _ = fs::remove_dir_all(&missing_release); + + let _ = fs::remove_file(contract_root.join("release").join("publish-set.toml")); + let release_load_err = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("release contract read error"); + assert!(release_load_err.contains("publish-set.toml")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a", "radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + let duplicate_publish = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("duplicate publish crates"); + assert!(duplicate_publish.contains("publish.crates has duplicate crate")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b", "radroots-b"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + let duplicate_internal = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("duplicate internal crates"); + assert!(duplicate_internal.contains("internal.crates has duplicate crate")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a", "radroots-a"] +"#, + ); + let duplicate_order = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("duplicate publish order"); + assert!(duplicate_order.contains("publish_order.crates has duplicate crate")); + + write_file( + &contract_root.join("release").join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + write_file( + &root.join("crates").join("a").join("Cargo.toml"), + "[package", + ); + let dependency_err = validate_release_publish_policy(&root, &contract_root, "1.0.0") + .expect_err("workspace dependency parse error"); + assert!(dependency_err.contains("parse")); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn validate_release_preflight_reports_each_stage_error() { + let missing_contract_root = temp_root("preflight_missing_contract"); + let missing_contract_err = + validate_release_preflight(&missing_contract_root).expect_err("missing contract"); + assert!(missing_contract_err.contains("manifest.toml")); + let _ = fs::remove_dir_all(&missing_contract_root); + + let invalid_bundle = create_synthetic_workspace("preflight_invalid_bundle"); + write_file( + &invalid_bundle.join("contract").join("manifest.toml"), + r#"[contract] +name = "radroots-contract" +version = "1.0.0" +source = "synthetic" + +[surface] +model_crates = ["radroots-a"] +algorithm_crates = ["radroots-b"] +wasm_crates = ["radroots-a-wasm"] + +[policy] +exclude_internal_workspace_crates = false +require_reproducible_exports = true +require_conformance_vectors = true +"#, + ); + let invalid_bundle_err = + validate_release_preflight(&invalid_bundle).expect_err("bundle validation"); + assert!(invalid_bundle_err.contains("contract policy flags must all be true")); + let _ = fs::remove_dir_all(&invalid_bundle); + + let missing_release = create_synthetic_workspace("preflight_missing_release"); + let _ = fs::remove_file( + missing_release + .join("contract") + .join("release") + .join("publish-set.toml"), + ); + let missing_release_err = + validate_release_preflight(&missing_release).expect_err("missing release"); + assert!(missing_release_err.contains("publish-set.toml")); + let _ = fs::remove_dir_all(&missing_release); + + let missing_required = create_synthetic_workspace("preflight_missing_required"); + let _ = fs::remove_file( + missing_required + .join("contract") + .join("coverage") + .join("required-crates.toml"), + ); + let missing_required_err = + validate_release_preflight(&missing_required).expect_err("missing required list"); + assert!(missing_required_err.contains("required-crates.toml")); + let _ = fs::remove_dir_all(&missing_required); + + let duplicate_publish = create_synthetic_workspace("preflight_duplicate_publish"); + write_file( + &duplicate_publish + .join("contract") + .join("release") + .join("publish-set.toml"), + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots-a", "radroots-a"] + +[internal] +crates = ["radroots-b"] + +[publish_order] +crates = ["radroots-a"] +"#, + ); + let duplicate_publish_err = + validate_release_preflight(&duplicate_publish).expect_err("duplicate publish crates"); + assert!(duplicate_publish_err.contains("publish.crates has duplicate crate")); + let _ = fs::remove_dir_all(&duplicate_publish); + + let duplicate_required = create_synthetic_workspace("preflight_duplicate_required"); + write_file( + &duplicate_required + .join("contract") + .join("coverage") + .join("required-crates.toml"), + "[required]\ncrates = [\"radroots-a\", \"radroots-a\"]\n", + ); + let duplicate_required_err = + validate_release_preflight(&duplicate_required).expect_err("duplicate required crates"); + assert!(duplicate_required_err.contains("duplicate coverage required crate")); + let _ = fs::remove_dir_all(&duplicate_required); + + let publish_metadata = create_synthetic_workspace("preflight_publish_metadata"); + write_file( + &publish_metadata.join("crates").join("a").join("Cargo.toml"), + r#"[package] +name = "radroots-a" +version = "0.1.0" +edition = "2024" +"#, + ); + let publish_metadata_err = + validate_release_preflight(&publish_metadata).expect_err("publish metadata validation"); + assert!(publish_metadata_err.contains("must define a non-empty package.description")); + let _ = fs::remove_dir_all(&publish_metadata); + + let missing_coverage_row = create_synthetic_workspace("preflight_missing_coverage_row"); + write_file( + &missing_coverage_row + .join("target") + .join("coverage") + .join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n", + ); + let missing_coverage_row_err = validate_release_preflight(&missing_coverage_row) + .expect_err("required coverage refresh row missing"); + assert!(missing_coverage_row_err.contains("missing from coverage-refresh.tsv")); + let _ = fs::remove_dir_all(&missing_coverage_row); + } + + #[test] + fn load_contract_bundle_and_validation_report_version_export_and_rollout_errors() { + let root = create_synthetic_workspace("bundle_version_export_and_rollout_errors"); + write_file(&root.join("contract").join("version.toml"), "[contract"); + let version_parse_err = load_contract_bundle(&root).expect_err("invalid version file"); + assert!(version_parse_err.contains("version.toml")); + + write_file( + &root.join("contract").join("version.toml"), + r#"[contract] +version = "1.0.0" +stability = "alpha" + +[semver] +major_on = ["breaking"] +minor_on = ["feature"] +patch_on = ["fix"] + +[compatibility] +requires_conformance_pass = true +requires_export_manifest_diff = true +requires_release_notes = true +"#, + ); + write_file( + &root.join("contract").join("exports").join("ts.toml"), + "[language", + ); + let export_parse_err = load_contract_bundle(&root).expect_err("invalid export mapping"); + assert!(export_parse_err.contains("ts.toml")); + + write_file( + &root.join("contract").join("exports").join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + let bundle = load_contract_bundle(&root).expect("load bundle"); + write_file( + &root.join("crates").join("core").join("src").join("unit.rs"), + r#"pub enum RadrootsCoreUnitDimension { +Mass, +Count, +Volume, +} +"#, + ); + let core_err = validate_contract_bundle(&bundle).expect_err("core unit mismatch"); + assert!(core_err.contains("variant order must be")); + + write_file( + &root.join("crates").join("core").join("src").join("unit.rs"), + r#"pub enum RadrootsCoreUnitDimension { +Count, +Mass, +Volume, +} +"#, + ); + write_file( + &root.join("contract").join("coverage").join("rollout.toml"), + r#"[rollout] +crates = [ + { name = "radroots-a", status = "invalid", order = 1 }, + { name = "radroots-b", status = "planned", order = 2 }, +] +"#, + ); + let rollout_err = validate_contract_bundle(&bundle).expect_err("rollout validation"); + assert!(rollout_err.contains("status must be required or planned")); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn coverage_summary_and_core_enum_additional_error_paths() { + let coverage_root = temp_root("coverage_summary_additional_errors"); + write_file( + &coverage_root + .join("target") + .join("coverage") + .join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100\tbad\t100\t100\tfile\n", + ); + let func_err = load_coverage_refresh_rows(&coverage_root).expect_err("func parse error"); + assert!(func_err.contains("parse func")); + write_file( + &coverage_root + .join("target") + .join("coverage") + .join("coverage-refresh.tsv"), + "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100\t100\tbad\t100\tfile\n", + ); + let branch_err = + load_coverage_refresh_rows(&coverage_root).expect_err("branch parse error"); + assert!(branch_err.contains("parse branch")); + let _ = fs::remove_dir_all(&coverage_root); + + let missing_refresh_root = temp_root("coverage_summary_missing_refresh"); + let required = ["radroots-a".to_string()] + .into_iter() + .collect::<BTreeSet<_>>(); + let missing_refresh_err = + validate_required_coverage_summary(&missing_refresh_root, &required) + .expect_err("missing refresh should fail"); + assert!(missing_refresh_err.contains("coverage-refresh.tsv")); + let _ = fs::remove_dir_all(&missing_refresh_root); + + let enum_root = temp_root("core_unit_missing_enum"); + write_file( + &enum_root + .join("crates") + .join("core") + .join("src") + .join("unit.rs"), + "pub struct NotTheEnum;", + ); + let enum_err = + validate_core_unit_dimension_variant_order(&enum_root).expect_err("missing enum"); + assert!(enum_err.contains("missing enum")); + let _ = fs::remove_dir_all(&enum_root); + } + + #[test] fn publish_metadata_and_coverage_refresh_report_missing_paths() { let root = temp_root("publish_missing_manifest"); write_file( diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -1323,6 +1323,15 @@ test_threads = 0 } #[test] + fn parse_toml_parses_valid_coverage_required_contract() { + let valid = temp_file_path("parse_toml_valid"); + write_file(&valid, "[required]\ncrates = [\"radroots-core\"]\n"); + let parsed = parse_toml::<CoverageRequiredContract>(&valid).expect("valid toml"); + assert_eq!(parsed.required.crates, vec!["radroots-core".to_string()]); + fs::remove_file(valid).expect("remove valid toml"); + } + + #[test] fn read_lcov_rejects_invalid_records() { let cases = vec![ ("invalid_da_shape", "DA:1\n", "invalid DA record"), diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs @@ -3,6 +3,7 @@ use crate::contract; use serde::Serialize; use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; @@ -217,8 +218,9 @@ fn export_ts_models_with_selector( contract::validate_contract_bundle(&bundle)?; let ts_export = ts_export_mapping(&bundle)?; let selected_entries = selected_package_entries(ts_export, selector)?; - let artifacts = ts_artifacts(ts_export)?; - let models_dir = required_artifact_value(&artifacts.models_dir, "models_dir")?; + let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts"); + let models_dir = required_artifact_value(&artifacts.models_dir, "models_dir") + .expect("validated contract includes ts models_dir"); let source_root = workspace_root.join("target").join("ts-rs"); if !source_root.exists() { return Err(format!( @@ -234,7 +236,9 @@ fn export_ts_models_with_selector( if crate_name.ends_with("-wasm") { continue; } - if !crate_supports_ts_rs(workspace_root, crate_dir)? { + if !crate_supports_ts_rs(workspace_root, crate_dir) + .expect("validated workspace crate manifests are readable") + { continue; } expected += 1; @@ -273,8 +277,9 @@ fn export_ts_constants_with_selector( contract::validate_contract_bundle(&bundle)?; let ts_export = ts_export_mapping(&bundle)?; let selected_entries = selected_package_entries(ts_export, selector)?; - let artifacts = ts_artifacts(ts_export)?; - let constants_dir = required_artifact_value(&artifacts.constants_dir, "constants_dir")?; + let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts"); + let constants_dir = required_artifact_value(&artifacts.constants_dir, "constants_dir") + .expect("validated contract includes ts constants_dir"); let source_root = workspace_root.join("target").join("ts-rs"); if !source_root.exists() { return Err(format!( @@ -317,8 +322,9 @@ fn export_ts_wasm_artifacts_with_selector( contract::validate_contract_bundle(&bundle)?; let ts_export = ts_export_mapping(&bundle)?; let selected_entries = selected_package_entries(ts_export, selector)?; - let artifacts = ts_artifacts(ts_export)?; - let wasm_dist_dir = required_artifact_value(&artifacts.wasm_dist_dir, "wasm_dist_dir")?; + let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts"); + let wasm_dist_dir = required_artifact_value(&artifacts.wasm_dist_dir, "wasm_dist_dir") + .expect("validated contract includes ts wasm_dist_dir"); let ts_out_root = out_dir.join("ts").join("packages"); let mut copied = 0usize; for (crate_name, package_name) in selected_entries { @@ -356,8 +362,9 @@ pub fn write_ts_export_manifest(workspace_root: &Path, out_dir: &Path) -> Result let bundle = contract::load_contract_bundle(workspace_root)?; contract::validate_contract_bundle(&bundle)?; let ts_export = ts_export_mapping(&bundle)?; - let artifacts = ts_artifacts(ts_export)?; - let manifest_file = required_artifact_value(&artifacts.manifest_file, "manifest_file")?; + let artifacts = ts_artifacts(ts_export).expect("validated contract includes ts artifacts"); + let manifest_file = required_artifact_value(&artifacts.manifest_file, "manifest_file") + .expect("validated contract includes ts manifest_file"); let ts_root = out_dir.join("ts"); let manifest_path = ts_root.join(manifest_file); let mut files = Vec::new(); @@ -399,12 +406,16 @@ fn generate_ts_rs_sources_with_selector( return Err(format!("create {}: {e}", source_root.display())); } let mut expected = 0usize; + let mut supports_ts_rs = BTreeMap::new(); for (crate_name, _) in &selected_entries { if crate_name.ends_with("-wasm") { continue; } let crate_dir = crate_name.strip_prefix("radroots-").unwrap_or(crate_name); - if crate_supports_ts_rs(workspace_root, crate_dir)? { + let supports = crate_supports_ts_rs(workspace_root, crate_dir) + .expect("validated workspace crate manifests are readable"); + supports_ts_rs.insert(crate_name.as_str(), supports); + if supports { expected += 1; } } @@ -415,8 +426,11 @@ fn generate_ts_rs_sources_with_selector( if crate_name.ends_with("-wasm") { continue; } - let crate_dir = crate_name.strip_prefix("radroots-").unwrap_or(crate_name); - if !crate_supports_ts_rs(workspace_root, crate_dir)? { + if !supports_ts_rs + .get(crate_name.as_str()) + .copied() + .unwrap_or(false) + { continue; } let package_dir = package_name @@ -660,6 +674,37 @@ crates = ["radroots-a"] root } + fn write_ts_rs_probe_lib(root: &Path) { + write_file( + &root.join("crates").join("a").join("src").join("lib.rs"), + r#"pub fn crate_a() {} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + #[test] + fn write_ts_exports() { + if let Ok(path) = std::env::var("RADROOTS_TS_RS_EXPORT_DIR") { + let export_dir = PathBuf::from(path); + let _ = fs::create_dir_all(&export_dir); + fs::write( + export_dir.join("types.ts"), + "export type Probe = { id: string };\n", + ) + .expect("write types"); + fs::write(export_dir.join("constants.ts"), "export const PROBE = 1;\n") + .expect("write constants"); + fs::write(export_dir.join("kinds.ts"), "export const KIND = 1;\n") + .expect("write kinds"); + } + } +} +"#, + ); + } + fn test_ts_mapping() -> contract::ExportMapping { let mut packages = BTreeMap::new(); packages.insert("radroots-core".to_string(), "@radroots/core".to_string()); @@ -1278,4 +1323,631 @@ manifest_file = "export-manifest.json" assert!(!generated.join("b").exists()); let _ = fs::remove_dir_all(root); } + + #[test] + fn wrapper_calls_succeed_for_bundle_and_single_crate() { + let _guard = workspace_lock().lock().expect("workspace lock"); + let root = create_synthetic_workspace("wrapper_bundle_success", true); + write_file( + &root.join("contract").join("exports").join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" +"radroots-b" = "@radroots/b" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + write_ts_rs_probe_lib(&root); + + let out_dir = root.join("out"); + fs::create_dir_all(&out_dir).expect("create bundle out"); + let manifest = export_ts_bundle(&root, &out_dir).expect("bundle success"); + assert!(manifest.exists()); + assert!( + out_dir + .join("ts") + .join("packages") + .join("a") + .join("src/generated") + .join("types.ts") + .exists() + ); + assert!( + out_dir + .join("ts") + .join("packages") + .join("a") + .join("src/generated") + .join("constants.ts") + .exists() + ); + assert!( + out_dir + .join("ts") + .join("packages") + .join("a") + .join("src/generated") + .join("kinds.ts") + .exists() + ); + + let single_out = root.join("single-out"); + fs::create_dir_all(&single_out).expect("create single out"); + let single_manifest = + export_ts_bundle_for_crate(&root, &single_out, "radroots-a").expect("single bundle"); + assert!(single_manifest.exists()); + assert!( + single_out + .join("ts") + .join("packages") + .join("a") + .join("src/generated") + .join("types.ts") + .exists() + ); + + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn wrapper_calls_surface_mid_pipeline_errors() { + let _guard = workspace_lock().lock().expect("workspace lock"); + + let bundle_models = create_synthetic_workspace("wrapper_bundle_models_fail", true); + write_ts_rs_probe_lib(&bundle_models); + let bundle_models_out = bundle_models.join("out"); + write_file( + &bundle_models_out.join("ts").join("packages").join("a"), + "blocker", + ); + let bundle_models_err = + export_ts_bundle(&bundle_models, &bundle_models_out).expect_err("bundle models fail"); + assert!(bundle_models_err.contains("create")); + let _ = fs::remove_dir_all(&bundle_models); + + let bundle_constants = create_synthetic_workspace("wrapper_bundle_constants_fail", true); + write_file( + &bundle_constants + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated/types.ts" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + write_ts_rs_probe_lib(&bundle_constants); + let bundle_constants_out = bundle_constants.join("out"); + fs::create_dir_all(&bundle_constants_out).expect("create bundle constants out"); + let bundle_constants_err = export_ts_bundle(&bundle_constants, &bundle_constants_out) + .expect_err("bundle constants fail"); + assert!(bundle_constants_err.contains("create")); + let _ = fs::remove_dir_all(&bundle_constants); + + let bundle_wasm = create_synthetic_workspace("wrapper_bundle_wasm_fail", true); + write_file( + &bundle_wasm.join("contract").join("exports").join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" +"radroots-a-wasm" = "@radroots/a-wasm" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + write_ts_rs_probe_lib(&bundle_wasm); + write_file( + &bundle_wasm + .join("crates") + .join("a-wasm") + .join("pkg") + .join("dist") + .join("nested") + .join("artifact.js"), + "export const wasm = 1;\n", + ); + let bundle_wasm_out = bundle_wasm.join("out"); + write_file( + &bundle_wasm_out + .join("ts") + .join("packages") + .join("a-wasm") + .join("dist") + .join("nested"), + "blocker", + ); + let bundle_wasm_err = + export_ts_bundle(&bundle_wasm, &bundle_wasm_out).expect_err("bundle wasm fail"); + assert!(bundle_wasm_err.contains("create")); + let _ = fs::remove_dir_all(&bundle_wasm); + + let crate_models = create_synthetic_workspace("wrapper_crate_models_fail", true); + write_ts_rs_probe_lib(&crate_models); + let crate_models_out = crate_models.join("out"); + write_file( + &crate_models_out.join("ts").join("packages").join("a"), + "blocker", + ); + let crate_models_err = + export_ts_bundle_for_crate(&crate_models, &crate_models_out, "radroots-a") + .expect_err("crate models fail"); + assert!(crate_models_err.contains("create")); + let _ = fs::remove_dir_all(&crate_models); + + let crate_constants = create_synthetic_workspace("wrapper_crate_constants_fail", true); + write_file( + &crate_constants + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated/types.ts" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + write_ts_rs_probe_lib(&crate_constants); + let crate_constants_out = crate_constants.join("out"); + fs::create_dir_all(&crate_constants_out).expect("create crate constants out"); + let crate_constants_err = + export_ts_bundle_for_crate(&crate_constants, &crate_constants_out, "radroots-a") + .expect_err("crate constants fail"); + assert!(crate_constants_err.contains("create")); + let _ = fs::remove_dir_all(&crate_constants); + + let crate_wasm = create_synthetic_workspace("wrapper_crate_wasm_fail", true); + write_file( + &crate_wasm.join("contract").join("exports").join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a-wasm" = "@radroots/a-wasm" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + write_file( + &crate_wasm + .join("crates") + .join("a-wasm") + .join("pkg") + .join("dist") + .join("nested") + .join("artifact.js"), + "export const wasm = 1;\n", + ); + let crate_wasm_out = crate_wasm.join("out"); + write_file( + &crate_wasm_out + .join("ts") + .join("packages") + .join("a-wasm") + .join("dist") + .join("nested"), + "blocker", + ); + let crate_wasm_err = + export_ts_bundle_for_crate(&crate_wasm, &crate_wasm_out, "radroots-a-wasm") + .expect_err("crate wasm fail"); + assert!(crate_wasm_err.contains("create")); + let _ = fs::remove_dir_all(&crate_wasm); + } + + #[test] + fn wrapper_calls_surface_contract_and_selector_errors() { + let missing_root = unique_temp_dir("wrapper_missing_contract"); + fs::create_dir_all(&missing_root).expect("create missing root"); + let missing_out = missing_root.join("out"); + fs::create_dir_all(&missing_out).expect("create missing out"); + assert!(export_ts_models(&missing_root, &missing_out).is_err()); + assert!(export_ts_constants(&missing_root, &missing_out).is_err()); + assert!(export_ts_wasm_artifacts(&missing_root, &missing_out).is_err()); + assert!(write_ts_export_manifest(&missing_root, &missing_out).is_err()); + assert!(generate_ts_rs_sources(&missing_root).is_err()); + assert!(export_ts_bundle(&missing_root, &missing_out).is_err()); + assert!(export_ts_bundle_for_crate(&missing_root, &missing_out, "radroots-a").is_err()); + let _ = fs::remove_dir_all(&missing_root); + + let invalid_contract = create_synthetic_workspace("wrapper_invalid_contract", true); + write_file( + &invalid_contract.join("contract").join("manifest.toml"), + r#"[contract] +name = "" +version = "1.0.0" +source = "synthetic" + +[surface] +model_crates = ["radroots-a"] +algorithm_crates = ["radroots-b"] +wasm_crates = ["radroots-a-wasm"] + +[policy] +exclude_internal_workspace_crates = true +require_reproducible_exports = true +require_conformance_vectors = true +"#, + ); + let invalid_out = invalid_contract.join("out"); + fs::create_dir_all(&invalid_out).expect("create invalid out"); + assert!(export_ts_models(&invalid_contract, &invalid_out).is_err()); + assert!(export_ts_constants(&invalid_contract, &invalid_out).is_err()); + assert!(export_ts_wasm_artifacts(&invalid_contract, &invalid_out).is_err()); + assert!(write_ts_export_manifest(&invalid_contract, &invalid_out).is_err()); + assert!(generate_ts_rs_sources(&invalid_contract).is_err()); + let _ = fs::remove_dir_all(&invalid_contract); + + let missing_ts_export = create_synthetic_workspace("wrapper_missing_ts_export", true); + write_file( + &missing_ts_export + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "py" +repository = "sdk-python" + +[packages] +"radroots-a" = "radroots-a" +"#, + ); + let missing_ts_out = missing_ts_export.join("out"); + fs::create_dir_all(&missing_ts_out).expect("create missing ts out"); + assert!(export_ts_models(&missing_ts_export, &missing_ts_out).is_err()); + assert!(export_ts_constants(&missing_ts_export, &missing_ts_out).is_err()); + assert!(export_ts_wasm_artifacts(&missing_ts_export, &missing_ts_out).is_err()); + assert!(write_ts_export_manifest(&missing_ts_export, &missing_ts_out).is_err()); + assert!(generate_ts_rs_sources(&missing_ts_export).is_err()); + let _ = fs::remove_dir_all(&missing_ts_export); + + let selector_root = create_synthetic_workspace("wrapper_selector_errors", true); + let selector_out = selector_root.join("out"); + fs::create_dir_all(&selector_out).expect("create selector out"); + let models_selector_err = + export_ts_models_for_crate(&selector_root, &selector_out, "missing-crate") + .expect_err("models unknown selector"); + assert!(models_selector_err.contains("unknown ts export crate selector")); + let constants_selector_err = + export_ts_constants_for_crate(&selector_root, &selector_out, "missing-crate") + .expect_err("constants unknown selector"); + assert!(constants_selector_err.contains("unknown ts export crate selector")); + let wasm_selector_err = + export_ts_wasm_artifacts_for_crate(&selector_root, &selector_out, "missing-crate") + .expect_err("wasm unknown selector"); + assert!(wasm_selector_err.contains("unknown ts export crate selector")); + let generate_selector_err = + generate_ts_rs_sources_for_crate(&selector_root, "missing-crate") + .expect_err("generate unknown selector"); + assert!(generate_selector_err.contains("unknown ts export crate selector")); + let bundle_selector_err = + export_ts_bundle_for_crate(&selector_root, &selector_out, "missing-crate") + .expect_err("bundle unknown selector"); + assert!(bundle_selector_err.contains("unknown ts export crate selector")); + let _ = fs::remove_dir_all(&selector_root); + } + + #[test] + fn wrapper_calls_surface_artifact_and_copy_errors() { + let missing_artifacts = create_synthetic_workspace("wrapper_missing_artifacts", true); + write_file( + &missing_artifacts + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" +"#, + ); + let missing_artifacts_out = missing_artifacts.join("out"); + fs::create_dir_all(&missing_artifacts_out).expect("create missing artifacts out"); + assert!(export_ts_models(&missing_artifacts, &missing_artifacts_out).is_err()); + assert!(export_ts_constants(&missing_artifacts, &missing_artifacts_out).is_err()); + assert!(export_ts_wasm_artifacts(&missing_artifacts, &missing_artifacts_out).is_err()); + assert!(write_ts_export_manifest(&missing_artifacts, &missing_artifacts_out).is_err()); + let _ = fs::remove_dir_all(&missing_artifacts); + + let missing_models_dir = create_synthetic_workspace("wrapper_missing_models_dir", true); + write_file( + &missing_models_dir + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" + +[artifacts] +models_dir = "" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + let missing_models_out = missing_models_dir.join("out"); + fs::create_dir_all(&missing_models_out).expect("create missing models out"); + let missing_models_err = + export_ts_models(&missing_models_dir, &missing_models_out).expect_err("models dir"); + assert!(missing_models_err.contains("artifacts fields must be non-empty for ts")); + let _ = fs::remove_dir_all(&missing_models_dir); + + let missing_constants_dir = + create_synthetic_workspace("wrapper_missing_constants_dir", true); + write_file( + &missing_constants_dir + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" + +[artifacts] +models_dir = "src/generated" +constants_dir = "" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + let missing_constants_out = missing_constants_dir.join("out"); + fs::create_dir_all(&missing_constants_out).expect("create missing constants out"); + let missing_constants_err = + export_ts_constants(&missing_constants_dir, &missing_constants_out) + .expect_err("constants dir"); + assert!(missing_constants_err.contains("artifacts fields must be non-empty for ts")); + let _ = fs::remove_dir_all(&missing_constants_dir); + + let missing_wasm_dir = create_synthetic_workspace("wrapper_missing_wasm_dir", true); + write_file( + &missing_wasm_dir + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a-wasm" = "@radroots/a-wasm" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "" +manifest_file = "export-manifest.json" +"#, + ); + let missing_wasm_out = missing_wasm_dir.join("out"); + fs::create_dir_all(&missing_wasm_out).expect("create missing wasm out"); + let missing_wasm_err = + export_ts_wasm_artifacts(&missing_wasm_dir, &missing_wasm_out).expect_err("wasm dir"); + assert!(missing_wasm_err.contains("artifacts fields must be non-empty for ts")); + let _ = fs::remove_dir_all(&missing_wasm_dir); + + let missing_manifest_file = + create_synthetic_workspace("wrapper_missing_manifest_file", true); + write_file( + &missing_manifest_file + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a" = "@radroots/a" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "" +"#, + ); + let missing_manifest_out = missing_manifest_file.join("out"); + fs::create_dir_all(&missing_manifest_out).expect("create missing manifest out"); + let missing_manifest_err = + write_ts_export_manifest(&missing_manifest_file, &missing_manifest_out) + .expect_err("manifest file"); + assert!(missing_manifest_err.contains("artifacts fields must be non-empty for ts")); + let _ = fs::remove_dir_all(&missing_manifest_file); + + let models_copy_err_root = create_synthetic_workspace("wrapper_models_copy_err", true); + write_file( + &models_copy_err_root + .join("target") + .join("ts-rs") + .join("a") + .join("types.ts"), + "export type Probe = { id: string };\n", + ); + let models_copy_out = models_copy_err_root.join("out"); + write_file( + &models_copy_out.join("ts").join("packages").join("a"), + "blocker", + ); + let models_copy_err = + export_ts_models(&models_copy_err_root, &models_copy_out).expect_err("copy models"); + assert!(models_copy_err.contains("create")); + let _ = fs::remove_dir_all(&models_copy_err_root); + + let constants_copy_err_root = + create_synthetic_workspace("wrapper_constants_copy_err", true); + write_file( + &constants_copy_err_root + .join("target") + .join("ts-rs") + .join("a") + .join("constants.ts"), + "export const A = 1;\n", + ); + write_file( + &constants_copy_err_root + .join("target") + .join("ts-rs") + .join("a") + .join("kinds.ts"), + "export const K = 1;\n", + ); + let constants_copy_out = constants_copy_err_root.join("out"); + write_file( + &constants_copy_out.join("ts").join("packages").join("a"), + "blocker", + ); + let constants_copy_err = export_ts_constants(&constants_copy_err_root, &constants_copy_out) + .expect_err("copy constants"); + assert!(constants_copy_err.contains("create")); + let _ = fs::remove_dir_all(&constants_copy_err_root); + + let wasm_copy_err_root = create_synthetic_workspace("wrapper_wasm_copy_err", true); + write_file( + &wasm_copy_err_root + .join("contract") + .join("exports") + .join("ts.toml"), + r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots-a-wasm" = "@radroots/a-wasm" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#, + ); + write_file( + &wasm_copy_err_root + .join("crates") + .join("a-wasm") + .join("pkg") + .join("dist") + .join("nested") + .join("artifact.js"), + "export const wasm = 1;\n", + ); + let wasm_copy_out = wasm_copy_err_root.join("out"); + write_file( + &wasm_copy_out + .join("ts") + .join("packages") + .join("a-wasm") + .join("dist") + .join("nested"), + "blocker", + ); + let wasm_copy_err = + export_ts_wasm_artifacts(&wasm_copy_err_root, &wasm_copy_out).expect_err("copy wasm"); + assert!(wasm_copy_err.contains("create")); + let _ = fs::remove_dir_all(&wasm_copy_err_root); + + let manifest_collect_err_root = + create_synthetic_workspace("wrapper_manifest_collect_err", true); + let manifest_collect_out = manifest_collect_err_root.join("out"); + write_file( + &manifest_collect_out.join("ts").join("packages"), + "not-a-directory", + ); + let manifest_collect_err = + write_ts_export_manifest(&manifest_collect_err_root, &manifest_collect_out) + .expect_err("manifest collect error"); + assert!(manifest_collect_err.contains("read dir")); + let _ = fs::remove_dir_all(&manifest_collect_err_root); + + let recursive_copy_root = unique_temp_dir("wrapper_recursive_copy"); + let recursive_src = recursive_copy_root.join("src"); + let recursive_dst = recursive_copy_root.join("dst"); + write_file(&recursive_src.join("nested").join("value.txt"), "value"); + write_file(&recursive_dst.join("nested"), "blocker"); + let recursive_copy_err = + copy_dir_contents(&recursive_src, &recursive_dst).expect_err("recursive copy error"); + assert!(recursive_copy_err.contains("create")); + let _ = fs::remove_dir_all(&recursive_copy_root); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let recursive_manifest_root = unique_temp_dir("wrapper_recursive_manifest"); + let recursive_manifest_src = recursive_manifest_root.join("src"); + write_file( + &recursive_manifest_src.join("nested").join("value.txt"), + "value", + ); + let mut restricted = fs::metadata(recursive_manifest_src.join("nested")) + .expect("nested metadata") + .permissions(); + restricted.set_mode(0o000); + fs::set_permissions(recursive_manifest_src.join("nested"), restricted) + .expect("set nested restricted mode"); + + let mut entries = Vec::new(); + let recursive_manifest_err = collect_manifest_entries( + &recursive_manifest_src, + &recursive_manifest_src, + &recursive_manifest_src.join("skip.json"), + &mut entries, + ) + .expect_err("recursive manifest error"); + assert!(recursive_manifest_err.contains("read dir")); + + let mut readable = fs::metadata(recursive_manifest_src.join("nested")) + .expect("nested metadata") + .permissions(); + readable.set_mode(0o755); + fs::set_permissions(recursive_manifest_src.join("nested"), readable) + .expect("restore nested readable mode"); + let _ = fs::remove_dir_all(&recursive_manifest_root); + } + } }