use serde_json::json;
use tix::config::{Config, InitiativeConfiguration};
use tix::roadmap::{RoadmapEntry, RoadmapFilter, TargetDateState, filter_and_order, order};
fn populated() -> RoadmapFilter {
RoadmapFilter {
period_from: Some("2026-01-01".into()),
period_to: Some("2026-12-31".into()),
owner: Some("alice".into()),
status: Some("active".into()),
labels: vec!["strategic".into(), "q4".into()],
query: Some("platform".into()),
}
}
fn seeded() -> Config {
let mut config = Config::default();
config.initiatives.insert(
"i-zulu".into(),
InitiativeConfiguration {
name: "Zulu".into(),
owner: Some("bob".into()),
status: Some("paused".into()),
labels: vec!["internal".into()],
target_date: Some("2026-03-31".into()),
..InitiativeConfiguration::default()
},
);
config.initiatives.insert(
"i-someday".into(),
InitiativeConfiguration {
name: "Someday".into(),
owner: Some("carol".into()),
status: Some("proposed".into()),
labels: vec!["exploratory".into()],
..InitiativeConfiguration::default()
},
);
config.initiatives.insert(
"i-alpha".into(),
InitiativeConfiguration {
name: "Alpha".into(),
outcome: Some("Ship the platform".into()),
owner: Some("alice".into()),
status: Some("active".into()),
labels: vec!["strategic".into(), "Q4".into()],
target_date: Some("2026-03-31".into()),
..InitiativeConfiguration::default()
},
);
config.initiatives.insert(
"i-early".into(),
InitiativeConfiguration {
name: "Early".into(),
owner: Some("alice".into()),
status: Some("active".into()),
labels: vec!["strategic".into()],
target_date: Some("2025-01-01".into()),
..InitiativeConfiguration::default()
},
);
config
}
fn ordered<'a>(config: &'a Config, filter: &RoadmapFilter) -> Vec<&'a str> {
filter_and_order(config, filter)
.unwrap()
.iter()
.map(|entry| entry.id)
.collect()
}
#[test]
fn the_default_filter_is_the_one_no_filter_value() {
let empty = RoadmapFilter::default();
assert!(empty.is_empty(), "the default must be the empty filter");
assert!(!populated().is_empty());
let blank_query = RoadmapFilter {
query: Some(" ".into()),
..RoadmapFilter::default()
};
assert!(blank_query.is_empty());
assert_eq!(
ordered(&seeded(), &empty),
["i-early", "i-alpha", "i-zulu", "i-someday"]
);
}
#[test]
fn filter_fields_serialize_with_stable_names() {
let value = serde_json::to_value(populated()).unwrap();
let mut keys: Vec<&str> = value
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
keys.sort_unstable();
assert_eq!(
keys,
[
"labels",
"owner",
"period_from",
"period_to",
"query",
"status"
]
);
assert_eq!(
serde_json::to_value(RoadmapFilter::default()).unwrap(),
json!({})
);
}
#[test]
fn a_filter_round_trips_through_json_and_toml() {
let filter = populated();
let json = serde_json::to_string(&filter).unwrap();
assert_eq!(
serde_json::from_str::<RoadmapFilter>(&json).unwrap(),
filter
);
let toml = toml::to_string(&filter).unwrap();
assert_eq!(toml::from_str::<RoadmapFilter>(&toml).unwrap(), filter);
}
#[test]
fn an_unknown_serialized_field_is_refused() {
let err = serde_json::from_value::<RoadmapFilter>(json!({ "ownerr": "alice" })).unwrap_err();
assert!(
err.to_string().contains("ownerr"),
"the refusal must name the unknown key: {err}"
);
}
#[test]
fn a_partial_surface_payload_fills_the_remaining_criteria_with_defaults() {
let filter: RoadmapFilter = toml::from_str("status = \"active\"\n").unwrap();
assert_eq!(filter.status.as_deref(), Some("active"));
assert_eq!(
filter,
RoadmapFilter {
status: Some("active".into()),
..RoadmapFilter::default()
}
);
}
#[test]
fn a_malformed_period_bound_is_refused() {
for bad in ["2026-13-01", "2026-02-30", "20261201", "1969-12-31", "soon"] {
let filter = RoadmapFilter {
period_from: Some(bad.into()),
..RoadmapFilter::default()
};
let err = filter.validate().unwrap_err();
assert!(
err.to_string().contains("period_from"),
"`{bad}` must be refused naming the field: {err}"
);
let filter = RoadmapFilter {
period_to: Some(bad.into()),
..RoadmapFilter::default()
};
let err = filter.validate().unwrap_err();
assert!(err.to_string().contains("period_to"), "`{bad}`: {err}");
let err = filter_and_order(&seeded(), &filter).unwrap_err();
assert!(err.to_string().contains("period_to"));
}
}
#[test]
fn a_reversed_period_is_refused_but_an_equal_bound_is_inclusive() {
let reversed = RoadmapFilter {
period_from: Some("2026-06-01".into()),
period_to: Some("2026-01-01".into()),
..RoadmapFilter::default()
};
let err = reversed.validate().unwrap_err();
assert!(err.to_string().contains("period_from"), "{err}");
let one_day = RoadmapFilter {
period_from: Some("2026-03-31".into()),
period_to: Some("2026-03-31".into()),
..RoadmapFilter::default()
};
assert_eq!(ordered(&seeded(), &one_day), ["i-alpha", "i-zulu"]);
}
#[test]
fn a_blank_owner_status_or_label_is_refused() {
for blank in ["", " ", "\t"] {
let owner = RoadmapFilter {
owner: Some(blank.into()),
..RoadmapFilter::default()
};
assert!(owner.validate().unwrap_err().to_string().contains("owner"));
let status = RoadmapFilter {
status: Some(blank.into()),
..RoadmapFilter::default()
};
assert!(
status
.validate()
.unwrap_err()
.to_string()
.contains("status")
);
let labels = RoadmapFilter {
labels: vec!["strategic".into(), blank.into()],
..RoadmapFilter::default()
};
assert!(
labels
.validate()
.unwrap_err()
.to_string()
.contains("labels")
);
}
}
#[test]
fn a_period_bound_is_inclusive_and_needs_a_target_date() {
let from = RoadmapFilter {
period_from: Some("2026-01-01".into()),
..RoadmapFilter::default()
};
assert_eq!(ordered(&seeded(), &from), ["i-alpha", "i-zulu"]);
let to = RoadmapFilter {
period_to: Some("2025-12-31".into()),
..RoadmapFilter::default()
};
assert_eq!(ordered(&seeded(), &to), ["i-early"]);
let bounded = RoadmapFilter {
period_from: Some("2025-01-01".into()),
period_to: Some("2026-12-31".into()),
..RoadmapFilter::default()
};
let config = seeded();
let ids = ordered(&config, &bounded);
assert_eq!(ids, ["i-early", "i-alpha", "i-zulu"]);
assert!(!ids.contains(&"i-someday"));
}
#[test]
fn owner_and_status_are_exact_normalized_matches() {
let owner = RoadmapFilter {
owner: Some(" ALICE ".into()),
..RoadmapFilter::default()
};
assert_eq!(ordered(&seeded(), &owner), ["i-early", "i-alpha"]);
let status = RoadmapFilter {
status: Some("Active".into()),
..RoadmapFilter::default()
};
assert_eq!(ordered(&seeded(), &status), ["i-early", "i-alpha"]);
let not_a_prefix = RoadmapFilter {
owner: Some("ali".into()),
..RoadmapFilter::default()
};
assert!(ordered(&seeded(), ¬_a_prefix).is_empty());
}
#[test]
fn labels_are_an_and_of_every_requested_value() {
let one = RoadmapFilter {
labels: vec!["strategic".into()],
..RoadmapFilter::default()
};
assert_eq!(ordered(&seeded(), &one), ["i-early", "i-alpha"]);
let both = RoadmapFilter {
labels: vec!["Q4".into(), "STRATEGIC".into()],
..RoadmapFilter::default()
};
assert_eq!(ordered(&seeded(), &both), ["i-alpha"]);
let contradictory = RoadmapFilter {
labels: vec!["strategic".into(), "internal".into()],
..RoadmapFilter::default()
};
assert!(ordered(&seeded(), &contradictory).is_empty());
}
#[test]
fn an_unknown_owner_status_or_label_is_an_empty_set_not_an_error() {
let config = seeded();
for filter in [
RoadmapFilter {
owner: Some("nobody".into()),
..RoadmapFilter::default()
},
RoadmapFilter {
status: Some("archived".into()),
..RoadmapFilter::default()
},
RoadmapFilter {
labels: vec!["unused".into()],
..RoadmapFilter::default()
},
] {
let entries = filter_and_order(&config, &filter)
.expect("a well-formed value naming nothing is not an error");
assert!(entries.is_empty(), "a non-matching filter selects no row");
}
}
#[test]
fn query_matches_each_searchable_field_case_insensitively() {
for (query, expected) in [
("platform", vec!["i-alpha"]), ("ALICE", vec!["i-early", "i-alpha"]), ("Exploratory", vec!["i-someday"]), ("proposed", vec!["i-someday"]), ("zulu", vec!["i-zulu"]), ("nothing-here", vec![]), ] {
let filter = RoadmapFilter {
query: Some(query.into()),
..RoadmapFilter::default()
};
assert_eq!(ordered(&seeded(), &filter), expected, "query `{query}`");
}
}
#[test]
fn query_does_not_match_across_field_boundaries() {
let config = seeded();
let spaced = RoadmapFilter {
query: Some("alpha alice".into()),
..RoadmapFilter::default()
};
assert!(ordered(&config, &spaced).is_empty());
let newline = RoadmapFilter {
query: Some("Alpha\nalice".into()),
..RoadmapFilter::default()
};
assert!(ordered(&config, &newline).is_empty());
}
#[test]
fn folding_covers_titlecase_glyphs_and_multi_character_expansions() {
let mut config = Config::default();
config.initiatives.insert(
"i-title".into(),
InitiativeConfiguration {
name: "Title".into(),
labels: vec!["\u{01C6}x".into(), "\u{0130}stanbul".into()],
..InitiativeConfiguration::default()
},
);
for filter in [
RoadmapFilter {
labels: vec!["\u{01C5}x".into()],
..RoadmapFilter::default()
},
RoadmapFilter {
query: Some("\u{01C5}x".into()),
..RoadmapFilter::default()
},
RoadmapFilter {
labels: vec!["i\u{0307}stanbul".into()],
..RoadmapFilter::default()
},
] {
assert_eq!(ordered(&config, &filter), ["i-title"], "{filter:?}");
}
}
#[test]
fn every_filter_dimension_combines_as_an_and() {
let only_alpha = RoadmapFilter {
period_from: Some("2026-01-01".into()),
period_to: Some("2026-12-31".into()),
owner: Some("alice".into()),
status: Some("active".into()),
labels: vec!["strategic".into(), "q4".into()],
query: Some("platform".into()),
};
let config = seeded();
let entries = filter_and_order(&config, &only_alpha).unwrap();
assert_eq!(
entries.iter().map(|entry| entry.id).collect::<Vec<_>>(),
["i-alpha"]
);
let none = RoadmapFilter {
query: Some("unrelated".into()),
..only_alpha
};
assert!(filter_and_order(&config, &none).unwrap().is_empty());
}
#[test]
fn order_is_target_date_ascending_then_stable_id() {
let config = seeded();
let mut entries: Vec<RoadmapEntry<'_>> = filter_and_order(&config, &RoadmapFilter::default())
.unwrap()
.into_iter()
.collect();
order(&mut entries);
assert_eq!(
entries.iter().map(|entry| entry.id).collect::<Vec<_>>(),
["i-early", "i-alpha", "i-zulu", "i-someday"]
);
}
#[test]
fn equal_target_dates_break_the_tie_by_stable_id() {
let mut config = Config::default();
for (id, name) in [
("i-zulu", "Zulu"),
("i-alpha", "Alpha"),
("i-bravo", "Bravo"),
] {
config.initiatives.insert(
id.into(),
InitiativeConfiguration {
name: name.into(),
target_date: Some("2026-03-31".into()),
..InitiativeConfiguration::default()
},
);
}
assert_eq!(
ordered(&config, &RoadmapFilter::default()),
["i-alpha", "i-bravo", "i-zulu"]
);
}
#[test]
fn undated_initiatives_sort_after_every_dated_one_and_keep_id_order() {
let mut config = Config::default();
for (id, date) in [
("i-undated-b", None),
("i-dated", Some("2027-01-01")),
("i-undated-a", None),
] {
config.initiatives.insert(
id.into(),
InitiativeConfiguration {
name: id.into(),
target_date: date.map(str::to_string),
..InitiativeConfiguration::default()
},
);
}
assert_eq!(
ordered(&config, &RoadmapFilter::default()),
["i-dated", "i-undated-a", "i-undated-b"]
);
}
#[test]
fn a_row_reports_the_explicit_missing_date_state() {
let config = seeded();
let entries = filter_and_order(&config, &RoadmapFilter::default()).unwrap();
let someday = entries
.iter()
.find(|entry| entry.id == "i-someday")
.unwrap();
assert_eq!(someday.target_date, TargetDateState::Undated);
assert!(someday.target_date.is_undated());
assert_eq!(someday.target_date.date(), None);
let early = entries.iter().find(|entry| entry.id == "i-early").unwrap();
assert_eq!(early.target_date, TargetDateState::Dated("2025-01-01"));
assert!(!early.target_date.is_undated());
assert_eq!(early.target_date.date(), Some("2025-01-01"));
assert_eq!(early.initiative.name, "Early");
}
#[test]
fn equivalent_surface_normalizations_replay_the_same_ordered_ids() {
let config = seeded();
let programmatic = RoadmapFilter {
period_from: Some("2026-01-01".into()),
owner: Some("alice".into()),
status: Some("active".into()),
..RoadmapFilter::default()
};
let from_json: RoadmapFilter = serde_json::from_value(json!({
"period_from": "2026-01-01",
"owner": "alice",
"status": "active",
}))
.unwrap();
let from_toml: RoadmapFilter =
toml::from_str("period_from = \"2026-01-01\"\nowner = \"alice\"\nstatus = \"active\"\n")
.unwrap();
let sparse = RoadmapFilter {
period_from: Some("2026-01-01".into()),
owner: Some("alice".into()),
status: Some("active".into()),
labels: Vec::new(),
query: None,
period_to: None,
};
let expected = ["i-alpha"];
for filter in [&programmatic, &from_json, &from_toml, &sparse] {
assert_eq!(ordered(&config, filter), expected);
}
}
#[test]
fn filtering_and_ordering_leave_the_registry_untouched() {
let config = seeded();
let before = config.clone();
let filter = populated();
let entries = filter_and_order(&config, &filter).unwrap();
assert!(!entries.is_empty());
assert_eq!(config, before);
for entry in &entries {
assert!(std::ptr::eq(
entry.initiative,
config.initiatives.get(entry.id).unwrap()
));
}
}
#[test]
fn the_order_is_total_and_independent_of_declaration_order() {
let config = seeded();
let canonical = ordered(&config, &RoadmapFilter::default());
let mut reversed = Config::default();
for (id, record) in config.initiatives.iter().rev() {
reversed.initiatives.insert(id.clone(), record.clone());
}
assert_eq!(ordered(&reversed, &RoadmapFilter::default()), canonical);
}