Cablea el Stage 0 del bootstrap a la CLI: subcomando `bootstrap stage0 --url --sha256 --version [--seed zig|musl-cross-make]` que abre el store, arma el SeedSpec y llama a hammer_bootstrap::stage0, imprimiendo el b3 del artefacto sellado. - stage0 ahora limpia el staging `.bootstrap-tmp` pase lo que pase (extraído stage0_into): un sha256 erróneo ya no deja el tarball a medio descargar bajo el store. Test reforzado para afirmarlo. - e2e de CLL `bootstrap_cli.rs`: tarball falso vía file:// → sella + imprime hash, idempotente, sha erróneo falla sin sellar. Sin red. Docs SDD 11 §5 y roadmap marcan Stage 0 (lib+CLI) hecho; Stage 1/2 pendientes. Workspace 219 verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
2.8 KiB
Rust
77 lines
2.8 KiB
Rust
//! 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)
|
|
);
|
|
}
|