use gwm::config::LabelConfig;
use gwm::labels::{
deterministic_color, diff_labels, normalize_color, resolve_labels, validate_color, validate_label_name, LabelAction,
LabelDiff, LabelSpec, RemoteLabel,
};
#[test]
fn deterministic_color_is_stable_for_same_name() {
let c1 = deterministic_color("bug");
let c2 = deterministic_color("bug");
assert_eq!(c1, c2);
}
#[test]
fn deterministic_color_returns_six_hex_chars() {
let c = deterministic_color("enhancement");
assert_eq!(c.len(), 6, "expected 6 chars, got {:?}", c);
assert!(
c.chars().all(|ch| ch.is_ascii_hexdigit()),
"expected hex only, got {:?}",
c
);
assert!(
c.chars().all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()),
"expected lowercase, got {:?}",
c
);
}
#[test]
fn deterministic_color_distinguishes_different_names() {
let a = deterministic_color("bug");
let b = deterministic_color("enhancement");
assert_ne!(a, b);
}
#[test]
fn deterministic_color_handles_whitespace_in_name() {
let c = deterministic_color("good first issue");
assert_eq!(c.len(), 6);
assert_ne!(c, deterministic_color("goodfirstissue"));
}
#[test]
fn validate_color_accepts_canonical_form() {
assert!(validate_color("d73a4a").is_ok());
assert!(validate_color("0e8a16").is_ok());
}
#[test]
fn validate_color_accepts_uppercase_and_normalises() {
let norm = normalize_color("D73A4A").unwrap();
assert_eq!(norm, "d73a4a");
}
#[test]
fn validate_color_strips_leading_hash() {
let norm = normalize_color("#d73a4a").unwrap();
assert_eq!(norm, "d73a4a");
}
#[test]
fn validate_color_rejects_wrong_length() {
assert!(validate_color("d73a4").is_err());
assert!(validate_color("d73a4abc").is_err());
assert!(validate_color("").is_err());
}
#[test]
fn validate_color_rejects_non_hex() {
assert!(validate_color("xyzxyz").is_err());
assert!(validate_color("zz4a4a").is_err());
}
#[test]
fn resolve_labels_keeps_declared_colour() {
let declared = vec![LabelConfig {
name: "bug".into(),
description: Some("Something broke".into()),
color: Some("d73a4a".into()),
}];
let resolved = resolve_labels(&declared, false).unwrap();
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].name, "bug");
assert_eq!(resolved[0].description.as_deref(), Some("Something broke"));
assert_eq!(resolved[0].color, "d73a4a");
}
#[test]
fn resolve_labels_normalises_hash_and_case() {
let declared = vec![LabelConfig {
name: "bug".into(),
description: None,
color: Some("#D73A4A".into()),
}];
let resolved = resolve_labels(&declared, false).unwrap();
assert_eq!(resolved[0].color, "d73a4a");
}
#[test]
fn resolve_labels_falls_back_to_deterministic_when_color_absent() {
let a = resolve_labels(
&[LabelConfig {
name: "wip".into(),
description: None,
color: None,
}],
false,
)
.unwrap();
let b = resolve_labels(
&[LabelConfig {
name: "wip".into(),
description: None,
color: None,
}],
false,
)
.unwrap();
assert_eq!(a[0].color, b[0].color);
assert_eq!(a[0].color.len(), 6);
}
#[test]
fn resolve_labels_random_overrides_omission() {
let declared = vec![
LabelConfig {
name: "x".into(),
description: None,
color: None,
},
LabelConfig {
name: "y".into(),
description: None,
color: Some("00aabb".into()),
},
];
let resolved = resolve_labels(&declared, true).unwrap();
assert_eq!(resolved[0].color.len(), 6);
assert!(resolved[0].color.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(resolved[1].color, "00aabb");
}
#[test]
fn resolve_labels_surfaces_invalid_color() {
let declared = vec![LabelConfig {
name: "bad".into(),
description: None,
color: Some("not-a-hex".into()),
}];
let err = resolve_labels(&declared, false).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("bad"), "should mention the label name: {}", msg);
}
fn spec(name: &str, color: &str, desc: Option<&str>) -> LabelSpec {
LabelSpec {
name: name.into(),
description: desc.map(|s| s.into()),
color: color.into(),
}
}
fn rlabel(name: &str, color: &str, desc: Option<&str>) -> RemoteLabel {
RemoteLabel {
name: name.into(),
description: desc.map(|s| s.into()),
color: color.into(),
}
}
#[test]
fn diff_empty_declared_yields_only_extras() {
let declared = vec![];
let remote = vec![rlabel("wontfix", "ffffff", None)];
let diff = diff_labels(&declared, &remote);
assert!(diff.to_create.is_empty());
assert!(diff.to_update.is_empty());
assert!(diff.matching.is_empty());
assert_eq!(diff.extra_on_remote.len(), 1);
assert_eq!(diff.extra_on_remote[0].name, "wontfix");
}
#[test]
fn diff_declared_not_on_remote_yields_create() {
let declared = vec![spec("bug", "d73a4a", Some("broken"))];
let remote = vec![];
let diff = diff_labels(&declared, &remote);
assert_eq!(diff.to_create.len(), 1);
assert_eq!(diff.to_create[0].name, "bug");
assert!(diff.to_update.is_empty());
assert!(diff.matching.is_empty());
assert!(diff.extra_on_remote.is_empty());
}
#[test]
fn diff_color_mismatch_yields_update() {
let declared = vec![spec("good first issue", "7057ff", Some("Good for newcomers"))];
let remote = vec![rlabel("good first issue", "008672", Some("Good for newcomers"))];
let diff = diff_labels(&declared, &remote);
assert!(diff.to_create.is_empty());
assert_eq!(diff.to_update.len(), 1);
assert_eq!(diff.to_update[0].action, LabelAction::Update);
assert_eq!(diff.to_update[0].spec.color, "7057ff");
assert_eq!(diff.to_update[0].previous_color.as_deref(), Some("008672"));
}
#[test]
fn diff_description_mismatch_yields_update() {
let declared = vec![spec("bug", "d73a4a", Some("broken"))];
let remote = vec![rlabel("bug", "d73a4a", Some("old description"))];
let diff = diff_labels(&declared, &remote);
assert_eq!(diff.to_update.len(), 1);
assert_eq!(diff.to_update[0].spec.description.as_deref(), Some("broken"));
}
#[test]
fn diff_full_match_yields_matching() {
let declared = vec![spec("documentation", "0075ca", Some("doc"))];
let remote = vec![rlabel("documentation", "0075ca", Some("doc"))];
let diff = diff_labels(&declared, &remote);
assert_eq!(diff.matching.len(), 1);
assert_eq!(diff.matching[0].name, "documentation");
assert!(diff.to_create.is_empty());
assert!(diff.to_update.is_empty());
assert!(diff.extra_on_remote.is_empty());
}
#[test]
fn diff_normalises_remote_color_case_before_compare() {
let declared = vec![spec("bug", "d73a4a", Some("broken"))];
let remote = vec![rlabel("bug", "D73A4A", Some("broken"))];
let diff = diff_labels(&declared, &remote);
assert_eq!(diff.matching.len(), 1);
assert!(diff.to_update.is_empty());
}
#[test]
fn validate_label_name_accepts_normal_names() {
assert!(validate_label_name("bug").is_ok());
assert!(validate_label_name("good first issue").is_ok());
assert!(validate_label_name("priority/p1").is_ok());
assert!(validate_label_name("🚀 ship-it").is_ok());
}
#[test]
fn validate_label_name_rejects_leading_dash() {
let err = validate_label_name("-h").unwrap_err();
let msg = format!("{}", err);
assert!(
msg.contains("'-'") || msg.contains("- "),
"error must mention the offending leading dash; got: {}",
msg
);
assert!(
msg.contains("#100"),
"error must cite issue #100 so the user can find context; got: {}",
msg
);
assert!(validate_label_name("--repo").is_err());
assert!(validate_label_name("-").is_err());
}
#[test]
fn validate_label_name_rejects_empty() {
let err = validate_label_name("").unwrap_err();
assert!(format!("{}", err).contains("empty"));
}
#[test]
fn validate_label_name_rejects_comma() {
assert!(validate_label_name("foo,bar").is_err());
}
#[test]
fn validate_label_name_rejects_ascii_control_chars() {
assert!(validate_label_name("foo\nbar").is_err());
assert!(validate_label_name("foo\tbar").is_err());
}
#[test]
fn resolve_labels_propagates_invalid_name_from_config() {
let declared = vec![LabelConfig {
name: "-h".into(),
description: None,
color: None,
}];
let err = resolve_labels(&declared, false).unwrap_err();
assert!(format!("{}", err).contains("'-h'") || format!("{}", err).contains("-h"));
}
#[test]
fn diff_summary_counts_match_buckets() {
let diff = LabelDiff {
to_create: vec![spec("a", "aaaaaa", None), spec("b", "bbbbbb", None)],
to_update: vec![gwm::labels::LabelUpdate {
action: LabelAction::Update,
spec: spec("c", "cccccc", None),
previous_color: Some("dddddd".into()),
previous_description: None,
}],
matching: vec![
spec("d", "111111", None),
spec("e", "222222", None),
spec("f", "333333", None),
],
extra_on_remote: vec![rlabel("g", "999999", None)],
};
let (c, u, m, x) = diff.counts();
assert_eq!(c, 2);
assert_eq!(u, 1);
assert_eq!(m, 3);
assert_eq!(x, 1);
}
#[test]
fn diff_summary_line_renders_canonical_shape() {
use gwm::labels::diff_summary_line;
let s = diff_summary_line(2, 1, 3, 1);
assert_eq!(s, "summary: 2 create · 1 update · 3 match · 1 extra-on-remote");
}
#[test]
fn diff_dry_run_line_renders_counts_in_order() {
use gwm::labels::diff_dry_run_line;
let s = diff_dry_run_line(2, 1, 3, 1, 0);
assert_eq!(
s,
"would create 2, update 1, leave 3 untouched, prune 0, ignore 1 extra-on-remote"
);
}