diff --git a/hive-c0re/Cargo.toml b/hive-c0re/Cargo.toml index a32e5d1..747d918 100644 --- a/hive-c0re/Cargo.toml +++ b/hive-c0re/Cargo.toml @@ -20,3 +20,6 @@ tokio.workspace = true tokio-stream.workspace = true tracing.workspace = true tracing-subscriber.workspace = true + +[dev-dependencies] +tempfile = "3" diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index cd29476..deae694 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -735,6 +735,69 @@ async fn systemd_daemon_reload() -> Result<()> { Ok(()) } +#[cfg(test)] +mod tests { + use super::*; + + /// Regression test: setup_proposed must seed both agent.nix and flake.nix + /// in the initial commit. Before commit 5b5a93e flake.nix was missing from + /// the scaffold, requiring manual creation (seen with the damocles agent). + #[tokio::test] + async fn setup_proposed_seeds_flake_nix() { + let dir = tempfile::tempdir().expect("tempdir"); + let proposed = dir.path().join("proposed"); + setup_proposed(&proposed, "test-agent") + .await + .expect("setup_proposed"); + + // Both files must exist on disk. + assert!(proposed.join("agent.nix").exists(), "agent.nix missing"); + assert!(proposed.join("flake.nix").exists(), "flake.nix missing"); + + // flake.nix must export nixosModules.default (the meta-flake contract). + let flake = std::fs::read_to_string(proposed.join("flake.nix")).unwrap(); + assert!( + flake.contains("nixosModules.default"), + "flake.nix does not export nixosModules.default" + ); + + // Both files must be tracked in the initial git commit. + let out = git_command() + .current_dir(&proposed) + .args(["show", "--name-only", "--format=", "HEAD"]) + .output() + .await + .expect("git show"); + let tracked = String::from_utf8_lossy(&out.stdout); + assert!(tracked.contains("agent.nix"), "agent.nix not committed"); + assert!(tracked.contains("flake.nix"), "flake.nix not committed"); + } + + /// setup_proposed is idempotent: calling it on an existing repo is a + /// no-op (the fresh guard skips all writes). + #[tokio::test] + async fn setup_proposed_idempotent() { + let dir = tempfile::tempdir().expect("tempdir"); + let proposed = dir.path().join("proposed"); + setup_proposed(&proposed, "test-agent") + .await + .expect("first call"); + // Second call must not error even though .git already exists. + setup_proposed(&proposed, "test-agent") + .await + .expect("second call"); + // Still one commit. + let out = git_command() + .current_dir(&proposed) + .args(["rev-list", "--count", "HEAD"]) + .output() + .await + .expect("git rev-list"); + let count = String::from_utf8_lossy(&out.stdout).trim().to_owned(); + assert_eq!(count, "1", "expected exactly one commit after idempotent call"); + } +} + /// Idempotently rewrite the lines in `/etc/nixos-containers/.conf` /// that hive-c0re owns: `PRIVATE_NETWORK` (forced 0 so the agent's web UI port /// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind).