//! E2E del CLI `hammer bootstrap stage0` (track posterior, SDD 11). Construye un tarball de //! semilla falso, lo ingiere vía `file://` (sin red) y verifica el camino completo: //! sella + imprime el hash, es idempotente, y un sha256 erróneo falla sin sellar. use std::path::Path; use std::process::Command; const BIN: &str = env!("CARGO_BIN_EXE_hammer"); fn sha256_hex(bytes: &[u8]) -> String { use sha2::{Digest, Sha256}; let mut h = Sha256::new(); h.update(bytes); hex::encode(h.finalize()) } /// Crea un tarball con `bin/zig` dentro y devuelve `(url file://, sha256 hex)`. fn make_seed_tarball(dir: &Path) -> (String, String) { let src = dir.join("src"); std::fs::create_dir_all(src.join("bin")).unwrap(); std::fs::write(src.join("bin/zig"), b"#!/bin/sh\necho fake zig\n").unwrap(); let tarball = dir.join("seed.tar.gz"); let st = Command::new("tar") .arg("-czf") .arg(&tarball) .arg("-C") .arg(&src) .arg(".") .status() .unwrap(); assert!(st.success()); let sha = sha256_hex(&std::fs::read(&tarball).unwrap()); (format!("file://{}", tarball.display()), sha) } fn stage0(store: &Path, url: &str, sha256: &str, version: &str) -> std::process::Output { Command::new(BIN) .arg("--store") .arg(store) .args(["bootstrap", "stage0", "--url"]) .arg(url) .args(["--sha256", sha256, "--version", version]) .output() .unwrap() } #[test] fn stage0_seals_idempotent_and_rejects_bad_sha() { let tmp = tempfile::tempdir().unwrap(); let (url, sha) = make_seed_tarball(tmp.path()); let store = tmp.path().join("store"); // --- primera ingestión: sella e imprime el hash --- let out = stage0(&store, &url, &sha, "0.13.0"); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); let hash = String::from_utf8(out.stdout).unwrap().trim().to_string(); assert!(hash.starts_with("b3:"), "hash inesperado: {hash}"); // El artefacto está en el store con el contenido extraído. let zig = store.join(format!("{}-seed-zig", hash.trim_start_matches("b3:"))).join("bin/zig"); assert!(zig.is_file(), "bin/zig debe existir en el artefacto sellado"); // --- idempotente: mismo hash, sin re-sellar --- let out2 = stage0(&store, &url, &sha, "0.13.0"); assert!(out2.status.success()); assert_eq!(String::from_utf8(out2.stdout).unwrap().trim(), hash); // --- sha erróneo: falla y no sella un artefacto nuevo --- let bad = stage0(&store, &url, &"0".repeat(64), "bad"); assert!(!bad.status.success(), "un sha erróneo debe fallar"); assert!( String::from_utf8_lossy(&bad.stderr).contains("sha256 mismatch"), "stderr: {}", String::from_utf8_lossy(&bad.stderr) ); }