use super::*;
use crate::domain::action::identity::{ActionId, CommitSha};
use crate::domain::action::spec::Spec as ActionSpec;
use crate::domain::action::specifier::Specifier;
use crate::domain::lock::Lock;
use crate::domain::manifest::Manifest;
use crate::domain::resolution::testutil::FakeRegistry;
use crate::domain::resolution::{ActionResolver, Error as ResolutionError, Forge};
fn auth_required() -> ResolutionError {
ResolutionError::AuthRequired {
forge: Forge::GitHub,
}
}
fn make_manifest_with(action: &str, version: &str) -> Manifest {
let mut m = Manifest::default();
m.set(ActionId::from(action), Specifier::from_v1(version));
m
}
#[test]
fn lock_resolves_from_workflow_sha_first() {
let workflow_sha = "cccccccccccccccccccccccccccccccccccccccc";
let mut manifest = make_manifest_with("actions/checkout", "v4");
let mut lock = Lock::default();
let key = ActionSpec::new(ActionId::from("actions/checkout"), Specifier::from_v1("v4"));
let mut workflow_shas = HashMap::new();
workflow_shas.insert(key.clone(), CommitSha::from(workflow_sha));
let registry = FakeRegistry::new();
let resolver = ActionResolver::new(®istry);
update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
let entry = lock.get(&key).expect("lock entry must exist");
assert_eq!(
entry.commit.sha.as_str(),
workflow_sha,
"SHA must come from workflow (SHA-first)"
);
}
#[test]
fn sha_first_lock_uses_workflow_sha_and_most_specific_version() {
let workflow_sha = "6d1e696000000000000000000000000000000000";
let mut manifest = make_manifest_with("jdx/mise-action", "v3");
let mut lock = Lock::default();
let key = ActionSpec::new(ActionId::from("jdx/mise-action"), Specifier::from_v1("v3"));
let mut workflow_shas = HashMap::new();
workflow_shas.insert(key.clone(), CommitSha::from(workflow_sha));
let registry = FakeRegistry::new().with_sha_tags(
"jdx/mise-action",
workflow_sha,
vec!["v3", "v3.6", "v3.6.1"],
);
let resolver = ActionResolver::new(®istry);
update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
let entry = lock.get(&key).expect("lock entry must exist");
assert_eq!(
entry.commit.sha.as_str(),
workflow_sha,
"SHA must be from workflow"
);
assert_eq!(
entry.version_label(),
"v3.6.1",
"version must be most specific tag"
);
}
#[test]
fn version_ref_falls_back_to_registry_resolution() {
let registry_sha = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let mut manifest = make_manifest_with("actions/checkout", "v4");
let mut lock = Lock::default();
let key = ActionSpec::new(ActionId::from("actions/checkout"), Specifier::from_v1("v4"));
let workflow_shas = HashMap::new();
let registry = FakeRegistry::new().with_fixed_sha(registry_sha);
let resolver = ActionResolver::new(®istry);
update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
let entry = lock.get(&key).expect("lock entry must exist");
assert_eq!(
entry.commit.sha.as_str(),
registry_sha,
"SHA must come from registry when no workflow SHA"
);
}
#[test]
fn update_lock_recoverable_errors_are_skipped() {
let mut manifest = Manifest::default();
manifest.set(ActionId::from("actions/checkout"), Specifier::from_v1("v4"));
manifest.set(
ActionId::from("actions/setup-node"),
Specifier::from_v1("v4"),
);
let mut lock = Lock::default();
let workflow_shas = HashMap::new();
let registry = FakeRegistry::new().failing_action("actions/checkout", auth_required());
let resolver = ActionResolver::new(®istry);
update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
let setup_node_key = ActionSpec::new(
ActionId::from("actions/setup-node"),
Specifier::from_v1("v4"),
);
assert!(
lock.get(&setup_node_key).is_some(),
"setup-node must be resolved"
);
let checkout_key =
ActionSpec::new(ActionId::from("actions/checkout"), Specifier::from_v1("v4"));
assert!(
lock.get(&checkout_key).is_none(),
"checkout must be skipped (AuthRequired)"
);
}
#[test]
fn out_of_range_pinned_sha_is_reresolved_within_range() {
let workflow_sha = "6d1e696000000000000000000000000000000000";
let mut manifest = make_manifest_with("actions/checkout", "v5"); let mut lock = Lock::default();
let key = ActionSpec::new(ActionId::from("actions/checkout"), Specifier::from_v1("v5"));
let mut workflow_shas = HashMap::new();
workflow_shas.insert(key.clone(), CommitSha::from(workflow_sha));
let registry = FakeRegistry::new().with_sha_tags(
"actions/checkout",
workflow_sha,
vec!["v6", "v6.0", "v6.0.2"],
);
let resolver = ActionResolver::new(®istry);
update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
let entry = lock.get(&key).expect("lock entry must exist");
let version = entry.version_label();
assert_ne!(
version, "v6.0.2",
"out-of-range tag v6.0.2 must not be recorded under ^5"
);
assert_eq!(
version, "v5",
"resolved version must be re-resolved within the ^5 range"
);
}
#[test]
fn out_of_range_pinned_sha_sub_major_is_reresolved() {
let workflow_sha = "6d1e696000000000000000000000000000000000";
let mut manifest = make_manifest_with("some/action", "v1.15.2"); let mut lock = Lock::default();
let key = ActionSpec::new(ActionId::from("some/action"), Specifier::from_v1("v1.15.2"));
let mut workflow_shas = HashMap::new();
workflow_shas.insert(key.clone(), CommitSha::from(workflow_sha));
let registry =
FakeRegistry::new().with_sha_tags("some/action", workflow_sha, vec!["v1.16", "v1.16.0"]);
let resolver = ActionResolver::new(®istry);
update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
let entry = lock.get(&key).expect("lock entry must exist");
let version = entry.version_label();
assert_ne!(
version, "v1.16.0",
"out-of-range tag v1.16.0 must not be recorded under ~1.15.2"
);
assert_eq!(
version, "v1.15.2",
"resolved version must be re-resolved within the ~1.15.2 range"
);
}
#[test]
fn out_of_range_pin_emits_event() {
let workflow_sha = "6d1e696000000000000000000000000000000000";
let mut manifest = make_manifest_with("actions/checkout", "v5");
let mut lock = Lock::default();
let key = ActionSpec::new(ActionId::from("actions/checkout"), Specifier::from_v1("v5"));
let mut workflow_shas = HashMap::new();
workflow_shas.insert(key, CommitSha::from(workflow_sha));
let registry = FakeRegistry::new().with_sha_tags(
"actions/checkout",
workflow_sha,
vec!["v6", "v6.0", "v6.0.2"],
);
let resolver = ActionResolver::new(®istry);
let events = update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
let has_event = events.iter().any(|e| {
matches!(
e,
SyncEvent::PinOutOfRange { rejected, .. } if rejected.as_str() == "v6.0.2"
)
});
assert!(
has_event,
"expected a PinOutOfRange event for v6.0.2, got: {events:?}"
);
}
#[test]
fn in_range_pin_emits_no_event() {
let workflow_sha = "6d1e696000000000000000000000000000000000";
let mut manifest = make_manifest_with("actions/checkout", "v5");
let mut lock = Lock::default();
let key = ActionSpec::new(ActionId::from("actions/checkout"), Specifier::from_v1("v5"));
let mut workflow_shas = HashMap::new();
workflow_shas.insert(key.clone(), CommitSha::from(workflow_sha));
let registry = FakeRegistry::new().with_sha_tags(
"actions/checkout",
workflow_sha,
vec!["v5", "v5.4", "v5.4.0"],
);
let resolver = ActionResolver::new(®istry);
let events = update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
assert!(
!events
.iter()
.any(|e| matches!(e, SyncEvent::PinOutOfRange { .. })),
"in-range pin must not emit PinOutOfRange, got: {events:?}"
);
assert_eq!(
lock.get(&key).expect("entry").version_label(),
"v5.4.0",
"in-range SHA-first version must be kept"
);
}
#[test]
fn init_derived_specifier_keeps_sha_version() {
let workflow_sha = "6d1e696000000000000000000000000000000000";
let mut manifest = make_manifest_with("actions/checkout", "v6"); let mut lock = Lock::default();
let key = ActionSpec::new(ActionId::from("actions/checkout"), Specifier::from_v1("v6"));
let mut workflow_shas = HashMap::new();
workflow_shas.insert(key.clone(), CommitSha::from(workflow_sha));
let registry = FakeRegistry::new().with_sha_tags(
"actions/checkout",
workflow_sha,
vec!["v6", "v6.0", "v6.0.2"],
);
let resolver = ActionResolver::new(®istry);
let events = update_lock(&mut lock, &mut manifest, &resolver, &workflow_shas).unwrap();
assert!(
!events
.iter()
.any(|e| matches!(e, SyncEvent::PinOutOfRange { .. })),
"derived specifier must not trip the range check, got: {events:?}"
);
assert_eq!(
lock.get(&key).expect("entry").version_label(),
"v6.0.2",
"SHA-first version (v6.0.2) satisfies derived ^6 and is kept"
);
}