server/cli/tests/promote_cli.rs
//! **`holger-server promote` end-to-end robot** — the LOCAL promotion verb, driven
//! as an operator drives it: spawn the real binary, inject args, assert the
//! observable verdict in stdout + the bytes on disk.
//!
//! `PromotionService.PromoteArtifact` had a real engine and tests behind it but NO
//! consumer in any shipped binary — an operator could not trigger a DEV→PROD
//! promotion through anything. This test is the proof that the wired CLI path
//! actually promotes, and that its two refusals are honest:
//!
//! * `promote --execute` SEALS into the destination's immutable PROD store, at the
//! blake3 content address, with the OS user recorded as `promoted_by`.
//! * the DEFAULT (no `--execute`) is a dry run that writes **nothing** — the
//! irreversibility guard.
//! * a policy-gate BLOCK refuses with the artifact never sealed, and the refusal
//! carries the **typed** `holger-promote-2` code (assert the CODE, never a
//! prose substring — the message is free to be reworded, the code is frozen).
//! * a missing source artifact refuses with the typed `holger-promote-7`.
use std::path::Path;
use std::process::Command;
/// The package's own binary, provided by cargo to integration tests.
const BIN: &str = env!("CARGO_BIN_EXE_holger");
/// A one-repo file-backed store config. `audit.dir` is ABSOLUTE so the data root
/// (its parent) — and therefore the PROD store — is deterministic under the test's
/// tempdir instead of the process CWD.
fn write_config(path: &Path, repo: &str, data_dir: &Path, audit_dir: &Path) {
write_config_with(path, repo, data_dir, audit_dir, "")
}
/// [`write_config`] plus EXTRA RON — a `promotion:` block, which is where the
/// policy gate lives since 2026-09-20 (it used to be three environment
/// variables, which an appliance with no shell cannot set).
fn write_config_with(path: &Path, repo: &str, data_dir: &Path, audit_dir: &Path, extra: &str) {
let ron = format!(
r#"(
exposed_endpoints: [ ( ron_name: "e", ron_url: "127.0.0.1:0", ron_http_url: None ) ],
storage_endpoints: [],
repositories: [
( ron_name: "{repo}", ron_repo_type: "rust", ron_data_dir: Some("{data}"), ron_base_url: Some("http://x"), ron_upstreams: [], ron_in: None, ron_out: None ),
],
audit: ( dir: "{audit}", enabled: true ),
{extra}
)
"#,
data = data_dir.display(),
audit = audit_dir.display(),
extra = extra,
);
std::fs::write(path, ron).expect("write config");
}
/// [`write_config_with`] for a config that declares SEVERAL repositories — what a
/// ladder needs, since every rung's repository must be declared in the config
/// that owns the ladder.
fn write_config_repos(
path: &Path,
repos: &[(&str, std::path::PathBuf)],
audit_dir: &Path,
extra: &str,
) {
let decls: String = repos
.iter()
.map(|(name, dir)| {
format!(
" ( ron_name: \"{name}\", ron_repo_type: \"rust\", ron_data_dir: Some(\"{}\"), ron_base_url: Some(\"http://x\"), ron_upstreams: [], ron_in: None, ron_out: None ),\n",
dir.display()
)
})
.collect();
let ron = format!(
r#"(
exposed_endpoints: [ ( ron_name: "e", ron_url: "127.0.0.1:0", ron_http_url: None ) ],
storage_endpoints: [],
repositories: [
{decls} ],
audit: ( dir: "{audit}", enabled: true ),
{extra}
)
"#,
audit = audit_dir.display(),
);
std::fs::write(path, ron).expect("write config");
}
/// Seed one artifact into a file-backed store through the SAME engine the server
/// uses (no hand-laid files that could drift from the backend's real layout).
fn seed(config: &Path, repo: &str, name: &str, version: &str, body: &[u8]) {
let mut holger = server_lib::read_ron_config(config).expect("parse seed config");
holger.instantiate_backends().expect("backends");
server_lib::wire_holger(&mut holger).expect("wire");
let id = server_lib::ArtifactId {
namespace: None,
name: name.into(),
version: version.into(),
};
holger
.put(repo, &id, body)
.expect("seed artifact into source repo");
}
/// Run `holger-server promote …` with extra env, returning (success, merged output).
fn promote(args: &[&str], env: &[(&str, &str)]) -> (bool, String) {
let mut cmd = Command::new(BIN);
cmd.arg("promote").args(args);
// Neutralise any ambient gate config from the developer's shell so each test
// sees exactly the gates it declares.
for k in [
"HOLGER_PROMOTE_ENFORCE",
"HOLGER_PROMOTE_ADVISORY_DB",
"HOLGER_PROMOTE_DENY_LICENSES",
"HOLGER_PROMOTE_REQUIRE_PROPERTY",
"HOLGER_DATA",
] {
cmd.env_remove(k);
}
for (k, v) in env {
cmd.env(k, v);
}
let out = cmd.output().expect("spawn holger-server promote");
let merged = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
(out.status.success(), merged)
}
/// The sealed-ref path the engine writes:
/// `<prod>/refs/<to_repo>/<namespace>__<name>__<version>.ref` (the engine's
/// `coord_key` shape; namespace empty here, hence the leading `__`).
fn ref_path(prod_root: &Path, to_repo: &str, name: &str, version: &str) -> std::path::PathBuf {
prod_root
.join("refs")
.join(to_repo)
.join(format!("__{name}__{version}.ref"))
}
/// A DEV store + a PROD store + a seeded artifact — the fixture every case starts from.
struct Fixture {
_dir: tempfile::TempDir,
dev_cfg: std::path::PathBuf,
prod_cfg: std::path::PathBuf,
prod_root: std::path::PathBuf,
body: &'static [u8],
}
fn fixture() -> Fixture {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let dev_cfg = root.join("dev.ron");
let prod_cfg = root.join("prod.ron");
write_config(
&dev_cfg,
"mirror",
&root.join("dev/data"),
&root.join("dev/audit"),
);
write_config(
&prod_cfg,
"mirror",
&root.join("prod/data"),
&root.join("prod/audit"),
);
let body: &'static [u8] = b"serde-1.0.0-bytes";
seed(&dev_cfg, "mirror", "serde", "1.0.0", body);
Fixture {
prod_root: root.join("prod").join("holger-prod-store"),
_dir: dir,
dev_cfg,
prod_cfg,
body,
}
}
/// **The wiring proof.** `promote --execute` moves the artifact DEV → PROD: the ref
/// is sealed under the destination repo, the blob lands at its blake3 content
/// address, and the reported address is that hash. RED if the verb stops sealing,
/// seals to the wrong coordinate, or garbles the bytes.
#[test]
fn promote_execute_seals_the_artifact_into_the_prod_store_at_its_content_address() {
let f = fixture();
let expected = blake3::hash(f.body).to_hex().to_string();
let (ok, out) = promote(
&[
"--from",
f.dev_cfg.to_str().unwrap(),
"--to",
f.prod_cfg.to_str().unwrap(),
"--repo",
"mirror",
"--name",
"serde",
"--version",
"1.0.0",
"--execute",
],
&[],
);
assert!(ok, "promote --execute must succeed; output:\n{out}");
assert!(out.contains("SEALED"), "reports the seal; output:\n{out}");
assert!(
out.contains(&expected),
"reports the blake3 content address {expected}; output:\n{out}"
);
// The ref IS the seal.
let r = ref_path(&f.prod_root, "mirror", "serde", "1.0.0");
assert!(r.is_file(), "sealed ref must exist at {}", r.display());
let record = std::fs::read_to_string(&r).expect("read ref");
assert!(
record.contains(&expected),
"the promotion record carries the content address: {record}"
);
// The bytes are stored under the content address, byte-identical.
let blob = f.prod_root.join("blobs").join(&expected);
assert!(
blob.is_file(),
"content-addressed blob must exist at {}",
blob.display()
);
assert_eq!(
std::fs::read(&blob).unwrap(),
f.body,
"promoted bytes must be byte-identical"
);
}
/// **Dry-run is the DEFAULT and writes NOTHING.** A promotion is irreversible, so
/// omitting `--execute` must preview only. RED if the default ever starts sealing.
#[test]
fn promote_without_execute_is_a_dry_run_that_writes_nothing() {
let f = fixture();
let (ok, out) = promote(
&[
"--from",
f.dev_cfg.to_str().unwrap(),
"--to",
f.prod_cfg.to_str().unwrap(),
"--repo",
"mirror",
"--name",
"serde",
"--version",
"1.0.0",
],
&[],
);
assert!(ok, "a dry run is a success, not an error; output:\n{out}");
assert!(
out.contains("dry-run"),
"says it is a dry run; output:\n{out}"
);
assert!(
out.contains("would seal"),
"previews the seal; output:\n{out}"
);
assert!(
!out.contains("SEALED"),
"must NOT claim a seal; output:\n{out}"
);
// The load-bearing half: nothing on disk.
assert!(
!ref_path(&f.prod_root, "mirror", "serde", "1.0.0").exists(),
"dry run must not write the ref"
);
let blob = f
.prod_root
.join("blobs")
.join(blake3::hash(f.body).to_hex().to_string());
assert!(!blob.exists(), "dry run must not write the blob");
}
/// The OSV feed this suite blocks on: it names exactly the coordinate the
/// fixture promotes.
const OSV_FEED: &str = r#"{
"id": "OSV-TEST-0001",
"summary": "test advisory that must block the promotion",
"affected": [
{
"package": { "ecosystem": "crates.io", "name": "serde" },
"versions": ["1.0.0"]
}
],
"severity": [ { "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" } ]
}"#;
/// **A policy BLOCK refuses, and says so with the FROZEN code.** The destination
/// declares a gated rung and an advisory feed naming this exact name@version, so
/// the gate must refuse BEFORE anything is sealed. Asserts the typed
/// `holger-promote-2` (`PROMOTE_POLICY_BLOCKED`) — the code is the contract; the
/// prose is not.
///
/// **The gate is declared in the CONFIG, not in the environment.** It moved there
/// on 2026-09-20 (`Holger::promotion_engine`): an appliance has no shell. This
/// test was written against the environment spelling and therefore stopped
/// gating anything — measured on main `1138143`, it printed `SEALED`. The
/// environment spelling is now a refusal of its own, below.
#[test]
fn a_policy_blocked_promotion_refuses_with_the_typed_code_and_seals_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let osv = root.join("advisories.json");
std::fs::write(&osv, OSV_FEED).expect("write advisory feed");
// The destination's OWN policy: one gated rung, running the `osv` gate over
// that feed. This is the shape a real deployment writes.
let dev_cfg = root.join("dev.ron");
let prod_cfg = root.join("prod.ron");
write_config(
&dev_cfg,
"mirror",
&root.join("dev/data"),
&root.join("dev/audit"),
);
write_config_repos(
&prod_cfg,
&[
("mirror", root.join("prod/mirror")),
("release", root.join("prod/data")),
],
&root.join("prod/audit"),
&format!(
r#"promotion: Some((
ladder_name: "rust",
stages: [ ( name: "dev", repo: "mirror" ), ( name: "prod", repo: "release", gated: true, gates: ["osv"] ) ],
advisory_db: Some("{osv}"),
)),"#,
osv = osv.display()
),
);
let body: &[u8] = b"serde-1.0.0-bytes";
seed(&dev_cfg, "mirror", "serde", "1.0.0", body);
let prod_root = root.join("prod").join("holger-prod-store");
let (ok, out) = promote(
&[
"--from",
dev_cfg.to_str().unwrap(),
"--to",
prod_cfg.to_str().unwrap(),
"--repo",
"mirror",
"--to-repo",
"release",
"--name",
"serde",
"--version",
"1.0.0",
"--execute",
],
&[],
);
assert!(
!ok,
"a blocked promotion must FAIL, not succeed quietly; output:\n{out}"
);
assert!(
!out.contains("SEALED"),
"nothing may be sealed; output:\n{out}"
);
assert!(
out.contains(server_lib::errcode::PROMOTE_POLICY_BLOCKED.code),
"refusal must carry the typed code {}; output:\n{out}",
server_lib::errcode::PROMOTE_POLICY_BLOCKED.code
);
assert!(
out.contains("OSV-TEST-0001"),
"refusal names the finding that blocked it; output:\n{out}"
);
// Fail-closed: a refused promotion writes NOTHING.
assert!(
!ref_path(&prod_root, "release", "serde", "1.0.0").exists(),
"a blocked promotion must not seal the ref"
);
assert!(
!prod_root
.join("blobs")
.join(blake3::hash(body).to_hex().to_string())
.exists(),
"a blocked promotion must not write the blob"
);
}
/// ★ **Asking for the gate the OLD way refuses — it does not seal unscanned.**
///
/// `HOLGER_PROMOTE_ENFORCE` and friends no longer configure anything: the keys
/// live in the RON `promotion:` block. A caller still setting them asked for
/// enforcement this run would not perform, and until 2026-09-22 the verb sealed
/// anyway and said `SEALED`. Unreachable configuration that reads as enforcement
/// is worse than none, so the verb refuses by name and writes nothing.
///
/// RED-when-broken: drop the check in `run_promote` and this test gets `SEALED`.
#[test]
fn the_retired_environment_spelling_refuses_rather_than_sealing_unscanned() {
let f = fixture();
let osv = f._dir.path().join("advisories.json");
std::fs::write(&osv, OSV_FEED).expect("write advisory feed");
let (ok, out) = promote(
&[
"--from",
f.dev_cfg.to_str().unwrap(),
"--to",
f.prod_cfg.to_str().unwrap(),
"--repo",
"mirror",
"--name",
"serde",
"--version",
"1.0.0",
"--execute",
],
&[
("HOLGER_PROMOTE_ENFORCE", "block"),
("HOLGER_PROMOTE_ADVISORY_DB", osv.to_str().unwrap()),
],
);
assert!(!ok, "an unenforceable request must FAIL; output:\n{out}");
assert!(
!out.contains("SEALED"),
"nothing may be sealed; output:\n{out}"
);
assert!(
out.contains(server_lib::errcode::PROMOTE_POLICY_BLOCKED.code),
"the refusal carries the typed code; output:\n{out}"
);
assert!(
out.contains("HOLGER_PROMOTE_ENFORCE") && out.contains("promotion:"),
"the refusal names the keys and where they live now; output:\n{out}"
);
assert!(
!ref_path(&f.prod_root, "mirror", "serde", "1.0.0").exists(),
"an unenforceable promotion must not seal"
);
}
/// **A missing source artifact refuses with the FROZEN code.** The served path
/// answers `not_found`; the local path raises `holger-promote-7` with the artifact
/// and repo named, instead of a bare "no such file".
#[test]
fn promoting_an_artifact_absent_from_the_source_repo_refuses_with_the_typed_code() {
let f = fixture();
let (ok, out) = promote(
&[
"--from",
f.dev_cfg.to_str().unwrap(),
"--to",
f.prod_cfg.to_str().unwrap(),
"--repo",
"mirror",
"--name",
"ghost",
"--version",
"9.9.9",
"--execute",
],
&[],
);
assert!(!ok, "a missing artifact must FAIL; output:\n{out}");
assert!(
out.contains(server_lib::errcode::PROMOTE_SOURCE_NOT_FOUND.code),
"refusal must carry the typed code {}; output:\n{out}",
server_lib::errcode::PROMOTE_SOURCE_NOT_FOUND.code
);
assert!(out.contains("ghost"), "names the artifact; output:\n{out}");
assert!(
out.contains("mirror"),
"names the source repo; output:\n{out}"
);
}
/// **Immutability holds through the CLI.** A second promotion of a coordinate that
/// is already sealed is refused with `holger-promote-1`, and the original ref is
/// left untouched — the local path cannot overwrite a sealed generation.
#[test]
fn re_promoting_a_sealed_coordinate_is_refused_as_already_promoted() {
let f = fixture();
let args = [
"--from",
f.dev_cfg.to_str().unwrap(),
"--to",
f.prod_cfg.to_str().unwrap(),
"--repo",
"mirror",
"--name",
"serde",
"--version",
"1.0.0",
"--execute",
];
let (ok, out) = promote(&args, &[]);
assert!(ok, "first promotion seals; output:\n{out}");
let r = ref_path(&f.prod_root, "mirror", "serde", "1.0.0");
let first = std::fs::read_to_string(&r).expect("read sealed ref");
let (ok2, out2) = promote(&args, &[]);
assert!(
!ok2,
"the second promotion must be REFUSED; output:\n{out2}"
);
assert!(
out2.contains(server_lib::errcode::PROMOTE_ALREADY_PROMOTED.code),
"refusal must carry the typed code {}; output:\n{out2}",
server_lib::errcode::PROMOTE_ALREADY_PROMOTED.code
);
assert_eq!(
std::fs::read_to_string(&r).expect("re-read ref"),
first,
"the sealed record must be untouched by the refused re-promotion"
);
}