diff --git a/.config/nextest.toml b/.config/nextest.toml index 9f2dd7da5d9..509aac70129 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -20,13 +20,15 @@ test-threads = 4 # These fixtures own temporary state and loopback endpoints. Live tests stay opt-in. # Selecting the fake ssh relay's ignored test also archives nemoclaw-test-fixtures' # executables beside the extracted tests: the relay, which fake engines need on -# platforms without Unix sockets, the SSH simulator, and the fixture provider. +# platforms without Unix sockets, and the SSH simulator. The fixture provider's +# ignored test does the same for the provider protocol tests. default-filter = ''' test(native_state_json_preserves_module_and_instance_identity_without_refresh) | (package(nemoclaw-sdk) & test(/^deployment::runtime::helm_recovery::tests::/)) | (package(nemoclaw-sdk) & test(runtime_export_declares_the_native_helm_provider_inputs)) | ((package(nemoclaw-provider) | package(openshell-provider) | package(fabric-provider)) & binary(=contract)) | (package(nemoclaw-test-fixtures) & binary(=ssh_relay)) +| (package(nemoclaw-fixture-provider) & binary(=archive)) | (package(nemoclaw-e2e) & binary(=integration) & ( test(/^sandbox_readiness::/) | test(/^deployment::/) | test(/^export_observations::/) | test(/^fabric_deployment::/) diff --git a/Cargo.lock b/Cargo.lock index 78912a4929f..5e817e873d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2638,6 +2638,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "nemoclaw-fixture-provider" +version = "0.1.0" +dependencies = [ + "async-trait", + "nemoclaw-runtime", + "nemoclaw-sdk", + "nemoclaw-test-fixtures", + "nemoclaw-tofu", + "rustls", + "serde_json", + "tf-provider", + "tokio", +] + [[package]] name = "nemoclaw-ollama-proxy" version = "0.1.0" @@ -2807,18 +2822,13 @@ dependencies = [ name = "nemoclaw-test-fixtures" version = "0.1.0" dependencies = [ - "async-trait", "http", - "nemoclaw-runtime", - "nemoclaw-sdk", - "nemoclaw-tofu", "openshell-core", "prost", "rustls", "serde_json", "tar", "tempfile", - "tf-provider", "tokio", "tokio-stream", "tonic", diff --git a/Cargo.toml b/Cargo.toml index 71490116490..76d13e5c298 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/nemoclaw-provider", "crates/nemoclaw-e2e", "crates/nemoclaw-test-fixtures", + "crates/nemoclaw-fixture-provider", "crates/nemoclaw-cli", "crates/nemoclaw-runtime", "crates/nemoclaw-build", diff --git a/crates/fabric-provider/tests/contract/capabilities.rs b/crates/fabric-provider/tests/contract/capabilities.rs index e5cad03a9c9..992ddfe0676 100644 --- a/crates/fabric-provider/tests/contract/capabilities.rs +++ b/crates/fabric-provider/tests/contract/capabilities.rs @@ -84,7 +84,7 @@ output "observation" {{ value = data.fabric_capabilities.agent.observation_json /// an explicit read policy that omits the runtime's paths, is unsupported, and /// a missing image is unknown; none of these fail apply or change the engine. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; fake Docker engine"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; fake Docker engine"] async fn capabilities_judge_the_image_for_the_configuration() { let digest = format!("registry/agent@sha256:{}", "a".repeat(64)); let engine = engine(digest.clone()).await; @@ -100,7 +100,8 @@ async fn capabilities_judge_the_image_for_the_configuration() { ), ("missing:image", "aarch64", runtime_reads, "unknown"), ] { - let workspace = TofuWorkspace::with_providers(tofu(), &[("fabric", &provider("fabric"))]); + let workspace = + TofuWorkspace::with_providers(Bundle::from_env().tofu(), &[("fabric", &fabric())]); fs::write( workspace.path().join("main.tf"), configuration(image, architecture, reads), diff --git a/crates/fabric-provider/tests/contract/main.rs b/crates/fabric-provider/tests/contract/main.rs index 8257cfba805..69fdc6ec686 100644 --- a/crates/fabric-provider/tests/contract/main.rs +++ b/crates/fabric-provider/tests/contract/main.rs @@ -12,7 +12,7 @@ mod capabilities; mod http; mod readiness; -use nemoclaw_test_fixtures::{executable, openshell::Fixture, tofu::TofuWorkspace}; +use nemoclaw_test_fixtures::{Bundle, openshell::Fixture, tofu::TofuWorkspace}; use serde_json::{Value, json}; use std::{ collections::BTreeSet, @@ -23,34 +23,20 @@ use std::{ const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/contract/fixtures"); -fn tofu() -> PathBuf { - let tofu = PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_TOFU").expect("explicit pinned OpenTofu path"), - ); - assert!(tofu.is_absolute()); - tofu -} - -/// A provider built beside NEMOCLAW_TEST_PROVIDER, as the lifecycle tests -/// receive it. -fn provider(name: &str) -> PathBuf { - let nemoclaw = PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_PROVIDER").expect("explicit provider build path"), - ); - assert!(nemoclaw.is_absolute()); - nemoclaw - .parent() - .unwrap() - .join(executable(&format!("terraform-provider-{name}"))) +/// The fabric provider Cargo built from this checkout, the one under test. +fn fabric() -> PathBuf { + nemoclaw_test_fixtures::package_executable!("terraform-provider-fabric") } -/// A workspace with the openshell and fabric providers. +/// A workspace with the fabric provider under test and the bundle's openshell +/// provider. fn workspace() -> TofuWorkspace { + let bundle = Bundle::from_env(); TofuWorkspace::with_providers( - tofu(), + bundle.tofu(), &[ - ("openshell", &provider("openshell")), - ("fabric", &provider("fabric")), + ("openshell", &bundle.provider("openshell")), + ("fabric", &fabric()), ], ) } @@ -132,7 +118,7 @@ fn every_fabric_type_has_a_contract_test() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; fake OpenShell gateway"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; fake OpenShell gateway"] async fn pi_configuration_updates_without_replacing_the_sandbox() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint, &[]); diff --git a/crates/fabric-provider/tests/contract/readiness.rs b/crates/fabric-provider/tests/contract/readiness.rs index b9d47f91918..4abe714e9a5 100644 --- a/crates/fabric-provider/tests/contract/readiness.rs +++ b/crates/fabric-provider/tests/contract/readiness.rs @@ -10,7 +10,7 @@ fn output(tofu: &Standalone, name: &str) -> Value { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; fake OpenShell gateway"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; fake OpenShell gateway"] async fn readiness_reports_the_configured_agent_and_its_failed_health() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint, &["readiness.tf"]); diff --git a/crates/nemoclaw-build/src/run_ci.rs b/crates/nemoclaw-build/src/run_ci.rs index 194a268eeb0..8a48ff29ee2 100644 --- a/crates/nemoclaw-build/src/run_ci.rs +++ b/crates/nemoclaw-build/src/run_ci.rs @@ -248,18 +248,9 @@ fn run_step( )?; command.env("PATH", path); } - command - .env("NEMOCLAW_TEST_BUNDLE", &bundle) - .env( - "NEMOCLAW_TEST_TOFU", - bundle - .join("libexec") - .join(nemoclaw_build_executable("tofu")), - ) - .env( - "NEMOCLAW_TEST_PROVIDER", - std::path::absolute(tool("terraform-provider-nemoclaw"))?, - ); + // The bundle supplies pinned OpenTofu and the providers; each + // provider's contract tests use the one Cargo built with them. + command.env("NEMOCLAW_TEST_BUNDLE", &bundle); } run(&mut command)?; } diff --git a/crates/nemoclaw-build/src/run_ci/archive.rs b/crates/nemoclaw-build/src/run_ci/archive.rs index 16b1f1db0b7..19eaaa13102 100644 --- a/crates/nemoclaw-build/src/run_ci/archive.rs +++ b/crates/nemoclaw-build/src/run_ci/archive.rs @@ -20,18 +20,12 @@ pub(super) fn package_inputs(tools: &Tools<'_>) -> Result<()> { std::env::current_exe()?, Path::new(".build/ci").join(nemoclaw_build_executable("nemoclaw-build")), )?; - // Protocol fixtures locate sibling providers beside this explicit binary, - // and the lifecycle step puts the fake ssh first on PATH where Unix - // sockets are missing. - for name in [ - "terraform-provider-nemoclaw", - "terraform-provider-openshell", - "terraform-provider-fabric", - "nemoclaw-fixture-ssh", - ] { - let path = Path::new("target/debug").join(nemoclaw_build_executable(name)); - archive.append_path(&path)?; - } + // The lifecycle step puts the fake ssh first on PATH where Unix sockets + // are missing. The providers need no copy here: the bundle ships them, and + // nextest archives each provider package's executables with its tests. + archive.append_path( + Path::new("target/debug").join(nemoclaw_build_executable("nemoclaw-fixture-ssh")), + )?; archive.finish()?; Ok(()) } diff --git a/crates/nemoclaw-build/tests/ci_lifecycle.rs b/crates/nemoclaw-build/tests/ci_lifecycle.rs index 32c065a07a1..5879f8338fb 100644 --- a/crates/nemoclaw-build/tests/ci_lifecycle.rs +++ b/crates/nemoclaw-build/tests/ci_lifecycle.rs @@ -35,7 +35,7 @@ if [ "$1 $2" = 'nextest list' ]; then printf '%s\n' "$CARGO_LIST" exit "${CARGO_RESULT:-0}" fi -printf '%s\n' "$NEMOCLAW_TEST_BUNDLE" "$NEMOCLAW_TEST_TOFU" "$NEMOCLAW_TEST_PROVIDER" > inputs +printf '%s\n' "$NEMOCLAW_TEST_BUNDLE" "${NEMOCLAW_TEST_TOFU-unset}" "${NEMOCLAW_TEST_PROVIDER-unset}" > inputs exit "${CARGO_RESULT:-0}" "#, ), @@ -63,6 +63,8 @@ exit "${CARGO_RESULT:-0}" .env("PROTOC", self.0.path().join("bin/protoc")) .env("PATH", self.0.path().join("bin")) .env("TEST_PLATFORM", "linux_arm64") + .env_remove("NEMOCLAW_TEST_TOFU") + .env_remove("NEMOCLAW_TEST_PROVIDER") .env_remove("GITHUB_ENV") .env_remove("GITHUB_PATH"); command @@ -92,12 +94,14 @@ fn lifecycle_partition_reaches_nextest_without_changing_the_selected_suite() { args.contains("--profile\nlifecycle\n--run-ignored\nonly\n"), "{args}" ); + // The bundle supplies OpenTofu and the providers; each provider's own + // tests use the one Cargo built. let inputs = fs::read_to_string(fixture.0.path().join("inputs")).unwrap(); - assert!(inputs.contains("dist/linux_arm64/libexec/tofu"), "{inputs}"); - assert!( - inputs.contains("target/debug/terraform-provider-nemoclaw"), - "{inputs}" - ); + let inputs: Vec<_> = inputs.lines().collect(); + assert_eq!(inputs.len(), 3, "{inputs:?}"); + assert!(inputs[0].ends_with("/dist/linux_arm64"), "{inputs:?}"); + assert!(std::path::Path::new(inputs[0]).is_absolute(), "{inputs:?}"); + assert_eq!(inputs[1..], ["unset", "unset"]); } #[test] @@ -164,7 +168,7 @@ fn lifecycle_options_cannot_silently_narrow_default_or_other_ci_steps() { } #[test] -fn archive_packages_tools_and_provider_helpers_but_leaves_the_bundle_to_its_own_job() { +fn archive_packages_tools_and_the_ssh_relay_but_leaves_providers_to_the_bundle_and_nextest() { let fixture = Fixture::new(); for path in [ "dist/linux_arm64/bin/nemoclaw", @@ -202,11 +206,13 @@ fn archive_packages_tools_and_provider_helpers_but_leaves_the_bundle_to_its_own_ tar::Archive::new(packed).unpack(unpacked.path()).unwrap(); // The bundle job uploads dist/PLATFORM separately, in parallel with this build. assert!(!unpacked.path().join("dist").exists()); + // Nextest archives each provider package's executables with its tests. + for name in ["nemoclaw", "openshell", "fabric"] { + let path = format!("target/debug/terraform-provider-{name}"); + assert!(!unpacked.path().join(&path).exists(), "{path}"); + } for path in [ ".tools/nextest-0.9.144/cargo-nextest", - "target/debug/terraform-provider-nemoclaw", - "target/debug/terraform-provider-openshell", - "target/debug/terraform-provider-fabric", "target/debug/nemoclaw-fixture-ssh", ] { let file = unpacked.path().join(path); diff --git a/crates/nemoclaw-e2e/tests/managed.rs b/crates/nemoclaw-e2e/tests/managed.rs index 18481a65990..b3c864d0f15 100644 --- a/crates/nemoclaw-e2e/tests/managed.rs +++ b/crates/nemoclaw-e2e/tests/managed.rs @@ -5,13 +5,12 @@ use serde_json::{Value, json}; use std::{fs, path::PathBuf}; #[test] -#[ignore = "requires explicit retained runtime state, OpenTofu and production provider paths; reads owned storage only"] +#[ignore = "requires explicit retained runtime state and NEMOCLAW_TEST_BUNDLE; reads owned storage only"] fn provider_refreshes_retained_storage_without_changes() { let path = |name| PathBuf::from(std::env::var_os(name).expect("explicit qualification path")); - let tofu = path("NEMOCLAW_TEST_TOFU"); - let provider = path("NEMOCLAW_TEST_PROVIDER"); + let bundle = nemoclaw_test_fixtures::Bundle::from_env(); let retained = path("NEMOCLAW_TEST_RUNTIME_STATE"); - assert!(tofu.is_absolute() && provider.is_absolute() && retained.is_absolute()); + assert!(retained.is_absolute()); let original = fs::read(&retained).unwrap(); let mut state: Value = serde_json::from_slice(&original).unwrap(); state["resources"].as_array_mut().unwrap().retain(|r| { @@ -26,7 +25,7 @@ fn provider_refreshes_retained_storage_without_changes() { 2, "requires the two retained storage resources" ); - let directory = TofuWorkspace::new(tofu, provider); + let directory = TofuWorkspace::new(&bundle, bundle.provider("nemoclaw")); let mut graph = json!({"terraform":{"required_providers":{"nemoclaw":{"source":"registry.opentofu.org/nvidia/nemoclaw","version":"0.1.0"}}},"provider":{"nemoclaw":{}},"resource":{}}); for resource in resources { assert_eq!(resource["instances"].as_array().unwrap().len(), 1); diff --git a/crates/nemoclaw-e2e/tests/sandbox_readiness.rs b/crates/nemoclaw-e2e/tests/sandbox_readiness.rs index 9546cdeb626..3f630c9c22c 100644 --- a/crates/nemoclaw-e2e/tests/sandbox_readiness.rs +++ b/crates/nemoclaw-e2e/tests/sandbox_readiness.rs @@ -3,14 +3,13 @@ use nemoclaw_e2e::{openshell::Fixture, tofu::TofuWorkspace}; use nemoclaw_sdk::{compile, config::Document}; use serde_json::{Value, json}; -use std::{fs, path::PathBuf}; +use std::fs; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated gateway fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated gateway fixture"] async fn standalone_sandbox_completion_rejects_unknown_health_and_retains_bindings() { - let tofu = PathBuf::from(std::env::var_os("NEMOCLAW_TEST_TOFU").unwrap()); - let provider = PathBuf::from(std::env::var_os("NEMOCLAW_TEST_PROVIDER").unwrap()); - let directory = TofuWorkspace::new(tofu, provider); + let bundle = nemoclaw_test_fixtures::Bundle::from_env(); + let directory = TofuWorkspace::new(&bundle, bundle.provider("nemoclaw")); let root = directory.path(); let fixture = Fixture::start().await; let mut document = Document::parse( diff --git a/crates/nemoclaw-fixture-provider/Cargo.toml b/crates/nemoclaw-fixture-provider/Cargo.toml new file mode 100644 index 00000000000..15a08a815dc --- /dev/null +++ b/crates/nemoclaw-fixture-provider/Cargo.toml @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# A fixture nemoclaw provider for the provider protocol tests. It serves SDK +# resource definitions, so it lives apart from nemoclaw-test-fixtures: the +# openshell and fabric provider tests use those fixtures without compiling the +# SDK. Tests find the executable beside theirs (fixture_executable). +[package] +name = "nemoclaw-fixture-provider" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[[bin]] +name = "nemoclaw-fixture-provider" +path = "src/main.rs" + +[dependencies] +nemoclaw-sdk = { path = "../nemoclaw-sdk" } +nemoclaw-runtime = { path = "../nemoclaw-runtime", default-features = false } +nemoclaw-tofu = { path = "../nemoclaw-tofu" } +tf-provider.workspace = true +async-trait.workspace = true +rustls.workspace = true +serde_json.workspace = true +tokio.workspace = true + +[dev-dependencies] +nemoclaw-test-fixtures = { path = "../nemoclaw-test-fixtures" } + +[lints] +workspace = true diff --git a/crates/nemoclaw-test-fixtures/src/bin/nemoclaw-fixture-provider.rs b/crates/nemoclaw-fixture-provider/src/main.rs similarity index 100% rename from crates/nemoclaw-test-fixtures/src/bin/nemoclaw-fixture-provider.rs rename to crates/nemoclaw-fixture-provider/src/main.rs diff --git a/crates/nemoclaw-fixture-provider/tests/archive.rs b/crates/nemoclaw-fixture-provider/tests/archive.rs new file mode 100644 index 00000000000..ff759a053f0 --- /dev/null +++ b/crates/nemoclaw-fixture-provider/tests/archive.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//! Building this test also builds the fixture provider that the provider +//! protocol tests find beside their test executables. + +/// The lifecycle profile selects this ignored test, so nextest archives the +/// fixture provider beside the extracted lifecycle tests. +#[test] +#[ignore = "run by the lifecycle profile to archive the fixture provider with its tests"] +fn the_fixture_provider_is_found_beside_the_test_executables() { + let provider = nemoclaw_test_fixtures::fixture_executable("nemoclaw-fixture-provider"); + assert!(provider.is_file()); +} diff --git a/crates/nemoclaw-provider/tests/contract/discovery.rs b/crates/nemoclaw-provider/tests/contract/discovery.rs index 6dbc250432c..9b7305f4517 100644 --- a/crates/nemoclaw-provider/tests/contract/discovery.rs +++ b/crates/nemoclaw-provider/tests/contract/discovery.rs @@ -2,12 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::http_fixture as transport; -use crate::tofu::TofuWorkspace; use nemoclaw_sdk::fabric_catalog::{FabricCatalog, IMAGE_CATALOG_LABEL}; use serde_json::{Value, json}; use std::{ fs, - path::PathBuf, sync::{ Arc, atomic::{AtomicUsize, Ordering}, @@ -15,14 +13,8 @@ use std::{ }; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn discovery_plan_reads_target_metadata_without_gateway_or_mutations() { - let tofu = - PathBuf::from(std::env::var_os("NEMOCLAW_TEST_TOFU").expect("explicit OpenTofu required")); - let provider = PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_PROVIDER").expect("explicit provider required"), - ); - assert!(tofu.is_absolute() && provider.is_absolute()); let requests = Arc::new(AtomicUsize::new(0)); let seen = requests.clone(); let catalog = FabricCatalog::bundled(); @@ -39,7 +31,7 @@ async fn discovery_plan_reads_target_metadata_without_gateway_or_mutations() { }; Some((200, serde_json::to_vec(&body).unwrap())) }).await; - let directory = TofuWorkspace::new(tofu, provider); + let directory = crate::workspace(); let root = directory.path(); // An engine nothing serves: a missing socket, or a closed loopback port. let unreachable = if cfg!(unix) { @@ -101,13 +93,8 @@ async fn discovery_plan_reads_target_metadata_without_gateway_or_mutations() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn authored_fabric_requirements_take_the_configuration_and_policy_reads_separately() { - let tofu = - PathBuf::from(std::env::var_os("NEMOCLAW_TEST_TOFU").expect("explicit OpenTofu required")); - let provider = PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_PROVIDER").expect("explicit provider required"), - ); let catalog_json = serde_json::to_string(&FabricCatalog::bundled()).unwrap(); let fixture = transport::Fixture::engine(move |request| { let body = match request.path.as_str() { @@ -117,7 +104,7 @@ async fn authored_fabric_requirements_take_the_configuration_and_policy_reads_se Some((200, serde_json::to_vec(&body).unwrap())) }) .await; - let directory = TofuWorkspace::new(tofu, provider); + let directory = crate::workspace(); let write = |configuration: &str, reads: &str| { fs::write( directory.path().join("main.tf"), @@ -192,15 +179,9 @@ output "status" {{ // Its managed gateway needs a local engine, which only Unix clients reach. #[cfg(unix)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn compiled_discovery_requires_runtime_metadata_but_allows_unknown_capabilities() { use nemoclaw_sdk::{compile::compile, config::Document}; - let tofu = - PathBuf::from(std::env::var_os("NEMOCLAW_TEST_TOFU").expect("explicit OpenTofu required")); - let provider = PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_PROVIDER").expect("explicit provider required"), - ); - assert!(tofu.is_absolute() && provider.is_absolute()); let document = Document::parse(include_bytes!("../../../../examples/onboarding/openclaw.yaml").as_slice()) .unwrap(); @@ -265,7 +246,7 @@ async fn compiled_discovery_requires_runtime_metadata_but_allows_unknown_capabil }, "output":output }); - let directory = TofuWorkspace::new(tofu, provider); + let directory = crate::workspace(); let root = directory.path(); fs::write(root.join("main.tf.json"), graph.to_string()).unwrap(); for selected in 0..5 { diff --git a/crates/nemoclaw-provider/tests/contract/gateway_readiness.rs b/crates/nemoclaw-provider/tests/contract/gateway_readiness.rs index b3b72135f65..04c8174cd5e 100644 --- a/crates/nemoclaw-provider/tests/contract/gateway_readiness.rs +++ b/crates/nemoclaw-provider/tests/contract/gateway_readiness.rs @@ -1,11 +1,10 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use crate::{http_fixture as docker, openshell::Fixture, tofu::TofuWorkspace}; +use crate::{http_fixture as docker, openshell::Fixture}; use serde_json::{Value, json}; use std::{ fs, - path::PathBuf, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -14,11 +13,8 @@ use std::{ }; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated engine and gateway fixtures"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated engine and gateway fixtures"] async fn managed_gateway_exit_preserves_bootstrap_state_and_allows_recovery_or_teardown() { - let tofu = PathBuf::from(std::env::var_os("NEMOCLAW_TEST_TOFU").unwrap()); - let provider = PathBuf::from(std::env::var_os("NEMOCLAW_TEST_PROVIDER").unwrap()); - assert!(tofu.is_absolute() && provider.is_absolute()); let gateway = Fixture::start().await; let name = "nc-0123456789abcdef-gateway".to_owned(); let owner = "302ff5e1-088d-42ce-959f-4ff4c3570c13".to_owned(); @@ -34,7 +30,7 @@ async fn managed_gateway_exit_preserves_bootstrap_state_and_allows_recovery_or_t "State":{"Running":active,"Status":if active {"running"} else {"exited"},"ExitCode":42,"Error":"PRIVATE_SENTINEL"} })).unwrap())) }).await; - let directory = TofuWorkspace::new(tofu, provider); + let directory = crate::workspace(); let root = directory.path(); let mut graph = json!({ "terraform":{"required_version":"= 1.12.6","required_providers":{ diff --git a/crates/nemoclaw-provider/tests/contract/gateway_storage.rs b/crates/nemoclaw-provider/tests/contract/gateway_storage.rs index 19efa8988d3..9c5b5782c70 100644 --- a/crates/nemoclaw-provider/tests/contract/gateway_storage.rs +++ b/crates/nemoclaw-provider/tests/contract/gateway_storage.rs @@ -10,7 +10,6 @@ use std::{ collections::BTreeMap, fs, io::Read, - path::PathBuf, process::Output, sync::{Arc, Mutex}, }; @@ -209,14 +208,11 @@ fn run(workspace: &TofuWorkspace, args: &[&str], success: bool) -> String { } fn workspace() -> TofuWorkspace { - let path = |name| PathBuf::from(std::env::var_os(name).expect("explicit qualification path")); - let (tofu, provider) = (path("NEMOCLAW_TEST_TOFU"), path("NEMOCLAW_TEST_PROVIDER")); - assert!(tofu.is_absolute() && provider.is_absolute()); - TofuWorkspace::new(tofu, provider) + crate::workspace() } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn gateway_storage_initializes_once_rejects_drift_and_refuses_deletion() { let (fixture, engine) = docker_engine().await; let workspace = workspace(); diff --git a/crates/nemoclaw-provider/tests/contract/inference_capabilities.rs b/crates/nemoclaw-provider/tests/contract/inference_capabilities.rs index c00c69c7045..5907c2a9170 100644 --- a/crates/nemoclaw-provider/tests/contract/inference_capabilities.rs +++ b/crates/nemoclaw-provider/tests/contract/inference_capabilities.rs @@ -7,7 +7,6 @@ use crate::{http_fixture::Fixture, tofu::TofuWorkspace}; use serde_json::{Value, json}; use std::{ fs, - path::PathBuf, process::Output, sync::{Arc, Mutex}, }; @@ -17,10 +16,7 @@ use std::{ type Seen = (String, String, Option, bool); fn workspace() -> TofuWorkspace { - let path = |name| PathBuf::from(std::env::var_os(name).expect("explicit qualification path")); - let (tofu, provider) = (path("NEMOCLAW_TEST_TOFU"), path("NEMOCLAW_TEST_PROVIDER")); - assert!(tofu.is_absolute() && provider.is_absolute()); - TofuWorkspace::new(tofu, provider) + crate::workspace() } fn configure(workspace: &TofuWorkspace, endpoint: &str, api: &str) { @@ -81,7 +77,7 @@ async fn model_server(status: u16, body: &'static str) -> (Fixture, Arc TofuWorkspace { - let tofu = PathBuf::from(std::env::var_os("NEMOCLAW_TEST_TOFU").unwrap()); - let provider = PathBuf::from(std::env::var_os("NEMOCLAW_TEST_PROVIDER").unwrap()); - assert!(tofu.is_absolute() && provider.is_absolute()); - let workspace = TofuWorkspace::new(tofu, provider); + let workspace = crate::workspace(); fs::write( workspace.path().join("main.tf"), format!( @@ -68,7 +65,7 @@ resource "nemoclaw_kubernetes_gateway" "platform" {{ } #[test] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; no cluster"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; no cluster"] fn kubernetes_resources_plan_from_typed_settings_and_reject_invalid_ones() { let valid = workspace(&platform("agents", None)); let run = diff --git a/crates/nemoclaw-provider/tests/contract/main.rs b/crates/nemoclaw-provider/tests/contract/main.rs index bc4050234c1..f046c107306 100644 --- a/crates/nemoclaw-provider/tests/contract/main.rs +++ b/crates/nemoclaw-provider/tests/contract/main.rs @@ -31,6 +31,17 @@ mod vllm_runtime; pub use nemoclaw_test_fixtures::{openshell, tofu}; +/// The nemoclaw provider Cargo built from this checkout, the one under test. +pub fn provider() -> std::path::PathBuf { + nemoclaw_test_fixtures::package_executable!("terraform-provider-nemoclaw") +} + +/// A workspace with the provider under test, and OpenTofu and the openshell +/// and fabric providers from the bundle. +pub fn workspace() -> tofu::TofuWorkspace { + tofu::TofuWorkspace::new(&nemoclaw_test_fixtures::Bundle::from_env(), provider()) +} + /// Failed apply may record new data-source observations and condition /// results; its managed resources and lineage must still be preserved. pub fn assert_same_managed_resources(actual: &[u8], expected: &[u8]) { diff --git a/crates/nemoclaw-provider/tests/contract/provider_protocol.rs b/crates/nemoclaw-provider/tests/contract/provider_protocol.rs index c47bbcafd25..96d8b85faac 100644 --- a/crates/nemoclaw-provider/tests/contract/provider_protocol.rs +++ b/crates/nemoclaw-provider/tests/contract/provider_protocol.rs @@ -3,11 +3,7 @@ use crate::tofu::TofuWorkspace; use serde_json::{Value, json}; -use std::{ - fs, - path::{Path, PathBuf}, - process::Output, -}; +use std::{fs, path::Path, process::Output}; struct Experiment { dir: TofuWorkspace, @@ -42,13 +38,8 @@ impl Experiment { }).to_string()).unwrap(); } fn new(provider: impl AsRef) -> Self { - let tofu = PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_TOFU") - .expect("explicit pinned OpenTofu executable required"), - ); - assert!(tofu.is_absolute()); let this = Self { - dir: TofuWorkspace::new(tofu, provider), + dir: TofuWorkspace::new(&nemoclaw_test_fixtures::Bundle::from_env(), provider), }; this.config("https://initial.example/v1"); this.mode("normal"); @@ -93,7 +84,7 @@ impl Experiment { } #[test] -#[ignore = "requires NEMOCLAW_TEST_TOFU; no live services"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; no live services"] fn real_tofu_checks_hardware_during_validation_planning_and_saved_plan_apply() { let e = Experiment::new(nemoclaw_test_fixtures::fixture_executable( "nemoclaw-fixture-provider", @@ -155,7 +146,7 @@ fn real_tofu_checks_hardware_during_validation_planning_and_saved_plan_apply() { } #[test] -#[ignore = "requires NEMOCLAW_TEST_TOFU; no live services"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; no live services"] fn real_tofu_preserves_failed_observations_and_reconciles_registration_drift_and_absence() { let e = Experiment::new(nemoclaw_test_fixtures::fixture_executable( "nemoclaw-fixture-provider", @@ -200,7 +191,7 @@ fn real_tofu_preserves_failed_observations_and_reconciles_registration_drift_and } #[test] -#[ignore = "requires NEMOCLAW_TEST_TOFU; no live services"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; no live services"] fn real_tofu_retains_identity_after_creation_reports_a_later_failure() { let e = Experiment::new(nemoclaw_test_fixtures::fixture_executable( "nemoclaw-fixture-provider", @@ -217,15 +208,12 @@ fn real_tofu_retains_identity_after_creation_reports_a_later_failure() { // A managed gateway needs a local engine, which only Unix clients reach. #[cfg(unix)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn production_provider_rechecks_network_and_image_prerequisites_before_saved_plan_apply() { use nemoclaw_sdk::{compile, config::Document}; use std::sync::{Arc, Mutex}; - let provider = PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_PROVIDER").expect("explicit production provider required"), - ); - assert!(provider.is_absolute()); + let provider = crate::provider(); let mode = Arc::new(Mutex::new("normal")); let shared = mode.clone(); let fixture = crate::http_fixture::Fixture::engine(move |request| { diff --git a/crates/nemoclaw-provider/tests/contract/runtime_image.rs b/crates/nemoclaw-provider/tests/contract/runtime_image.rs index b22e1778f02..cc5a3fe95f4 100644 --- a/crates/nemoclaw-provider/tests/contract/runtime_image.rs +++ b/crates/nemoclaw-provider/tests/contract/runtime_image.rs @@ -7,7 +7,6 @@ use nemoclaw_sdk::managed::{RUNTIME_SPEC_VERSION, RUNTIME_SPEC_VERSION_LABEL}; use serde_json::{Value, json}; use std::{ fs, - path::PathBuf, sync::{Arc, Mutex}, }; @@ -26,10 +25,7 @@ async fn image_engine(image: Arc>) -> Fixture { } fn workspace(engine: &str) -> TofuWorkspace { - let path = |name| PathBuf::from(std::env::var_os(name).expect("explicit qualification path")); - let (tofu, provider) = (path("NEMOCLAW_TEST_TOFU"), path("NEMOCLAW_TEST_PROVIDER")); - assert!(tofu.is_absolute() && provider.is_absolute()); - let workspace = TofuWorkspace::new(tofu, provider); + let workspace = crate::workspace(); fs::write( workspace.path().join("main.tf"), format!( @@ -62,7 +58,7 @@ output "observation" {{ } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn authored_runtime_image_check_reads_labels_and_rejects_an_incompatible_image() { let image = Arc::new(Mutex::new(json!({ "Id": "sha256:runtime", "Os": "linux", "Architecture": "arm64", diff --git a/crates/nemoclaw-provider/tests/contract/service_capacity.rs b/crates/nemoclaw-provider/tests/contract/service_capacity.rs index 65536fb0efc..5fb734eec92 100644 --- a/crates/nemoclaw-provider/tests/contract/service_capacity.rs +++ b/crates/nemoclaw-provider/tests/contract/service_capacity.rs @@ -1,21 +1,14 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use crate::tofu::TofuWorkspace; use nemoclaw_sdk::{compile, config::Document}; use serde_json::{Value, json}; -use std::{fs, path::PathBuf}; +use std::fs; #[test] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated SSH fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated SSH fixture"] fn production_capacity_data_blocks_overcommit_defers_unknowns_and_preserves_state() { - let tofu = - PathBuf::from(std::env::var_os("NEMOCLAW_TEST_TOFU").expect("explicit OpenTofu required")); - let provider = PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_PROVIDER").expect("explicit provider required"), - ); - assert!(tofu.is_absolute() && provider.is_absolute()); - let directory = TofuWorkspace::new(tofu, provider); + let directory = crate::workspace(); let root = directory.path(); // Beside the providers, where Windows finds it before the relay on PATH. nemoclaw_test_fixtures::ssh::install_simulator(root, root); diff --git a/crates/nemoclaw-provider/tests/contract/service_readiness.rs b/crates/nemoclaw-provider/tests/contract/service_readiness.rs index ea72797c462..059b9f8fdff 100644 --- a/crates/nemoclaw-provider/tests/contract/service_readiness.rs +++ b/crates/nemoclaw-provider/tests/contract/service_readiness.rs @@ -1,28 +1,24 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use crate::tofu::TofuWorkspace; use nemoclaw_sdk::{compile, config::Document}; use serde_json::{Value, json}; -use std::{fs, path::PathBuf}; +use std::fs; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated SSH fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated SSH fixture"] async fn standalone_readiness_defers_to_apply_rechecks_unchanged_services_and_allows_destroy() { standalone_readiness(false).await; } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated SSH fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated SSH fixture"] async fn standalone_proxy_readiness_rechecks_identity_and_credentials_without_sdk_or_gateway() { standalone_readiness(true).await; } async fn standalone_readiness(proxy: bool) { - let tofu = PathBuf::from(std::env::var_os("NEMOCLAW_TEST_TOFU").unwrap()); - let provider = PathBuf::from(std::env::var_os("NEMOCLAW_TEST_PROVIDER").unwrap()); - assert!(tofu.is_absolute() && provider.is_absolute()); - let directory = TofuWorkspace::new(tofu, provider); + let directory = crate::workspace(); let root = directory.path(); // Beside the providers, where Windows finds it before the relay on PATH. nemoclaw_test_fixtures::ssh::install_simulator(root, root); diff --git a/crates/nemoclaw-provider/tests/contract/service_storage.rs b/crates/nemoclaw-provider/tests/contract/service_storage.rs index ef3edb18776..d2dc6b42f67 100644 --- a/crates/nemoclaw-provider/tests/contract/service_storage.rs +++ b/crates/nemoclaw-provider/tests/contract/service_storage.rs @@ -7,7 +7,6 @@ use serde_json::{Value, json}; use std::{ collections::BTreeMap, fs, - path::PathBuf, sync::{Arc, Mutex}, }; @@ -60,10 +59,7 @@ async fn volume_engine() -> (Fixture, Arc>) { } fn workspace() -> TofuWorkspace { - let path = |name| PathBuf::from(std::env::var_os(name).expect("explicit qualification path")); - let (tofu, provider) = (path("NEMOCLAW_TEST_TOFU"), path("NEMOCLAW_TEST_PROVIDER")); - assert!(tofu.is_absolute() && provider.is_absolute()); - TofuWorkspace::new(tofu, provider) + crate::workspace() } /// Write one storage resource; `identity` supplies owner and generation. @@ -114,7 +110,7 @@ fn success(workspace: &TofuWorkspace, args: &[&str]) -> std::process::Output { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn authored_service_storage_applies_refreshes_and_names_invalid_attributes() { for (kind, name) in [ ( @@ -197,7 +193,7 @@ fn lowercase_hex(value: &str) -> bool { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn omitted_storage_identity_is_generated_once_and_kept() { for kind in KINDS { let (engine, volumes) = volume_engine().await; @@ -246,7 +242,7 @@ async fn omitted_storage_identity_is_generated_once_and_kept() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn lost_reply_with_omitted_identity_stops_until_its_identity_is_supplied() { for kind in KINDS { let (engine, volumes) = volume_engine().await; @@ -295,7 +291,7 @@ async fn lost_reply_with_omitted_identity_stops_until_its_identity_is_supplied() } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker fixture"] async fn omitted_proxy_storage_identity_is_generated_and_labels_its_credential_volume() { let (engine, volumes) = volume_engine().await; let workspace = workspace(); @@ -347,7 +343,7 @@ async fn upstream(digest: Arc>) -> Fixture { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated Docker and Ollama fixtures"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated Docker and Ollama fixtures"] async fn authored_external_model_shares_proxy_identity_and_rechecks_its_digest() { let (engine, volumes) = volume_engine().await; let digest = Arc::new(Mutex::new("a".repeat(64))); diff --git a/crates/nemoclaw-test-fixtures/Cargo.toml b/crates/nemoclaw-test-fixtures/Cargo.toml index b53ef029e5d..f3313821ed9 100644 --- a/crates/nemoclaw-test-fixtures/Cargo.toml +++ b/crates/nemoclaw-test-fixtures/Cargo.toml @@ -4,7 +4,9 @@ # Test fixtures shared by the provider contract tests and the end-to-end # tests. Provider crates use them as development dependencies; nemoclaw-e2e # depends on the providers, so they cannot live there. The fixture executables -# in src/bin are found beside the test executables (fixture_executable). +# in src/bin are found beside the test executables (fixture_executable). The +# fixture provider, which needs the SDK, is in nemoclaw-fixture-provider so the +# openshell and fabric provider tests do not compile the SDK. [package] name = "nemoclaw-test-fixtures" version.workspace = true @@ -14,12 +16,6 @@ license.workspace = true publish = false [dependencies] -# The fixture provider serves SDK resource definitions through the shared adapter. -nemoclaw-sdk = { path = "../nemoclaw-sdk" } -nemoclaw-runtime = { path = "../nemoclaw-runtime", default-features = false } -nemoclaw-tofu = { path = "../nemoclaw-tofu" } -tf-provider.workspace = true -async-trait.workspace = true tar.workspace = true openshell-core.workspace = true tonic.workspace = true diff --git a/crates/nemoclaw-test-fixtures/src/bundle.rs b/crates/nemoclaw-test-fixtures/src/bundle.rs new file mode 100644 index 00000000000..3f7064b14cd --- /dev/null +++ b/crates/nemoclaw-test-fixtures/src/bundle.rs @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +//! The bundle that `NEMOCLAW_TEST_BUNDLE` names: the one input of the tests +//! that run OpenTofu. It supplies pinned OpenTofu and the providers it ships. + +use std::{ + fs, + path::{Path, PathBuf}, +}; + +/// A built bundle directory, such as `dist/linux_arm64`. +#[derive(Clone, Debug)] +pub struct Bundle { + root: PathBuf, +} + +impl Bundle { + /// The bundle `NEMOCLAW_TEST_BUNDLE` names, which must be an absolute path. + #[must_use] + pub fn from_env() -> Self { + let root = PathBuf::from( + std::env::var_os("NEMOCLAW_TEST_BUNDLE") + .expect("NEMOCLAW_TEST_BUNDLE names a bundle; build one with cargo ci bundle"), + ); + assert!( + root.is_absolute(), + "NEMOCLAW_TEST_BUNDLE is not absolute: {}", + root.display() + ); + Self::open(root) + } + + /// The bundle at `root`. + pub fn open(root: impl Into) -> Self { + Self { root: root.into() } + } + + #[must_use] + pub fn root(&self) -> &Path { + &self.root + } + + /// The bundle's pinned OpenTofu. + #[must_use] + pub fn tofu(&self) -> PathBuf { + let tofu = self.root.join("libexec").join(crate::executable("tofu")); + assert!(tofu.is_file(), "{} is missing", tofu.display()); + tofu + } + + /// The bundle's `registry.opentofu.org/nvidia/{name}` provider, such as + /// `nemoclaw`, `openshell`, or `fabric`. + #[must_use] + pub fn provider(&self, name: &str) -> PathBuf { + // providers/registry.opentofu.org/nvidia/NAME/VERSION/PLATFORM/terraform-provider-NAME_vVERSION + let source = ["providers", "registry.opentofu.org", "nvidia", name] + .iter() + .fold(self.root.clone(), |path, part| path.join(part)); + assert!( + source.is_dir(), + "the bundle ships no {name} provider: {} is missing", + source.display() + ); + let version = only_entry(&source); + let platform = only_entry(&version); + let version = version.file_name().unwrap().to_string_lossy(); + let provider = platform.join(crate::executable(&format!( + "terraform-provider-{name}_v{version}" + ))); + assert!(provider.is_file(), "{} is missing", provider.display()); + provider + } +} + +/// The one entry of `directory`, as a bundle has one version and platform of each provider. +fn only_entry(directory: &Path) -> PathBuf { + let entries: Vec<_> = fs::read_dir(directory) + .unwrap_or_else(|error| panic!("{}: {error}", directory.display())) + .map(|entry| entry.unwrap().path()) + .collect(); + assert_eq!( + entries.len(), + 1, + "{} must hold exactly one entry", + directory.display() + ); + entries.into_iter().next().unwrap() +} + +/// A binary target of the calling test's own package, such as the provider +/// its contract tests check. +/// +/// Nextest names the executable at run time, also where it extracts an +/// archive; under `cargo test`, the path Cargo compiled in is used. +#[macro_export] +macro_rules! package_executable { + ($name:literal) => { + $crate::bundle::package_executable($name, env!(concat!("CARGO_BIN_EXE_", $name))) + }; +} + +#[doc(hidden)] +#[must_use] +pub fn package_executable(name: &str, compiled: &str) -> PathBuf { + std::env::var_os(format!("NEXTEST_BIN_EXE_{name}")) + .map_or_else(|| PathBuf::from(compiled), PathBuf::from) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn bundle() -> (tempfile::TempDir, Bundle) { + let root = tempfile::tempdir().unwrap(); + let files = [ + "libexec/tofu".to_owned(), + "providers/registry.opentofu.org/nvidia/openshell/0.1.0-dev.abc/linux_arm64/terraform-provider-openshell_v0.1.0-dev.abc".to_owned(), + "providers/registry.opentofu.org/nvidia/fabric/0.1.0-dev.abc/linux_arm64/terraform-provider-fabric_v0.1.0-dev.abc".to_owned(), + ]; + for file in files { + let path = root.path().join(crate::executable(&file)); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, file).unwrap(); + } + let bundle = Bundle::open(root.path()); + (root, bundle) + } + + #[test] + fn finds_pinned_opentofu_and_each_shipped_provider() { + let (root, bundle) = bundle(); + assert_eq!( + bundle.tofu(), + root.path().join("libexec").join(crate::executable("tofu")) + ); + for name in ["openshell", "fabric"] { + let provider = bundle.provider(name); + let contents = fs::read_to_string(&provider).unwrap(); + assert!(contents.contains(&format!("nvidia/{name}/")), "{contents}"); + } + } + + #[test] + #[should_panic(expected = "the bundle ships no nemoclaw provider")] + fn a_provider_the_bundle_does_not_ship_is_named_in_the_failure() { + let (_root, bundle) = bundle(); + let _ = bundle.provider("nemoclaw"); + } + + #[test] + #[should_panic(expected = "exactly one entry")] + fn a_second_provider_version_is_ambiguous() { + let (root, bundle) = bundle(); + fs::create_dir_all( + root.path() + .join("providers/registry.opentofu.org/nvidia/fabric/0.1.0-dev.def"), + ) + .unwrap(); + let _ = bundle.provider("fabric"); + } +} diff --git a/crates/nemoclaw-test-fixtures/src/lib.rs b/crates/nemoclaw-test-fixtures/src/lib.rs index 25f828c12aa..9ef367a447e 100644 --- a/crates/nemoclaw-test-fixtures/src/lib.rs +++ b/crates/nemoclaw-test-fixtures/src/lib.rs @@ -1,25 +1,24 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! A fake OpenShell gateway, owned OpenTofu workspaces, and fixture -//! executables, shared by the provider contract tests and the end-to-end tests. +//! A fake OpenShell gateway, owned OpenTofu workspaces, the bundle they run, +//! and fixture executables, shared by the provider contract tests and the +//! end-to-end tests. +pub mod bundle; pub mod openshell; pub mod ssh; pub mod tofu; -/// The fake `ssh` relay: beside `providers`, where archived lifecycle runs -/// unpack it with the providers, or beside the test executables. +pub use bundle::Bundle; + +/// The fake `ssh` relay, beside the test executables. #[must_use] -pub fn ssh_relay(providers: &std::path::Path) -> std::path::PathBuf { - let beside = providers.join(executable("nemoclaw-fixture-ssh")); - if beside.is_file() { - return beside; - } +pub fn ssh_relay() -> std::path::PathBuf { fixture_executable("nemoclaw-fixture-ssh") } -/// A fixture executable this crate builds, such as `nemoclaw-fixture-provider` -/// or `nemoclaw-fixture-ssh-simulator`. +/// A fixture executable from this crate, such as `nemoclaw-fixture-ssh-simulator`, +/// or from `nemoclaw-fixture-provider`. /// /// Cargo sets `CARGO_BIN_EXE_*` only for a package's own tests, so other /// packages find these beside their test executables: Cargo writes test @@ -35,7 +34,7 @@ pub fn fixture_executable(name: &str) -> std::path::PathBuf { .join(executable(name)); assert!( path.is_file(), - "{} is missing; build it with cargo build -p nemoclaw-test-fixtures", + "{} is missing; build it with cargo build -p nemoclaw-test-fixtures -p nemoclaw-fixture-provider", path.display() ); path diff --git a/crates/nemoclaw-test-fixtures/src/tofu.rs b/crates/nemoclaw-test-fixtures/src/tofu.rs index f794118b255..70f16b04440 100644 --- a/crates/nemoclaw-test-fixtures/src/tofu.rs +++ b/crates/nemoclaw-test-fixtures/src/tofu.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 //! Owned OpenTofu workspaces for explicitly selected test executables. +use crate::Bundle; use std::{ fs, path::{Path, PathBuf}, @@ -12,13 +13,9 @@ use tempfile::TempDir; /// Where Unix sockets are missing, fake engines are reached over SSH. Windows /// resolves a bare program name in the launching executable's directory /// before `PATH`, so providers copied here run the relay as `ssh`. -fn install_ssh_relay(directory: &Path, providers: &Path) { +fn install_ssh_relay(directory: &Path) { if cfg!(not(unix)) { - fs::copy( - crate::ssh_relay(providers), - directory.join(crate::executable("ssh")), - ) - .unwrap(); + fs::copy(crate::ssh_relay(), directory.join(crate::executable("ssh"))).unwrap(); } } @@ -28,39 +25,18 @@ pub struct TofuWorkspace { } impl TofuWorkspace { - /// A workspace whose OpenTofu uses `provider` and, beside it, the - /// `openshell` and `fabric` providers built with it. - pub fn new(tofu: impl AsRef, provider: impl AsRef) -> Self { - let directory = tempfile::tempdir().unwrap(); - let provider = provider.as_ref(); - fs::copy( - provider, - directory - .path() - .join(crate::executable("terraform-provider-nemoclaw")), + /// A workspace whose OpenTofu is the bundle's and uses the `nemoclaw` + /// provider at `nemoclaw` and the bundle's `openshell` and `fabric` + /// providers. + pub fn new(bundle: &Bundle, nemoclaw: impl AsRef) -> Self { + Self::with_providers( + bundle.tofu(), + &[ + ("nemoclaw", nemoclaw.as_ref()), + ("openshell", &bundle.provider("openshell")), + ("fabric", &bundle.provider("fabric")), + ], ) - .unwrap(); - for name in ["openshell", "fabric"] { - let binary = crate::executable(&format!("terraform-provider-{name}")); - let sibling = provider.with_file_name(&binary); - if sibling.exists() { - fs::copy(sibling, directory.path().join(binary)).unwrap(); - } - } - install_ssh_relay(directory.path(), provider.parent().unwrap()); - let path = serde_json::to_string(directory.path()).unwrap(); - let overrides = ["nemoclaw", "openshell", "fabric"] - .map(|name| format!("\"registry.opentofu.org/nvidia/{name}\" = {path}")) - .join(" "); - fs::write( - directory.path().join("tofu.rc"), - format!("provider_installation {{ dev_overrides {{ {overrides} }} direct {{}} }}"), - ) - .unwrap(); - Self { - directory, - tofu: tofu.as_ref().to_owned(), - } } /// A workspace whose OpenTofu uses only the given `(name, executable)` @@ -76,8 +52,8 @@ impl TofuWorkspace { ) .unwrap(); } - if let Some((_, executable)) = providers.first() { - install_ssh_relay(directory.path(), executable.parent().unwrap()); + if !providers.is_empty() { + install_ssh_relay(directory.path()); } let path = serde_json::to_string(directory.path()).unwrap(); let overrides = providers @@ -125,7 +101,7 @@ mod tests { let script = format!("#!/bin/sh\nprintf '%s' '{selected}'\n"); fs::write(&provider, &script).unwrap(); fs::set_permissions(&provider, fs::Permissions::from_mode(0o700)).unwrap(); - let workspace = TofuWorkspace::new("/bin/sh", &provider); + let workspace = TofuWorkspace::with_providers("/bin/sh", &[("nemoclaw", &provider)]); let staged = workspace .path() .join(crate::executable("terraform-provider-nemoclaw")); @@ -152,7 +128,7 @@ mod tests { let sources = tempfile::tempdir().unwrap(); let provider = sources.path().join("provider"); fs::write(&provider, b"explicit provider").unwrap(); - let workspace = TofuWorkspace::new("/bin/sh", provider); + let workspace = TofuWorkspace::with_providers("/bin/sh", &[("nemoclaw", &provider)]); let output = workspace.command() .args(["-c", r#"pwd -P; printf '%s\n' "$TF_CLI_CONFIG_FILE" "$TF_IN_AUTOMATION" "$CHECKPOINT_DISABLE" "$SCENARIO_VALUE"; printf 'expected failure' >&2; exit 23"#]) .env("SCENARIO_VALUE", "literal scenario value") diff --git a/crates/nemoclaw-test-fixtures/tests/ssh_relay.rs b/crates/nemoclaw-test-fixtures/tests/ssh_relay.rs index f2d453aba06..dedc5869a29 100644 --- a/crates/nemoclaw-test-fixtures/tests/ssh_relay.rs +++ b/crates/nemoclaw-test-fixtures/tests/ssh_relay.rs @@ -62,6 +62,6 @@ fn anything_but_a_loopback_dial_stdio_fails_like_ssh() { #[test] #[ignore = "run by the lifecycle profile to archive the relay with its tests"] fn the_relay_is_found_beside_the_test_executables() { - let relay = nemoclaw_test_fixtures::ssh_relay(std::path::Path::new("/nonexistent")); + let relay = nemoclaw_test_fixtures::ssh_relay(); assert!(relay.is_file()); } diff --git a/crates/openshell-provider/tests/contract/capabilities.rs b/crates/openshell-provider/tests/contract/capabilities.rs index f13186330f4..5df8bab8206 100644 --- a/crates/openshell-provider/tests/contract/capabilities.rs +++ b/crates/openshell-provider/tests/contract/capabilities.rs @@ -79,7 +79,7 @@ impl Local { /// without changing the gateway or state. nemoclaw-openshell's observation /// tests own each incompatibility reason. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; fake OpenShell gateway"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; fake OpenShell gateway"] async fn gateway_capabilities_gate_plans_and_saved_plan_applies() { let gateway = Fixture::start().await; let local = Local::new(&gateway.endpoint); @@ -224,7 +224,7 @@ async fn gateway_capability_observations_preserve_metadata_and_fail_closed_witho } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; fake OpenShell gateway"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; fake OpenShell gateway"] async fn gateway_capability_reads_wait_for_unknown_bootstrap_dependencies() { let gateway = Fixture::start().await; gateway.state.lock().unwrap().fail_read = Some(("gateway", tonic::Code::Unavailable)); diff --git a/crates/openshell-provider/tests/contract/main.rs b/crates/openshell-provider/tests/contract/main.rs index 72a88b3ce9f..7a1e06b2594 100644 --- a/crates/openshell-provider/tests/contract/main.rs +++ b/crates/openshell-provider/tests/contract/main.rs @@ -14,27 +14,14 @@ mod standalone; const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/contract/fixtures"); +/// The bundle's pinned OpenTofu. fn tofu() -> std::path::PathBuf { - let tofu = std::path::PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_TOFU").expect("explicit pinned OpenTofu path"), - ); - assert!(tofu.is_absolute()); - tofu + nemoclaw_test_fixtures::Bundle::from_env().tofu() } -/// The openshell provider built beside NEMOCLAW_TEST_PROVIDER, as the -/// lifecycle tests receive it. +/// The openshell provider Cargo built from this checkout, the one under test. fn provider() -> std::path::PathBuf { - let nemoclaw = std::path::PathBuf::from( - std::env::var_os("NEMOCLAW_TEST_PROVIDER").expect("explicit provider build path"), - ); - assert!(nemoclaw.is_absolute()); - nemoclaw - .parent() - .unwrap() - .join(nemoclaw_test_fixtures::executable( - "terraform-provider-openshell", - )) + nemoclaw_test_fixtures::package_executable!("terraform-provider-openshell") } /// Credential variables the fixture's registrations name, with fixture values. @@ -131,7 +118,7 @@ macro_rules! fixtures { ($($test:ident => $name:literal,)*) => { $( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - #[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; fake OpenShell gateway"] + #[ignore = "requires NEMOCLAW_TEST_BUNDLE; fake OpenShell gateway"] async fn $test() { reads_back_unchanged($name).await; } diff --git a/crates/openshell-provider/tests/contract/standalone.rs b/crates/openshell-provider/tests/contract/standalone.rs index c4cc4d5a20c..de20bb3ad18 100644 --- a/crates/openshell-provider/tests/contract/standalone.rs +++ b/crates/openshell-provider/tests/contract/standalone.rs @@ -100,7 +100,7 @@ impl Standalone { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_registrations_recreate_confirmed_absence_without_replacing_sandboxes() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -143,7 +143,7 @@ async fn standalone_registrations_recreate_confirmed_absence_without_replacing_s } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_registrations_replace_and_remove_without_destroy_mode() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -210,7 +210,7 @@ async fn standalone_registrations_replace_and_remove_without_destroy_mode() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_registrations_preserve_bindings_on_failed_or_foreign_observations() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -245,7 +245,7 @@ async fn standalone_registrations_preserve_bindings_on_failed_or_foreign_observa } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_registrations_recover_lost_create_response_without_duplicate_creation() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -265,7 +265,7 @@ async fn standalone_registrations_recover_lost_create_response_without_duplicate } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_registration_authentication_mode_replaces_but_secret_reference_rotation_updates() { let fixture = Fixture::start().await; @@ -333,7 +333,7 @@ async fn standalone_registration_authentication_mode_replaces_but_secret_referen } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_hcl_applies_resources_without_sdk_encoding_or_coordination() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -356,7 +356,7 @@ async fn standalone_hcl_applies_resources_without_sdk_encoding_or_coordination() } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_hcl_recovers_partial_creation_after_untaint_and_retains_workspace() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -411,7 +411,7 @@ async fn standalone_hcl_recovers_partial_creation_after_untaint_and_retains_work } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_hcl_rejects_missing_bound_resources_before_recreation() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -437,7 +437,7 @@ async fn standalone_hcl_rejects_missing_bound_resources_before_recreation() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_hcl_rejects_replacement_and_unauthorized_removal_during_plan() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -462,7 +462,7 @@ async fn standalone_hcl_rejects_replacement_and_unauthorized_removal_during_plan } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_hcl_preserves_state_on_failed_observation_and_foreign_identity() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -507,7 +507,7 @@ async fn standalone_hcl_preserves_state_on_failed_observation_and_foreign_identi } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_hcl_recovers_lost_delete_response_without_repeating_the_mutation() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -548,7 +548,7 @@ async fn standalone_hcl_recovers_lost_delete_response_without_repeating_the_muta } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_hcl_defers_provider_endpoint_until_bootstrap_apply() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -561,7 +561,7 @@ async fn standalone_hcl_defers_provider_endpoint_until_bootstrap_apply() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn deferred_provider_rechecks_ownership_before_saved_plan_apply() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -584,7 +584,7 @@ async fn deferred_provider_rechecks_ownership_before_saved_plan_apply() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn deferred_provider_reports_apply_observation_failures_and_recovers() { let fixture = Fixture::start().await; fixture.state.lock().unwrap().fail_read = Some(("workspace", tonic::Code::Unavailable)); @@ -599,7 +599,7 @@ async fn deferred_provider_reports_apply_observation_failures_and_recovers() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn deferred_provider_keeps_bindings_when_bootstrap_endpoint_changes() { let fixture = Fixture::start().await; let next = Fixture::start().await; @@ -619,7 +619,7 @@ async fn deferred_provider_keeps_bindings_when_bootstrap_endpoint_changes() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn unavailable_bound_gateway_blocks_bootstrap_replacement_before_apply() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -644,7 +644,7 @@ async fn unavailable_bound_gateway_blocks_bootstrap_replacement_before_apply() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn gateway_readiness_dependency_waits_for_startup_before_workspace_creation() { let fixture = Fixture::start().await; fixture.state.lock().unwrap().fail_read = Some(("gateway", tonic::Code::Unavailable)); @@ -698,7 +698,7 @@ async fn gateway_readiness_dependency_waits_for_startup_before_workspace_creatio } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_create_readback_rejects_substitution_and_retains_established_binding() { for field in [ "id", @@ -747,7 +747,7 @@ async fn standalone_create_readback_rejects_substitution_and_retains_established } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_registrations_recover_after_absence_was_committed_by_refresh_only() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); @@ -783,7 +783,7 @@ async fn standalone_registrations_recover_after_absence_was_committed_by_refresh } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -#[ignore = "requires NEMOCLAW_TEST_TOFU and NEMOCLAW_TEST_PROVIDER; isolated OpenShell fixture"] +#[ignore = "requires NEMOCLAW_TEST_BUNDLE; isolated OpenShell fixture"] async fn standalone_lost_create_reply_requires_original_intent_to_recover_untracked_registration() { let fixture = Fixture::start().await; let tofu = Standalone::new(&fixture.endpoint); diff --git a/docs/contributing/integration-tests.md b/docs/contributing/integration-tests.md index 6d3766d8a66..df690ecda3e 100644 --- a/docs/contributing/integration-tests.md +++ b/docs/contributing/integration-tests.md @@ -10,17 +10,18 @@ Complete the [build prerequisites](../build.md) first. ## OpenTofu and Bundle Lifecycle The provider contract tests and the private `nemoclaw-e2e` crate run the provider protocol through OpenTofu 1.12.6. -Build the production providers and the `nemoclaw-test-fixtures` executables, and supply absolute executable paths explicitly. -Tests install `terraform-provider-openshell` and `terraform-provider-fabric` from the directory that holds `NEMOCLAW_TEST_PROVIDER`: +They take one input: `NEMOCLAW_TEST_BUNDLE`, the absolute path of a bundle such as the `dist/PLATFORM` directory that `cargo ci bundle` writes. +The bundle supplies the pinned OpenTofu and the providers the tests run. +Each provider's contract tests run instead the provider Cargo builds with them, so they test the checkout without a new bundle; the other providers they need come from the bundle. +Tests in other packages find fixture executables beside the test executables, so build those first: ```sh -cargo build -p nemoclaw-provider -p openshell-provider -p fabric-provider -p nemoclaw-test-fixtures -NEMOCLAW_TEST_TOFU=/absolute/path/to/tofu \ -NEMOCLAW_TEST_PROVIDER=/absolute/path/to/terraform-provider-nemoclaw \ +cargo build -p nemoclaw-test-fixtures -p nemoclaw-fixture-provider +NEMOCLAW_TEST_BUNDLE=/absolute/path/to/bundle \ cargo test -p nemoclaw-provider --test contract provider_protocol:: -- --ignored ``` -These tests launch the fixture provider that `nemoclaw-test-fixtures` builds, found beside the test executables, and use temporary files. +These tests launch the fixture provider that `nemoclaw-fixture-provider` builds and use temporary files. On Unix, they also run the production provider against a local Docker API fixture to check network and image planning, including prerequisite changes before saved-plan application. They create no Docker, OpenShell, or inference resources. The fixture provider is not a production bundle component. @@ -28,8 +29,7 @@ The fixture provider is not a production bundle component. On Unix, run from the repository root to test combined service capacity through the production provider and an isolated SSH simulator: ```sh -NEMOCLAW_TEST_TOFU=/absolute/path/to/tofu \ -NEMOCLAW_TEST_PROVIDER=/absolute/path/to/terraform-provider-nemoclaw \ +NEMOCLAW_TEST_BUNDLE=/absolute/path/to/bundle \ cargo test -p nemoclaw-provider --test contract service_capacity:: -- --ignored ``` @@ -39,7 +39,7 @@ It uses a built-in OpenTofu resource to exercise the generated precondition and Test the runtime bundle and live serving backend separately. The [OpenShell provider contract](#openshell-provider-contract) tests also check gateway version and driver preconditions, failed observations without resource changes, and data-source reads deferred until bootstrap inputs become known. -The `gateway_readiness::` tests use the same explicit OpenTofu/provider paths with local engine and OpenShell fixtures. +The `gateway_readiness::` tests use the same bundle with local engine and OpenShell fixtures. It checks prompt managed-gateway exit diagnostics, bootstrap state retained after failure, corrected retry, unchanged-apply rechecks, and teardown with the readiness data source omitted. Its bootstrap identity is a built-in OpenTofu resource; it creates no live container. Standalone HCL cases exercise provider/profile replacement and removal without sandbox teardown mode, recreation after confirmed absence, credential-reference updates, and recovery after a lost creation response. @@ -79,8 +79,6 @@ The mixed-search export case checks shared registrations, unused definitions, ex The `nemoclaw-provider` crate's `contract` tests run its types through OpenTofu against fake Docker engines, gateways, and model servers: ```sh -NEMOCLAW_TEST_TOFU=/absolute/path/to/tofu \ -NEMOCLAW_TEST_PROVIDER=/absolute/path/to/terraform-provider-nemoclaw \ NEMOCLAW_TEST_BUNDLE=/absolute/path/to/bundle \ cargo test -p nemoclaw-provider --test contract -- --ignored ``` @@ -95,8 +93,7 @@ The `openshell-provider` crate's `contract` tests apply each fixture in `crates/ Each fixture must plan no managed changes after it is applied, and teardown must remove every sandbox, provider registration, and profile while the gateway keeps the workspace: ```sh -NEMOCLAW_TEST_TOFU=/absolute/path/to/tofu \ -NEMOCLAW_TEST_PROVIDER=/absolute/path/to/terraform-provider-nemoclaw \ +NEMOCLAW_TEST_BUNDLE=/absolute/path/to/bundle \ cargo test -p openshell-provider --test contract -- --ignored ``` @@ -106,11 +103,10 @@ The fixtures are the OpenShell resources the SDK compiles for each example with ## Fabric Provider Contract -The `fabric-provider` crate's `contract` tests run each Fabric type through OpenTofu with the same paths: +The `fabric-provider` crate's `contract` tests run each Fabric type through OpenTofu with the same bundle: ```sh -NEMOCLAW_TEST_TOFU=/absolute/path/to/tofu \ -NEMOCLAW_TEST_PROVIDER=/absolute/path/to/terraform-provider-nemoclaw \ +NEMOCLAW_TEST_BUNDLE=/absolute/path/to/bundle \ cargo test -p fabric-provider --test contract -- --ignored ``` @@ -121,27 +117,25 @@ A coverage test requires a contract test for every Fabric type. ## Standalone Sandbox Completion -On Unix, build the production provider and supply the explicit OpenTofu and provider paths as above. +On Unix, supply the bundle as above; this test runs the bundle's `nemoclaw` provider. Run from the repository root: ```sh -NEMOCLAW_TEST_TOFU=/absolute/path/to/tofu \ -NEMOCLAW_TEST_PROVIDER=/absolute/path/to/terraform-provider-nemoclaw \ +NEMOCLAW_TEST_BUNDLE=/absolute/path/to/bundle \ cargo test -p nemoclaw-e2e --test integration sandbox_readiness:: -- --ignored ``` The fixture runs the sandbox completion data source through OpenTofu against a local gRPC server, without SDK deployment orchestration. It checks deferred health reads, failed postconditions with retained observations and bindings, unchanged-apply rechecks, prompt configuration-admission rejection with safe sandbox context, and teardown without readiness. It creates temporary state and simulated OpenShell resources; it does not start containers or invoke a model. -On failure, inspect the OpenTofu diagnostic and verify that the selected provider matches the checkout before rerunning. +On failure, inspect the OpenTofu diagnostic and verify that the bundle matches the checkout before rerunning. ## Standalone Service Readiness -On Unix, build the production provider as above and run from the repository root: +On Unix, supply the bundle as above and run from the repository root: ```sh -NEMOCLAW_TEST_TOFU=/absolute/path/to/tofu \ -NEMOCLAW_TEST_PROVIDER=/absolute/path/to/terraform-provider-nemoclaw \ +NEMOCLAW_TEST_BUNDLE=/absolute/path/to/bundle \ cargo test -p nemoclaw-provider --test contract service_readiness:: -- --ignored ``` diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index 2cfaf45a115..97a9d4d74a7 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -79,6 +79,7 @@ Every `CI / Native` platform uses cargo-nextest 0.9.144 for both the `ci` and `l `cargo ci test` runs tests not marked `#[ignore]` with the `ci` profile, then the doctests. The `ci` profile runs at most eight tests concurrently, reports slow tests every 30 seconds, terminates a test after five minutes, and does not retry failures. The `lifecycle` profile selects the isolated bundle fixtures and native-state test, with four concurrent tests and the same timeout. +The `lifecycle` step passes the platform's bundle as `NEMOCLAW_TEST_BUNDLE`, the tests' only input; each provider's contract tests run the provider Cargo built from the checkout. CI retains the same workspace and target selection across builds so Cargo can reuse the compiled tests. Each platform's build-and-test job and bundle job run in parallel on separate runners; its lifecycle workers start when both finish. Linux and macOS run two nextest hash partitions on separate runners and Windows runs three, each with four test slots. @@ -116,7 +117,7 @@ cargo ci lifecycle --partition hash:2/2 To reproduce the archive handoff, run `cargo ci archive` after building. It writes `.build/ci/lifecycle.tar.zst` and `.build/ci/lifecycle-inputs.tar`. -The first contains only binaries selected by the lifecycle profile and their nextest metadata; the second preserves the pinned tools, `nemoclaw-build` executable, provider helpers, and their executable permissions. +The first contains only binaries selected by the lifecycle profile, the executables their packages build, and their nextest metadata; the second preserves the pinned tools, `nemoclaw-build` executable, the fake `ssh` relay, and their executable permissions. The bundle is not included; CI builds it in a parallel job and transfers `dist/PLATFORM` separately. `cargo ci bundle` builds its own `nemoclaw-build`, so it does not need the `build` step first. Copy both archives into `.build/ci` and the bundle into `dist/PLATFORM` in a separate checkout of the same revision on the same operating system and architecture, then run: @@ -279,7 +280,7 @@ For deployment tests, keep the YAML and expected plan/apply resource actions eas Use assertions and the test runner's output for failures; do not add separate reports, host inventories, or project-tracking metadata to tests. Keep inference requests, fault injection, and recovery checks in explicitly named scenarios. -- [Run integration tests](integration-tests.md) with explicit OpenTofu and bundle paths. +- [Run integration tests](integration-tests.md) with an explicit bundle path. - [Run live tests](live-tests.md) only against explicitly owned resources. ### SSH Engine Transport