use super::*;
#[test]
fn resolve_msi_tools_threads_resolved_wix_tool_from_config() {
use anodizer_core::config::{Config, CrateConfig, MsiConfig};
let msi_cfg = MsiConfig {
wxs: Some("app.wxs".to_string()),
version: Some("v4".to_string()),
..Default::default()
};
let config = Config {
project_name: "myapp".to_string(),
crates: vec![CrateConfig {
name: "myapp".to_string(),
path: ".".to_string(),
tag_template: Some("v{{ .Version }}".to_string()),
msis: Some(vec![msi_cfg]),
..Default::default()
}],
..Default::default()
};
assert_eq!(resolve_msi_tools(Some(&config)), vec!["wix".to_string()]);
}
#[test]
fn resolve_msi_tools_none_config_is_empty() {
assert!(resolve_msi_tools(None).is_empty());
}
#[test]
fn resolve_upx_tools_threads_enabled_binary_from_config() {
use anodizer_core::config::{Config, StringOrBool, UpxConfig};
let config = Config {
project_name: "myapp".to_string(),
upx: vec![
UpxConfig {
enabled: Some(StringOrBool::Bool(true)),
binary: "upx".to_string(),
..Default::default()
},
UpxConfig {
enabled: Some(StringOrBool::Bool(false)),
binary: "other-upx".to_string(),
..Default::default()
},
],
..Default::default()
};
assert_eq!(resolve_upx_tools(Some(&config)), vec!["upx".to_string()]);
}
#[test]
fn resolve_upx_tools_none_config_is_empty() {
assert!(resolve_upx_tools(None).is_empty());
}
#[test]
fn resolve_upx_tools_force_requires_templated_enabled() {
use anodizer_core::config::{Config, StringOrBool, UpxConfig};
let config = Config {
project_name: "myapp".to_string(),
upx: vec![UpxConfig {
enabled: Some(StringOrBool::String(
"{{ if false }}true{{ else }}false{{ end }}".to_string(),
)),
binary: "upx".to_string(),
..Default::default()
}],
..Default::default()
};
assert_eq!(resolve_upx_tools(Some(&config)), vec!["upx".to_string()]);
}
#[test]
fn resolve_upx_tools_omits_literal_false_enabled() {
use anodizer_core::config::{Config, StringOrBool, UpxConfig};
let config = Config {
project_name: "myapp".to_string(),
upx: vec![UpxConfig {
enabled: Some(StringOrBool::Bool(false)),
binary: "upx".to_string(),
..Default::default()
}],
..Default::default()
};
assert!(resolve_upx_tools(Some(&config)).is_empty());
}
#[test]
fn parse_stages_default_returns_host_native_partition() {
let stages = parse_stages(None).expect("None is always Ok");
assert_eq!(stages, default_stages_for_host());
for base in [
StageId::Build,
StageId::Source,
StageId::Upx,
StageId::Archive,
StageId::Sbom,
StageId::Sign,
StageId::Checksum,
] {
assert!(stages.contains(&base), "base stage {base:?} missing");
}
}
#[test]
fn default_stages_for_host_includes_os_native_producers() {
let stages = default_stages_for_host();
assert!(
!stages.contains(&StageId::CargoPackage),
"cargo-package must never be in the host default"
);
#[cfg(target_os = "linux")]
for s in [
StageId::Nfpm,
StageId::Makeself,
StageId::Snapcraft,
StageId::Srpm,
StageId::Docker,
StageId::Appimage,
StageId::Flatpak,
] {
assert!(stages.contains(&s), "linux default missing {s:?}");
}
#[cfg(target_os = "macos")]
for s in [StageId::Appbundle, StageId::Dmg, StageId::Pkg] {
assert!(stages.contains(&s), "macos default missing {s:?}");
}
#[cfg(target_os = "windows")]
for s in [StageId::Msi, StageId::Nsis] {
assert!(stages.contains(&s), "windows default missing {s:?}");
}
}
#[test]
fn derived_stage_sets_are_subsets_of_stage_id_vocabulary() {
let vocab: std::collections::HashSet<StageId> = StageId::iter().collect();
for s in default_stages_for_host() {
assert!(
vocab.contains(&s),
"host-default stage {s:?} not in StageId vocabulary"
);
}
use anodizer_core::config::{Config, CrateConfig, NfpmConfig, SrpmConfig};
let config = Config {
crates: vec![CrateConfig {
name: "x".to_string(),
path: ".".to_string(),
nfpms: Some(vec![NfpmConfig::default()]),
..Default::default()
}],
srpms: Some(SrpmConfig::default()),
..Default::default()
};
let configured = anodizer_core::env_preflight::configured_producer_stages(&config);
assert!(configured.contains("nfpm") && configured.contains("srpm"));
for tok in &configured {
let s = StageId::from_token(tok)
.unwrap_or_else(|| panic!("producer token `{tok}` has no StageId variant"));
assert!(vocab.contains(&s));
}
let host: std::collections::HashSet<StageId> = default_stages_for_host().into_iter().collect();
#[cfg(target_os = "linux")]
for s in [
StageId::Nfpm,
StageId::Makeself,
StageId::Snapcraft,
StageId::Srpm,
StageId::Docker,
StageId::Appimage,
StageId::Flatpak,
] {
assert!(host.contains(&s), "linux derived default missing {s:?}");
}
#[cfg(target_os = "macos")]
for s in [StageId::Appbundle, StageId::Dmg, StageId::Pkg] {
assert!(host.contains(&s), "macos derived default missing {s:?}");
}
#[cfg(target_os = "windows")]
for s in [StageId::Msi, StageId::Nsis] {
assert!(host.contains(&s), "windows derived default missing {s:?}");
}
assert!(!host.contains(&StageId::CargoPackage));
}
#[test]
fn parse_stages_accepts_appimage_and_flatpak() {
let stages = parse_stages(Some("appimage,flatpak")).expect("both are known stages");
assert_eq!(stages, vec![StageId::Appimage, StageId::Flatpak]);
}
#[test]
fn host_default_excludes_unconfigured_producers() {
use anodizer_core::config::{Config, CrateConfig};
let config = Config {
project_name: "minimal".to_string(),
crates: vec![CrateConfig {
name: "minimal".to_string(),
path: ".".to_string(),
tag_template: Some("v{{ .Version }}".to_string()),
..Default::default()
}],
..Default::default()
};
let stages = host_default_for_config(Some(&config));
for base in ALWAYS_ON_STAGES {
assert!(stages.contains(base), "base stage {base:?} must remain");
}
for gated in [
StageId::Nfpm,
StageId::Makeself,
StageId::Snapcraft,
StageId::Srpm,
StageId::Docker,
StageId::Appimage,
StageId::Flatpak,
StageId::Appbundle,
StageId::Dmg,
StageId::Pkg,
StageId::Msi,
StageId::Nsis,
] {
assert!(
!stages.contains(&gated),
"unconfigured producer {gated:?} must be pruned from the default"
);
}
}
#[cfg(target_os = "linux")]
#[test]
fn host_default_includes_configured_linux_producers() {
use anodizer_core::config::{
AppImageConfig, Config, CrateConfig, DockerV2Config, FlatpakConfig, MakeselfConfig,
NfpmConfig, SnapcraftConfig, SrpmConfig,
};
let config = Config {
project_name: "full".to_string(),
crates: vec![CrateConfig {
name: "full".to_string(),
path: ".".to_string(),
tag_template: Some("v{{ .Version }}".to_string()),
nfpms: Some(vec![NfpmConfig::default()]),
snapcrafts: Some(vec![SnapcraftConfig::default()]),
flatpaks: Some(vec![FlatpakConfig::default()]),
dockers_v2: Some(vec![DockerV2Config::default()]),
..Default::default()
}],
appimages: vec![AppImageConfig::default()],
makeselfs: vec![MakeselfConfig::default()],
srpms: Some(SrpmConfig::default()),
..Default::default()
};
let stages = host_default_for_config(Some(&config));
for producer in [
StageId::Nfpm,
StageId::Makeself,
StageId::Snapcraft,
StageId::Srpm,
StageId::Docker,
StageId::Appimage,
StageId::Flatpak,
] {
assert!(
stages.contains(&producer),
"configured producer {producer:?} must stay in the default"
);
}
}
#[test]
fn host_default_none_config_is_full_partition() {
assert_eq!(host_default_for_config(None), default_stages_for_host());
}
#[test]
fn resolve_docker_configs_propagates_render_error() {
use anodizer_core::config::{Config, CrateConfig, DockerV2Config};
let config = Config {
project_name: "full".to_string(),
crates: vec![CrateConfig {
name: "full".to_string(),
path: ".".to_string(),
tag_template: Some("v{{ .Version }}".to_string()),
dockers_v2: Some(vec![DockerV2Config {
dockerfile: "{{ Version | this_filter_does_not_exist }}".to_string(),
..Default::default()
}]),
..Default::default()
}],
..Default::default()
};
let log = StageLogger::new("test", Verbosity::Quiet);
let err = resolve_docker_configs(Some(&config), Some("full"), true, &log)
.expect_err("a malformed dockerfile template must propagate, not resolve to empty");
let msg = format!("{err:#}");
assert!(
msg.contains("dockerfile"),
"the propagated error must be the dockerfile render failure (not a swallowed empty \
result nor an unrelated setup error): {msg}"
);
}
#[test]
fn resolve_docker_configs_truthy_skip_resolves_empty_without_error() {
use anodizer_core::config::{Config, CrateConfig, DockerV2Config, StringOrBool};
let config = Config {
project_name: "full".to_string(),
crates: vec![CrateConfig {
name: "full".to_string(),
path: ".".to_string(),
tag_template: Some("v{{ .Version }}".to_string()),
dockers_v2: Some(vec![DockerV2Config {
dockerfile: "Dockerfile.release".to_string(),
skip: Some(StringOrBool::Bool(true)),
..Default::default()
}]),
..Default::default()
}],
..Default::default()
};
let log = StageLogger::new("test", Verbosity::Quiet);
let resolved = resolve_docker_configs(Some(&config), Some("full"), true, &log)
.expect("a truthy skip must resolve cleanly (Ok), mirroring production");
assert!(
resolved.is_empty(),
"a skipped entry must produce no ResolvedDockerConfig: {resolved:?}"
);
}
#[test]
fn setup_env_release_mode_skips_token_gate_with_determinism_skip_stages() {
use anodizer_core::config::{Config, CrateConfig, ReleaseConfig};
use anodizer_core::context::{Context, ContextOptions};
let skip_stages: Vec<String> = anodizer_core::determinism_runner::SIDE_EFFECT_STAGES
.iter()
.map(|s| (*s).to_string())
.collect();
let config = Config {
project_name: "full".to_string(),
crates: vec![CrateConfig {
name: "full".to_string(),
path: ".".to_string(),
release: Some(ReleaseConfig::default()),
..Default::default()
}],
..Default::default()
};
let opts = ContextOptions {
snapshot: false,
skip_stages,
..Default::default()
};
let mut ctx = Context::new(config.clone(), opts);
ctx.set_env_source(anodizer_core::env_source::MapEnvSource::new());
assert!(
ctx.should_skip("release"),
"resolve_docker_configs's skip_stages must mirror the determinism child \
build's SIDE_EFFECT_STAGES"
);
let log = StageLogger::new("test", Verbosity::Quiet);
crate::commands::helpers::setup_env(&mut ctx, &config, &log).expect(
"a release-mode (tagged-HEAD) probe with skip_stages mirroring the \
determinism child build must not demand a GitHub token",
);
}
#[test]
fn classify_docker_stage_state_host_default_error_forces_not_declared() {
let log = StageLogger::new("test", Verbosity::Quiet);
let (configs, declared) = classify_docker_stage_state(
Err(anyhow::anyhow!("boom: could not resolve")),
true, false, &log,
)
.expect("a host-default resolve error must warn-and-skip, not hard-fail");
assert!(configs.is_empty(), "an errored resolve carries no configs");
assert!(
!declared,
"an errored host-default resolve must be reflected as NOT declared so the harness \
quiet-skips instead of emitting the misleading legit-skip warn"
);
}
#[test]
fn classify_docker_stage_state_explicit_error_hard_fails() {
let log = StageLogger::new("test", Verbosity::Quiet);
let res = classify_docker_stage_state(
Err(anyhow::anyhow!("boom: could not resolve")),
true,
true, &log,
);
assert!(
res.is_err(),
"an explicit docker request whose resolve errored must hard-fail, not skip"
);
}
#[test]
fn classify_docker_stage_state_ok_preserves_declared_and_configs() {
let log = StageLogger::new("test", Verbosity::Quiet);
let resolved = vec![ResolvedDockerConfig {
dockerfile: "FROM scratch\n".to_string(),
extra_files: Vec::new(),
build_args: Vec::new(),
}];
let (configs, declared) =
classify_docker_stage_state(Ok(resolved), true, true, &log).expect("Ok must pass through");
assert_eq!(configs.len(), 1, "resolved configs must be carried through");
assert!(declared, "a successful resolve must not alter `declared`");
}
#[test]
fn resolve_stages_explicit_ignores_config() {
use anodizer_core::config::{Config, CrateConfig};
let bare = Config {
crates: vec![CrateConfig {
name: "x".to_string(),
path: ".".to_string(),
..Default::default()
}],
..Default::default()
};
let stages = resolve_stages(Some("nfpm"), Some(&bare)).expect("nfpm is a known stage");
assert_eq!(stages, vec![StageId::Nfpm]);
}
#[test]
fn is_explicit_stage_selection_matches_nonblank_token_only() {
assert!(is_explicit_stage_selection(Some("msi")));
assert!(is_explicit_stage_selection(Some(" archive , checksum ")));
assert!(!is_explicit_stage_selection(None));
assert!(!is_explicit_stage_selection(Some("")));
assert!(!is_explicit_stage_selection(Some(" , , ")));
}
#[test]
fn parse_stages_subset_filters_to_named_set() {
let stages = parse_stages(Some("archive,checksum")).expect("all known stages");
assert_eq!(
stages.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vec!["archive", "checksum"]
);
}
#[test]
fn parse_stages_accepts_full_byte_stable_set() {
let stages = parse_stages(Some(
"build,source,upx,archive,nfpm,makeself,snapcraft,sbom,sign,checksum",
))
.expect("all stages in the action's Linux default must parse");
assert_eq!(
stages.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vec![
"build",
"source",
"upx",
"archive",
"nfpm",
"makeself",
"snapcraft",
"sbom",
"sign",
"checksum"
]
);
}
#[test]
fn parse_stages_errors_on_unknown_token() {
let err =
parse_stages(Some(" archive , bogus, checksum ")).expect_err("unknown token must error");
assert!(
err.contains("bogus") && err.contains("Known stages"),
"error must name the bad token and the legal vocabulary: {err}"
);
let err = parse_stages(Some("archve,nope")).expect_err("multiple unknowns must error");
assert!(
err.contains("archve") && err.contains("nope"),
"all unknown tokens must be named: {err}"
);
}
#[test]
fn parse_stages_tolerates_trailing_comma_and_whitespace() {
let stages = parse_stages(Some("archive,checksum,")).expect("trailing comma tolerated");
assert_eq!(
stages.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vec!["archive", "checksum"]
);
let stages = parse_stages(Some(" archive , , checksum ")).expect("empty middle tolerated");
assert_eq!(
stages.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vec!["archive", "checksum"]
);
}
#[test]
fn parse_stages_installers_umbrella_expands_to_full_set() {
let stages = parse_stages(Some("installers")).expect("umbrella token must parse");
assert_eq!(
stages.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vec!["nfpm", "makeself", "srpm", "msi", "nsis", "dmg", "pkg"]
);
}
#[test]
fn parse_stages_installers_dedupes_against_individual_members() {
let stages = parse_stages(Some("installers,msi")).expect("umbrella + individual must parse");
let names: Vec<&str> = stages.iter().map(|s| s.as_str()).collect();
assert_eq!(names.iter().filter(|n| **n == "msi").count(), 1);
}
#[test]
fn parse_stages_accepts_each_individual_installer_token() {
for token in ["msi", "nsis", "dmg", "pkg", "srpm"] {
let stages =
parse_stages(Some(token)).unwrap_or_else(|e| panic!("token `{token}` must parse: {e}"));
assert_eq!(
stages.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vec![token]
);
}
}
#[test]
fn parse_stages_accepts_appbundle_token() {
let stages = parse_stages(Some("appbundle,dmg")).expect("appbundle token must parse");
assert_eq!(
stages.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
vec!["appbundle", "dmg"]
);
}
#[test]
fn parse_stages_empty_string_falls_back_to_default() {
let expected = default_stages_for_host();
let stages = parse_stages(Some("")).expect("empty list returns default");
assert_eq!(stages, expected);
let stages = parse_stages(Some(" , , ")).expect("whitespace-only returns default");
assert_eq!(stages, expected);
}
#[test]
fn parse_targets_default_is_none() {
assert_eq!(parse_targets(None).unwrap(), None);
}
#[test]
fn parse_targets_subset_filters_to_named_list() {
let got = parse_targets(Some("x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu"))
.expect("ascii triples accepted");
assert_eq!(
got,
Some(vec![
"x86_64-unknown-linux-gnu".to_string(),
"aarch64-unknown-linux-gnu".to_string(),
])
);
}
#[test]
fn parse_targets_tolerates_trailing_comma_and_whitespace() {
let got = parse_targets(Some(" x86_64-apple-darwin , aarch64-apple-darwin , "))
.expect("trailing comma + spaces tolerated");
assert_eq!(
got,
Some(vec![
"x86_64-apple-darwin".to_string(),
"aarch64-apple-darwin".to_string(),
])
);
}
#[test]
fn parse_targets_errors_on_all_empty_csv() {
let err = parse_targets(Some("")).expect_err("empty CSV must error");
assert!(
err.contains("at least one entry"),
"error must explain the requirement: {err}"
);
let err = parse_targets(Some(" , , ")).expect_err("whitespace-only CSV must error");
assert!(
err.contains("at least one entry"),
"error must explain the requirement: {err}"
);
}
#[test]
fn commit_short_truncates_to_seven_chars() {
assert_eq!(commit_short("abcdef1234567890"), "abcdef1");
}
#[test]
fn commit_short_keeps_short_commit_as_is() {
assert_eq!(commit_short("abc"), "abc");
}
#[test]
fn dispatcher_args_are_consumed() {
let _args = CheckDeterminismArgs {
runs: 2,
stages: None,
targets: None,
report: None,
snapshot: false,
no_snapshot: false,
inject_drift: None,
preserve_dist: None,
crate_name: None,
require_tools: false,
};
}
#[test]
fn resolve_child_snapshot_auto_off_when_head_at_tag() {
assert!(!resolve_child_snapshot(false, false, true));
}
#[test]
fn resolve_child_snapshot_auto_on_when_head_not_at_tag() {
assert!(resolve_child_snapshot(false, false, false));
}
#[test]
fn resolve_child_snapshot_explicit_snapshot_beats_auto() {
assert!(resolve_child_snapshot(true, false, true));
assert!(resolve_child_snapshot(true, false, false));
}
#[test]
fn resolve_child_snapshot_explicit_no_snapshot_beats_auto() {
assert!(!resolve_child_snapshot(false, true, false));
assert!(!resolve_child_snapshot(false, true, true));
}
#[test]
fn read_project_version_returns_none_when_cargo_toml_missing() {
let tmp = tempfile::tempdir().unwrap();
assert_eq!(read_project_version(tmp.path()), None);
}
#[test]
fn read_project_version_reads_workspace_package_version() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Cargo.toml"),
r#"[workspace]
members = ["crates/*"]
[workspace.package]
version = "1.2.3-test"
edition = "2021"
"#,
)
.unwrap();
assert_eq!(
read_project_version(tmp.path()),
Some("1.2.3-test".to_string())
);
}
#[test]
fn read_project_version_reads_package_version() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Cargo.toml"),
r#"[package]
name = "demo"
version = "0.4.2"
edition = "2021"
"#,
)
.unwrap();
assert_eq!(read_project_version(tmp.path()), Some("0.4.2".to_string()));
}
#[test]
fn read_project_version_prefers_workspace_when_both_present() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join("Cargo.toml"),
r#"[workspace.package]
version = "9.9.9"
[package]
name = "root-crate"
version = "0.0.1"
"#,
)
.unwrap();
assert_eq!(read_project_version(tmp.path()), Some("9.9.9".to_string()));
}
#[test]
fn read_project_version_returns_none_on_malformed_toml() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join("Cargo.toml"), "not valid \x00 toml ===").unwrap();
assert_eq!(read_project_version(tmp.path()), None);
}
#[test]
fn signature_allowlist_derives_custom_cosign_bundle_suffix() {
use anodizer_core::config::{Config, SignConfig};
let cfg = Config {
signs: vec![
SignConfig {
signature: Some("{{ .Artifact }}.cosign.bundle".into()),
..Default::default()
},
SignConfig::default(), ],
..Default::default()
};
let entries = signature_allowlist_entries_from_config(&cfg);
let patterns: Vec<&str> = entries.iter().map(|e| e.artifact.as_str()).collect();
assert!(
patterns.contains(&"*.cosign.bundle"),
"custom signature suffix must be allow-listed, got {patterns:?}"
);
assert!(patterns.contains(&"*.sig"), "got {patterns:?}");
assert!(entries.iter().all(|e| e.artifact != "*"));
}
#[test]
fn signature_allowlist_derives_keyless_certificate_suffix() {
use anodizer_core::config::{Config, SignConfig};
let cfg = Config {
signs: vec![SignConfig {
certificate: Some("{{ .Artifact }}.pem".into()),
..Default::default()
}],
..Default::default()
};
let entries = signature_allowlist_entries_from_config(&cfg);
let patterns: Vec<&str> = entries.iter().map(|e| e.artifact.as_str()).collect();
assert!(
patterns.contains(&"*.pem"),
"configured certificate suffix must be allow-listed, got {patterns:?}"
);
}
#[test]
fn cfgd_v040_drift_set_is_fully_allowlisted() {
use anodizer_core::DeterminismState;
use anodizer_core::config::{Config, SignConfig};
let cfg = Config {
signs: vec![
SignConfig {
signature: Some("{{ .Artifact }}.cosign.bundle".into()),
..Default::default()
},
SignConfig::default(),
],
binary_signs: vec![SignConfig::default()],
..Default::default()
};
let mut state = DeterminismState::seed_from_commit(0).expect("non-negative");
for entry in signature_allowlist_entries_from_config(&cfg) {
state.append_runtime(entry.artifact, entry.reason);
}
let drifted = [
"cfgd-0.4.0-linux-amd64-installer.run.sha256.cosign.bundle",
"cfgd-0.4.0-linux-amd64.tar.gz.cdx.json",
"cfgd-0.4.0-linux-amd64.tar.gz.cdx.json.sha256",
"cfgd-0.4.0-linux-amd64.tar.gz.cdx.json.sha256.cosign.bundle",
"cfgd-0.4.0-linux-amd64.tar.gz.sha256.cosign.bundle",
"cfgd-0.4.0-linux-arm64-installer.run.sha256.cosign.bundle",
"cfgd-0.4.0-linux-arm64.tar.gz.cdx.json",
"cfgd-0.4.0-linux-arm64.tar.gz.cdx.json.sha256",
"cfgd-0.4.0-linux-arm64.tar.gz.cdx.json.sha256.cosign.bundle",
"cfgd-0.4.0-linux-arm64.tar.gz.sha256.cosign.bundle",
"cfgd-0.4.0-source.tar.gz.sha256.cosign.bundle",
"cfgd_0.4.0_linux_amd64.apk.sha256.cosign.bundle",
"cfgd_0.4.0_linux_amd64.deb.sha256.cosign.bundle",
"cfgd_0.4.0_linux_amd64.rpm.sha256.cosign.bundle",
"cfgd_0.4.0_linux_arm64.apk.sha256.cosign.bundle",
"cfgd_0.4.0_linux_arm64.deb.sha256.cosign.bundle",
"cfgd_0.4.0_linux_arm64.rpm.sha256.cosign.bundle",
"install.sh.sha256.cosign.bundle",
"cfgd-0.4.0-darwin-all.tar.gz.cdx.json",
"cfgd-0.4.0-darwin-all.tar.gz.cdx.json.sha256",
"cfgd-0.4.0-darwin-all.tar.gz.cdx.json.sha256.cosign.bundle",
"cfgd-0.4.0-darwin-all.tar.gz.sha256.cosign.bundle",
"cfgd-0.4.0-darwin-amd64.tar.gz.cdx.json",
"cfgd-0.4.0-darwin-arm64.tar.gz.cdx.json.sha256.cosign.bundle",
"cfgd_0.4.0_checksums.txt.cosign.bundle",
];
for name in drifted {
assert!(
state.resolve_reason(name).is_some(),
"{name} drifted v0.4.0 and must now be allow-listed"
);
}
assert!(
state
.resolve_reason("cfgd-0.4.0-linux-amd64.tar.gz")
.is_none(),
"archive bytes must still be drift-checked"
);
assert!(
state.resolve_reason("cfgd").is_none(),
"raw binary must still be drift-checked"
);
}
#[test]
fn signature_allowlist_collects_per_workspace_signs() {
use anodizer_core::config::{Config, SignConfig, WorkspaceConfig};
let cfg = Config {
workspaces: Some(vec![WorkspaceConfig {
name: "member".into(),
binary_signs: vec![SignConfig {
signature: Some("{{ .Artifact }}.bundle".into()),
..Default::default()
}],
..Default::default()
}]),
..Default::default()
};
let patterns: Vec<String> = signature_allowlist_entries_from_config(&cfg)
.into_iter()
.map(|e| e.artifact)
.collect();
assert!(
patterns.contains(&"*.bundle".to_string()),
"per-workspace signature suffix must be collected, got {patterns:?}"
);
}