fix: allow same label in trash and delete rules in validate (#178)
## Summary `Rules::validate()` was flagging `DuplicateLabel` when the same label appeared in both a Trash rule and a Delete rule. This is intentional two-stage processing: trash first (recoverable), delete later (permanent). The validator should only flag a label appearing in two rules of the **same action type**. Fix: key the duplicate-label check on `(label, action)` rather than `label` alone. ## Test plan - [x] New test `test_validate_same_label_different_actions_not_duplicate` — confirms Trash+Delete on the same label passes validation (was failing before this fix) - [x] Existing `test_validate_duplicate_label_reported` still passes — same label + same action is still flagged - [x] Full test suite: 125 unit tests pass - [x] `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo audit` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -688,7 +688,9 @@ impl Rules {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn validate(&self) -> Vec<ValidationIssue> {
|
pub fn validate(&self) -> Vec<ValidationIssue> {
|
||||||
let mut issues = Vec::new();
|
let mut issues = Vec::new();
|
||||||
let mut seen_labels: BTreeMap<String, usize> = BTreeMap::new();
|
// Key: (label, action_str) — the same label in a Trash and a Delete rule is
|
||||||
|
// intentional two-stage processing and must not be flagged as a duplicate.
|
||||||
|
let mut seen_label_actions: BTreeMap<(String, String), usize> = BTreeMap::new();
|
||||||
|
|
||||||
for rule in self.rules.values() {
|
for rule in self.rules.values() {
|
||||||
let id = rule.id();
|
let id = rule.id();
|
||||||
@@ -712,14 +714,15 @@ impl Rules {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for label in rule.labels() {
|
for label in rule.labels() {
|
||||||
if let Some(&other_id) = seen_labels.get(&label) {
|
let key = (label.clone(), rule.action_str().to_lowercase());
|
||||||
|
if let Some(&other_id) = seen_label_actions.get(&key) {
|
||||||
if other_id != id {
|
if other_id != id {
|
||||||
issues.push(ValidationIssue::DuplicateLabel {
|
issues.push(ValidationIssue::DuplicateLabel {
|
||||||
label: label.clone(),
|
label: label.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
seen_labels.insert(label, id);
|
seen_label_actions.insert(key, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1224,6 +1227,33 @@ action = "Trash"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_same_label_different_actions_not_duplicate() {
|
||||||
|
setup_test_environment();
|
||||||
|
// A label in a Trash rule AND a Delete rule is intentional two-stage processing.
|
||||||
|
let toml_str = r#"
|
||||||
|
[rules."1"]
|
||||||
|
id = 1
|
||||||
|
retention = "w:1"
|
||||||
|
labels = ["Development/Notifications"]
|
||||||
|
action = "Trash"
|
||||||
|
|
||||||
|
[rules."2"]
|
||||||
|
id = 2
|
||||||
|
retention = "w:2"
|
||||||
|
labels = ["Development/Notifications"]
|
||||||
|
action = "Delete"
|
||||||
|
"#;
|
||||||
|
let rules: Rules = toml::from_str(toml_str).unwrap();
|
||||||
|
let issues = rules.validate();
|
||||||
|
assert!(
|
||||||
|
!issues
|
||||||
|
.iter()
|
||||||
|
.any(|i| matches!(i, ValidationIssue::DuplicateLabel { .. })),
|
||||||
|
"Same label with different actions should NOT be flagged as duplicate, got: {issues:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validate_multiple_issues_collected() {
|
fn test_validate_multiple_issues_collected() {
|
||||||
setup_test_environment();
|
setup_test_environment();
|
||||||
|
|||||||
Reference in New Issue
Block a user