H1a (proof-carrying recipes): bloque evidence en receta y .swm
Frontera AI-nativa SDD 15 §H1. Agrega `Evidence { checks: Vec<EvidenceCheck> }` a Recipe
(TOML) y a Mutation::SourcePatch (.swm YAML): cada check es {kind, cmd, expected_exit,
expected_output?} con kind ∈ {cmd-exit, proptest, contract, kani} (estratos de confianza
crecientes, EvidenceKind: Ord). DECISIÓN CLAVE: la evidencia NO entra en hash_inputs —
certifica comportamiento, no identidad ⇒ no mueve el artifact_hash (baseline de
reproducibilidad intacto). Round-trip completo: from_recipe (forward) + swm_bridge
synthesize_recipe (reverse) + camino de pack (cli). verify_schema valida forma (cmd no
vacío, expected_output con prefijo b3:). El checker que EJECUTA la evidencia es H1b; el
cableado al Orchestrator VERIFY es H1c (marcado con evidence: _). Tests: recipe + swm,
incl. que la evidencia no cambia el hash. Sin warnings clippy nuevos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,7 @@ impl<T: IntentTranslator> Orchestrator<T> {
|
||||
target_bin,
|
||||
expected_hash,
|
||||
deps: _,
|
||||
evidence: _, // H1c cableará la ejecución de la evidencia en el paso VERIFY
|
||||
} => {
|
||||
let CompileMode::ViaBus { store, timeout, .. } = &self.compile else {
|
||||
skipped_source_patches += 1;
|
||||
|
||||
@@ -73,14 +73,14 @@ pub fn recipe_from_source_patch(
|
||||
name: Option<&str>,
|
||||
catalog_dir: &Path,
|
||||
) -> hammer_core::Result<Recipe> {
|
||||
let (repo, commit, tarball, sha256, strip_components, patch, patch_url, build_cfg, target_bin, deps) =
|
||||
let (repo, commit, tarball, sha256, strip_components, patch, patch_url, build_cfg, target_bin, deps, evidence) =
|
||||
match mutation {
|
||||
Mutation::SourcePatch {
|
||||
repo, commit, tarball, sha256, strip_components, patch, patch_url, build,
|
||||
target_bin, deps, ..
|
||||
target_bin, deps, evidence, ..
|
||||
} => (
|
||||
repo, commit, tarball, sha256, strip_components, patch, patch_url, build,
|
||||
target_bin, deps,
|
||||
target_bin, deps, evidence,
|
||||
),
|
||||
_ => {
|
||||
return Err(hammer_core::Error::Recipe(
|
||||
@@ -133,6 +133,7 @@ pub fn recipe_from_source_patch(
|
||||
build_cfg,
|
||||
*strip_components,
|
||||
deps,
|
||||
evidence,
|
||||
patches,
|
||||
catalog_dir,
|
||||
)
|
||||
@@ -156,6 +157,7 @@ fn synthesize_recipe(
|
||||
build_cfg: &SwmBuild,
|
||||
strip_components: Option<usize>,
|
||||
deps: &hammer_core::Deps,
|
||||
evidence: &hammer_core::Evidence,
|
||||
patches: Vec<String>,
|
||||
base_dir: &Path,
|
||||
) -> hammer_core::Result<Recipe> {
|
||||
@@ -213,6 +215,9 @@ flags = []
|
||||
// necesitan, pero la resolución de build-deps SÍ. `install` puebla el catálogo con los
|
||||
// `{dep}.toml` antes de construir; sin deps, el dir simplemente no se consulta.
|
||||
recipe.deps = deps.clone();
|
||||
// Evidencia de comportamiento (H1): viaja en el .swm y se reinyecta en la receta efímera para
|
||||
// que el checker la vea al reproducir. No afecta el hash del artefacto (comportamiento ≠ identidad).
|
||||
recipe.evidence = evidence.clone();
|
||||
recipe.base_dir = base_dir.to_path_buf();
|
||||
Ok(recipe)
|
||||
}
|
||||
@@ -272,6 +277,7 @@ mod tests {
|
||||
&fake_swm_build(),
|
||||
None,
|
||||
&hammer_core::Deps::default(),
|
||||
&hammer_core::Evidence::default(),
|
||||
vec![],
|
||||
d.path(),
|
||||
)
|
||||
@@ -300,6 +306,7 @@ mod tests {
|
||||
&fake_swm_build(),
|
||||
None,
|
||||
&hammer_core::Deps::default(),
|
||||
&hammer_core::Evidence::default(),
|
||||
vec![],
|
||||
d.path(),
|
||||
)
|
||||
@@ -334,6 +341,7 @@ mod tests {
|
||||
target_bin: "/usr/bin/jq".into(),
|
||||
expected_hash: None,
|
||||
deps: Default::default(),
|
||||
evidence: Default::default(),
|
||||
};
|
||||
let recipe = recipe_from_source_patch(&m, None, &catalog).unwrap();
|
||||
assert_eq!(recipe.source.patches.len(), 1);
|
||||
@@ -388,6 +396,7 @@ mod tests {
|
||||
target_bin: "/bin/x".into(),
|
||||
expected_hash: None,
|
||||
deps: Default::default(),
|
||||
evidence: Default::default(),
|
||||
};
|
||||
let store = Store::open(d.path().join("store")).unwrap();
|
||||
let cfg = BuildConfig::defaults_for_store(store.root());
|
||||
|
||||
@@ -2350,6 +2350,7 @@ fn build_export_mutations(
|
||||
target_bin,
|
||||
expected_hash: Some(art_hash),
|
||||
deps: recipe.deps.clone(),
|
||||
evidence: recipe.evidence.clone(),
|
||||
});
|
||||
stats.source_patches += 1;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ pub mod swm;
|
||||
pub use caps::{AgentCapsConfig, CapRule};
|
||||
pub use hash::ArtifactHash;
|
||||
pub use installed::{InstalledDb, InstalledPackage};
|
||||
pub use recipe::{Compiler, Deps, LinkMode, Phases, Recipe, Source, SourceKind};
|
||||
pub use recipe::{
|
||||
Compiler, Deps, Evidence, EvidenceCheck, EvidenceKind, LinkMode, Phases, Recipe, Source,
|
||||
SourceKind,
|
||||
};
|
||||
pub use repo::{PackageEntry, RepoIndex};
|
||||
pub use sign::{KeyPair, SigStatus, TrustStore};
|
||||
pub use store::Store;
|
||||
|
||||
@@ -14,6 +14,11 @@ pub struct Recipe {
|
||||
pub build: Build,
|
||||
#[serde(default)]
|
||||
pub deps: Deps,
|
||||
/// Evidencia de COMPORTAMIENTO que acompaña la receta (H1 — proof-carrying recipes, SDD 15).
|
||||
/// Opcional. **No entra en `hash_inputs`**: certifica comportamiento, no identidad ⇒ agregar o
|
||||
/// cambiar evidencia NO mueve el `artifact_hash` (el baseline de reproducibilidad no se toca).
|
||||
#[serde(default, skip_serializing_if = "Evidence::is_empty")]
|
||||
pub evidence: Evidence,
|
||||
/// Directorio base contra el que se resuelven rutas relativas de la receta
|
||||
/// (típicamente, `patches`). Lo fija `load_from_path`; al deserializar puro queda vacío.
|
||||
#[serde(skip, default)]
|
||||
@@ -158,6 +163,100 @@ impl Deps {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evidencia de comportamiento de una receta (H1 — proof-carrying recipes, SDD 15 §H1). La IA
|
||||
/// entrega, junto al build, un conjunto de `checks` que el verificador (`hammer swm-verify
|
||||
/// --evidence`) corre DENTRO del sandbox reproducible; la mutación sólo se propone si todos pasan.
|
||||
/// La confianza vive en el checker (pequeño, auditable), no en el generador: el sistema mejora solo
|
||||
/// sin poder corromperse solo. Deliberadamente FUERA de `Recipe::hash_inputs` (comportamiento ≠
|
||||
/// identidad). Frontera honesta: garantiza que *lo declarado pasa*, no que *lo declarado basta*.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Evidence {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub checks: Vec<EvidenceCheck>,
|
||||
}
|
||||
|
||||
impl Evidence {
|
||||
/// `true` si no hay ningún check. Sirve a serde (`skip_serializing_if`) para no emitir el
|
||||
/// bloque `evidence` vacío en una receta/`.swm`.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.checks.is_empty()
|
||||
}
|
||||
|
||||
/// Valida la forma del bloque (schema, no ejecución): cada `cmd` no vacío y cada
|
||||
/// `expected_output`, si está, con prefijo `b3:`. El checker de H1b corre esto antes de ejecutar.
|
||||
pub fn validate(&self) -> crate::Result<()> {
|
||||
for (i, c) in self.checks.iter().enumerate() {
|
||||
if c.cmd.trim().is_empty() {
|
||||
return Err(crate::Error::Recipe(format!(
|
||||
"evidence.checks[{i}] ({}): `cmd` vacío",
|
||||
c.kind.as_str()
|
||||
)));
|
||||
}
|
||||
if let Some(h) = &c.expected_output {
|
||||
if !h.starts_with("b3:") {
|
||||
return Err(crate::Error::Recipe(format!(
|
||||
"evidence.checks[{i}] ({}): `expected_output` debe ser un hash `b3:…`, es {h:?}",
|
||||
c.kind.as_str()
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Un ítem de evidencia: un comando + el resultado exigido. `kind` estratifica la CONFIANZA
|
||||
/// (`cmd-exit` < `proptest` < `contract` < `kani`), que se reporta tal cual — H1 no finge que todo
|
||||
/// es prueba formal.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EvidenceCheck {
|
||||
pub kind: EvidenceKind,
|
||||
/// Comando shell a correr en el sandbox reproducible tras el build (misma vía que las fases).
|
||||
pub cmd: String,
|
||||
/// Exit code exigido. Default 0.
|
||||
#[serde(default)]
|
||||
pub expected_exit: i32,
|
||||
/// Hash BLAKE3 (`b3:…`) opcional del stdout, para checks deterministas cuyo OUTPUT también se
|
||||
/// ancla (no sólo el exit). `None` ⇒ sólo se exige el exit code.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub expected_output: Option<String>,
|
||||
}
|
||||
|
||||
/// Estrato de confianza de un `EvidenceCheck`, en orden creciente (`Ord` = comparación de estratos).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum EvidenceKind {
|
||||
/// El comando sale con `expected_exit`. La garantía más débil (un smoke test).
|
||||
CmdExit,
|
||||
/// Property tests (proptest/quickcheck): pasan sobre entradas generadas.
|
||||
Proptest,
|
||||
/// Contratos / aserciones de pre y post-condición.
|
||||
Contract,
|
||||
/// Prueba formal acotada (Kani/Creusot). La garantía más fuerte.
|
||||
Kani,
|
||||
}
|
||||
|
||||
impl EvidenceKind {
|
||||
/// Nivel numérico de confianza (0 = más débil), para reportar/ordenar.
|
||||
pub fn level(&self) -> u8 {
|
||||
match self {
|
||||
EvidenceKind::CmdExit => 0,
|
||||
EvidenceKind::Proptest => 1,
|
||||
EvidenceKind::Contract => 2,
|
||||
EvidenceKind::Kani => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
EvidenceKind::CmdExit => "cmd-exit",
|
||||
EvidenceKind::Proptest => "proptest",
|
||||
EvidenceKind::Contract => "contract",
|
||||
EvidenceKind::Kani => "kani",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compilador del lab, POR RECETA (no global). `zig-cc` por defecto; escotilla a clang/gcc
|
||||
/// para paquetes con gcc-ismos. Ver ADR 0003.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
@@ -472,4 +571,107 @@ commit = "deadbeef"
|
||||
let r = Recipe::load_from_path(&recipe_path).unwrap();
|
||||
assert_eq!(r.base_dir, dir.path());
|
||||
}
|
||||
|
||||
const WITH_EVIDENCE: &str = r#"
|
||||
name = "grep"
|
||||
version = "3.11"
|
||||
[source]
|
||||
repo = "git://git.savannah.gnu.org/grep.git"
|
||||
commit = "a1b2c3d4e5f6"
|
||||
[build]
|
||||
[deps]
|
||||
build = ["pcre2"]
|
||||
|
||||
[[evidence.checks]]
|
||||
kind = "cmd-exit"
|
||||
cmd = "grep --version"
|
||||
|
||||
[[evidence.checks]]
|
||||
kind = "proptest"
|
||||
cmd = "cd /src && cargo test --release proptest_"
|
||||
expected_exit = 0
|
||||
|
||||
[[evidence.checks]]
|
||||
kind = "cmd-exit"
|
||||
cmd = "printf hola | grep -q hola"
|
||||
expected_output = "b3:deadbeef"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parse_evidence_block() {
|
||||
let r = Recipe::from_toml(WITH_EVIDENCE).expect("parse");
|
||||
assert_eq!(r.evidence.checks.len(), 3);
|
||||
assert_eq!(r.evidence.checks[0].kind, EvidenceKind::CmdExit);
|
||||
assert_eq!(r.evidence.checks[0].cmd, "grep --version");
|
||||
assert_eq!(r.evidence.checks[0].expected_exit, 0); // default
|
||||
assert_eq!(r.evidence.checks[1].kind, EvidenceKind::Proptest);
|
||||
assert_eq!(r.evidence.checks[2].expected_output.as_deref(), Some("b3:deadbeef"));
|
||||
r.evidence.validate().expect("schema válido");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evidence_absent_is_empty_and_omitted() {
|
||||
let r = Recipe::from_toml(SAMPLE).unwrap();
|
||||
assert!(r.evidence.is_empty());
|
||||
// `skip_serializing_if` ⇒ una receta sin evidencia no emite el bloque.
|
||||
let toml = r.to_toml().unwrap();
|
||||
assert!(!toml.contains("evidence"), "toml no debería traer `evidence`: {toml}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evidence_does_not_affect_artifact_hash() {
|
||||
// La clave de H1a: la evidencia certifica COMPORTAMIENTO, no IDENTIDAD ⇒ el hash del
|
||||
// artefacto es idéntico con y sin bloque evidence (el baseline de reproducibilidad no se mueve).
|
||||
// Comparamos LA MISMA receta con y sin evidencia (clon + limpieza), no dos recetas distintas.
|
||||
let con = Recipe::from_toml(WITH_EVIDENCE).unwrap();
|
||||
let mut sin = con.clone();
|
||||
sin.evidence = Evidence::default();
|
||||
assert!(con.evidence.checks.len() == 3 && sin.evidence.is_empty());
|
||||
assert_eq!(
|
||||
sin.hash_inputs(&[]).unwrap(),
|
||||
con.hash_inputs(&[]).unwrap(),
|
||||
"la evidencia NO debe entrar en hash_inputs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evidence_validate_rejects_empty_cmd_and_bad_hash() {
|
||||
let bad_cmd = r#"
|
||||
name = "x"
|
||||
version = "0"
|
||||
[source]
|
||||
repo = "git://x"
|
||||
commit = "deadbeef"
|
||||
[build]
|
||||
[[evidence.checks]]
|
||||
kind = "cmd-exit"
|
||||
cmd = " "
|
||||
"#;
|
||||
let r = Recipe::from_toml(bad_cmd).unwrap();
|
||||
assert!(r.evidence.validate().unwrap_err().to_string().contains("vacío"));
|
||||
|
||||
let bad_hash = r#"
|
||||
name = "x"
|
||||
version = "0"
|
||||
[source]
|
||||
repo = "git://x"
|
||||
commit = "deadbeef"
|
||||
[build]
|
||||
[[evidence.checks]]
|
||||
kind = "cmd-exit"
|
||||
cmd = "true"
|
||||
expected_output = "sha256:zzz"
|
||||
"#;
|
||||
let r = Recipe::from_toml(bad_hash).unwrap();
|
||||
assert!(r.evidence.validate().unwrap_err().to_string().contains("b3:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evidence_kind_levels_are_stratified() {
|
||||
assert!(EvidenceKind::CmdExit < EvidenceKind::Proptest);
|
||||
assert!(EvidenceKind::Proptest < EvidenceKind::Contract);
|
||||
assert!(EvidenceKind::Contract < EvidenceKind::Kani);
|
||||
assert_eq!(EvidenceKind::CmdExit.level(), 0);
|
||||
assert_eq!(EvidenceKind::Kani.level(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,11 @@ pub enum Mutation {
|
||||
/// Vacío ⇒ paquete autocontenido (el caso común, binarios estáticos sin deps).
|
||||
#[serde(default, skip_serializing_if = "crate::recipe::Deps::is_empty")]
|
||||
deps: crate::recipe::Deps,
|
||||
/// Evidencia de comportamiento que acompaña al paquete (H1 — proof-carrying recipes). El
|
||||
/// receptor puede correr `hammer swm-verify --evidence` para reproducir cada check en el
|
||||
/// sandbox antes de aceptar la mutación. Vacío ⇒ paquete sin evidencia (sólo reproducibilidad).
|
||||
#[serde(default, skip_serializing_if = "crate::recipe::Evidence::is_empty")]
|
||||
evidence: crate::recipe::Evidence,
|
||||
},
|
||||
ConfigEdit {
|
||||
file: String,
|
||||
@@ -290,6 +295,7 @@ impl Swm {
|
||||
target_bin: target_bin.into(),
|
||||
expected_hash,
|
||||
deps: recipe.deps.clone(),
|
||||
evidence: recipe.evidence.clone(),
|
||||
}],
|
||||
signature: None,
|
||||
};
|
||||
@@ -326,7 +332,7 @@ impl Mutation {
|
||||
use crate::recipe::SourceKind;
|
||||
match self {
|
||||
Mutation::SourcePatch {
|
||||
repo, commit, tarball, sha256, patch, patch_url, target_bin, ..
|
||||
repo, commit, tarball, sha256, patch, patch_url, target_bin, evidence, ..
|
||||
} => {
|
||||
match swm_source_kind(
|
||||
repo.as_deref(),
|
||||
@@ -361,6 +367,9 @@ impl Mutation {
|
||||
"source_patch: target_bin debe ser ruta absoluta, no '{target_bin}'"
|
||||
));
|
||||
}
|
||||
// La evidencia (H1) viaja con el paquete: validamos su FORMA acá (cmd no vacío,
|
||||
// hash bien formado). La EJECUCIÓN la hace `hammer swm-verify --evidence` (H1b).
|
||||
evidence.validate().map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
Mutation::ConfigEdit { file, inline_diff } => {
|
||||
@@ -578,6 +587,7 @@ mutations:
|
||||
target_bin: "/bin/x".into(),
|
||||
expected_hash: None,
|
||||
deps: Default::default(),
|
||||
evidence: Default::default(),
|
||||
}],
|
||||
signature: None,
|
||||
};
|
||||
@@ -608,6 +618,7 @@ mutations:
|
||||
target_bin: "/bin/grep".into(),
|
||||
expected_hash: None,
|
||||
deps: Default::default(),
|
||||
evidence: Default::default(),
|
||||
}],
|
||||
signature: None,
|
||||
};
|
||||
@@ -635,6 +646,7 @@ mutations:
|
||||
target_bin: "/bin/x".into(),
|
||||
expected_hash: None,
|
||||
deps: Default::default(),
|
||||
evidence: Default::default(),
|
||||
};
|
||||
let err = m.verify_schema().unwrap_err();
|
||||
assert!(err.contains("no ambos"), "mensaje inesperado: {err}");
|
||||
@@ -735,8 +747,89 @@ zig_version = "0.13.0"
|
||||
target_bin: "/bin/x".into(),
|
||||
expected_hash: None,
|
||||
deps: Default::default(),
|
||||
evidence: Default::default(),
|
||||
};
|
||||
let err = m.verify_schema().unwrap_err();
|
||||
assert!(err.contains("faltan campos"), "mensaje inesperado: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_recipe_carries_evidence() {
|
||||
// H1a: la evidencia de la receta viaja al .swm (source_patch), roundtripea por YAML,
|
||||
// y verify_schema la valida.
|
||||
let toml = r#"
|
||||
name = "grep"
|
||||
version = "3.11"
|
||||
[source]
|
||||
tarball = "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz"
|
||||
sha256 = "deadbeef"
|
||||
[build]
|
||||
[[evidence.checks]]
|
||||
kind = "cmd-exit"
|
||||
cmd = "grep --version"
|
||||
[[evidence.checks]]
|
||||
kind = "proptest"
|
||||
cmd = "cd /src && cargo test proptest_"
|
||||
expected_exit = 0
|
||||
"#;
|
||||
let recipe = crate::Recipe::from_toml(toml).unwrap();
|
||||
let swm = Swm::from_recipe(&recipe, "/usr/bin/grep", None, None, "dev").unwrap();
|
||||
// La evidencia llegó a la mutación.
|
||||
match &swm.mutations[0] {
|
||||
Mutation::SourcePatch { evidence, .. } => {
|
||||
assert_eq!(evidence.checks.len(), 2);
|
||||
assert_eq!(evidence.checks[0].kind, crate::recipe::EvidenceKind::CmdExit);
|
||||
assert_eq!(evidence.checks[1].kind, crate::recipe::EvidenceKind::Proptest);
|
||||
}
|
||||
other => panic!("esperaba SourcePatch, obtuve {other:?}"),
|
||||
}
|
||||
// Roundtrip YAML: el bloque evidence sobrevive.
|
||||
let yaml = swm.to_yaml().unwrap();
|
||||
assert!(yaml.contains("evidence"), "el YAML debe traer evidence: {yaml}");
|
||||
let back = Swm::from_yaml(&yaml).unwrap();
|
||||
match &back.mutations[0] {
|
||||
Mutation::SourcePatch { evidence, .. } => assert_eq!(evidence.checks.len(), 2),
|
||||
other => panic!("esperaba SourcePatch, obtuve {other:?}"),
|
||||
}
|
||||
back.verify_schema().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_schema_rejects_evidence_with_empty_cmd() {
|
||||
let yaml = r#"
|
||||
swm_version: 1
|
||||
base:
|
||||
distro_version: "dev"
|
||||
mutations:
|
||||
- type: source_patch
|
||||
tarball: "https://x/a.tar.gz"
|
||||
sha256: "deadbeef"
|
||||
build: {}
|
||||
target_bin: "/usr/bin/x"
|
||||
evidence:
|
||||
checks:
|
||||
- kind: cmd-exit
|
||||
cmd: " "
|
||||
"#;
|
||||
let swm = Swm::from_yaml(yaml).unwrap();
|
||||
let err = swm.verify_schema().unwrap_err().to_string();
|
||||
assert!(err.contains("vacío"), "mensaje inesperado: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_patch_without_evidence_omits_block() {
|
||||
// skip_serializing_if ⇒ un paquete sin evidencia no emite el bloque (compat hacia atrás).
|
||||
let toml = r#"
|
||||
name = "x"
|
||||
version = "0"
|
||||
[source]
|
||||
repo = "git://x"
|
||||
commit = "deadbeef"
|
||||
[build]
|
||||
"#;
|
||||
let recipe = crate::Recipe::from_toml(toml).unwrap();
|
||||
let swm = Swm::from_recipe(&recipe, "/usr/bin/x", None, None, "dev").unwrap();
|
||||
let yaml = swm.to_yaml().unwrap();
|
||||
assert!(!yaml.contains("evidence"), "no debería emitir evidence: {yaml}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +379,7 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
|
||||
target_bin: format!("/usr/bin/{name}"),
|
||||
expected_hash: None,
|
||||
deps: Default::default(),
|
||||
evidence: Default::default(),
|
||||
};
|
||||
let store = match hammer_core::Store::open(&store_root) {
|
||||
Ok(s) => s,
|
||||
|
||||
Reference in New Issue
Block a user