use tix::relationships::{
BlockedState, CanonicalEdge, DiagnosticKind, IssueView, RelationKind, Relationship,
RelationshipGraph, RelationshipValidationResult, generate_relationship_id, normalize_all,
};
fn reference(number: u32) -> tix::id::IssueRef {
tix::id::IssueRef::Number(number)
}
struct Owned {
number: u32,
title: String,
status: String,
relationships: Vec<Relationship>,
}
impl Owned {
fn new(number: u32, title: &str, status: &str) -> Self {
Self {
number,
title: title.to_string(),
status: status.to_string(),
relationships: Vec::new(),
}
}
fn with(&mut self, rel: Relationship) -> &mut Self {
self.relationships.push(rel);
self
}
fn blocks(&mut self, target: &Owned) -> &mut Self {
let rel = Relationship::new(
self.number,
target.number,
RelationKind::Blocks,
&target.title,
);
self.relationships.push(rel);
self
}
fn view(&self) -> IssueView<'_> {
IssueView::new(self.number, &self.title, &self.status, &self.relationships)
}
}
fn graph<'a>(owned: &'a [Owned], done_column: &str) -> RelationshipGraph<'a> {
let views: Vec<IssueView<'a>> = owned.iter().map(Owned::view).collect();
RelationshipGraph::new(&views, done_column)
}
fn blockers(state: &BlockedState) -> Vec<u32> {
state.blockers.iter().map(|n| n.number).collect()
}
fn chain_fixture() -> Vec<Owned> {
let three = Owned::new(3, "C", "todo");
let mut two = Owned::new(2, "B", "todo");
two.blocks(&three);
let mut one = Owned::new(1, "A", "todo");
one.blocks(&two);
vec![one, two, three]
}
#[test]
fn graph_reports_shape_and_issue_membership() {
let owned = chain_fixture();
let g = graph(&owned, "done");
assert_eq!(g.issue_count(), 3);
assert_eq!(g.edge_count(), 2);
assert_eq!(g.issue_numbers(), &[1, 2, 3]);
assert!(g.has_issue(2));
assert!(!g.has_issue(99));
assert_eq!(g.done_column(), "done");
}
#[test]
fn a_chain_is_walked_downstream_from_its_root() {
let owned = chain_fixture();
let g = graph(&owned, "done");
assert_eq!(g.blocks(1), &[2]);
assert_eq!(g.blocks(2), &[3]);
assert!(g.blocks(3).is_empty());
let chain = g.dependency_chain(1);
assert_eq!(chain.root, 1);
assert_eq!(chain.paths.len(), 1);
assert_eq!(
chain.paths[0].iter().map(|n| n.number).collect::<Vec<_>>(),
vec![1, 2, 3]
);
assert_eq!(chain.paths[0][2].title, "C");
assert!(!chain.truncated);
}
#[test]
fn blocked_state_follows_the_resolution_predicate() {
let owned = chain_fixture();
let g = graph(&owned, "done");
assert!(g.blocked_state(2).is_blocked);
assert_eq!(blockers(&g.blocked_state(2)), vec![1]);
assert!(g.blocked_state(3).is_blocked);
assert_eq!(blockers(&g.blocked_state(3)), vec![2]);
assert!(!g.blocked_state(1).is_blocked);
assert!(g.blocked_state(1).reason.is_empty());
}
#[test]
fn completion_is_read_from_the_configured_done_column() {
let mut owned = chain_fixture();
owned[0].status = "shipped".to_string();
let g = graph(&owned, "shipped");
assert!(
!g.blocked_state(2).is_blocked,
"a blocker in the configured done_column stops blocking"
);
let mut other = chain_fixture();
other[0].status = "done".to_string();
assert!(
graph(&other, "shipped").blocked_state(2).is_blocked,
"completion must never be a hard-coded string match"
);
}
#[test]
fn both_spellings_of_a_blocking_pair_produce_one_graph() {
let mut two = Owned::new(2, "B", "todo");
two.with(Relationship::new(1, 2, RelationKind::BlockedBy, "B"));
let owned = vec![Owned::new(1, "A", "todo"), two];
let g = graph(&owned, "done");
assert_eq!(g.edge_count(), 1, "one edge, not two");
assert_eq!(g.blocks(2), &[1], "stored in canonical blocks form");
assert!(
g.blocked_state(1).is_blocked,
"the inverse spelling still blocks, from the right endpoint"
);
assert!(g.find_relationship(&g.all_relationships()[0].id).is_some());
}
#[test]
fn re_applying_the_inverse_spelling_is_not_a_conflict() {
let owned = chain_fixture();
let g = graph(&owned, "done");
let result = g.validate_create(2, 1, RelationKind::BlockedBy);
assert!(
result.is_clean(),
"the same edge in the other spelling is neither a conflict nor a cycle: {result:?}"
);
}
#[test]
fn asserting_the_reverse_direction_is_a_cycle() {
let owned = chain_fixture();
let g = graph(&owned, "done");
let result = g.validate_create(1, 2, RelationKind::BlockedBy);
assert!(
result.has(DiagnosticKind::Cycle),
"the opposite direction closes a cycle: {result:?}"
);
}
#[test]
fn validation_rejects_invalid_mutations_before_any_write() {
let owned = chain_fixture();
let g = graph(&owned, "done");
let self_link = g.validate_create(2, 2, RelationKind::Blocks);
assert!(self_link.has(DiagnosticKind::SelfLink));
assert!(self_link.blocking());
let missing = g.validate_create(1, 404, RelationKind::Blocks);
assert!(missing.has(DiagnosticKind::MissingTarget));
assert!(missing.blocking());
let cycle = g.validate_create(3, 1, RelationKind::Blocks);
assert!(cycle.has(DiagnosticKind::Cycle));
assert!(
cycle.errors.iter().any(|d| d.message.contains("->")),
"the cycle diagnostic names the path: {cycle:?}"
);
}
#[test]
fn a_conflicting_kind_on_the_same_pair_is_reported() {
let two = Owned::new(2, "B", "todo");
let mut one = Owned::new(1, "A", "todo");
one.blocks(&two)
.with(Relationship::new(1, 2, RelationKind::Related, "B"));
let owned = vec![one, two];
let result = graph(&owned, "done").validate_snapshot();
assert!(result.has(DiagnosticKind::ConflictingEdge), "{result:?}");
}
#[test]
fn a_reciprocal_pair_is_a_cycle_not_a_conflict() {
let mut two = Owned::new(2, "B", "todo");
let mut one = Owned::new(1, "A", "todo");
one.blocks(&two);
two.with(Relationship::new(2, 1, RelationKind::Blocks, "A"));
let owned = vec![one, two];
let result = graph(&owned, "done").validate_snapshot();
assert!(result.has(DiagnosticKind::Cycle), "{result:?}");
assert!(
!result.has(DiagnosticKind::ConflictingEdge),
"a reciprocal blocking pair is a cycle, not a kind conflict"
);
assert_eq!(
result
.errors
.iter()
.filter(|d| d.kind == DiagnosticKind::Cycle)
.count(),
1,
"one distinct cycle is reported once: {result:?}"
);
}
#[test]
fn a_dangling_edge_is_reported_but_does_not_freeze_the_issue() {
let mut dangling = Owned::new(2, "B", "todo");
dangling.with(Relationship::new(2, 404, RelationKind::Blocks, "gone"));
let owned = vec![Owned::new(1, "A", "todo"), dangling];
let g = graph(&owned, "done");
assert!(g.validate_snapshot().has(DiagnosticKind::MissingTarget));
assert!(
!g.blocked_state(2).is_blocked,
"invalid data must not permanently freeze an issue"
);
}
#[test]
fn a_legacy_links_key_is_a_diagnostic_not_parsed_data() {
let flagged = Owned::new(1, "A", "todo");
let two = Owned::new(2, "B", "todo");
let three = Owned::new(3, "C", "todo");
let views: Vec<IssueView<'_>> = vec![
IssueView::new(
flagged.number,
&flagged.title,
&flagged.status,
&flagged.relationships,
)
.with_retired_links_key(true),
two.view(),
three.view(),
];
let g = RelationshipGraph::new(&views, "done");
let result = g.validate_snapshot();
assert!(result.has(DiagnosticKind::RetiredLinksKey), "{result:?}");
assert_eq!(g.edge_count(), 0, "the legacy key produced no typed edges");
}
#[test]
fn validate_snapshot_returns_a_value_and_never_an_error() {
let owned = chain_fixture();
let result: RelationshipValidationResult = graph(&owned, "done").validate_snapshot();
assert!(
result.is_clean(),
"a well-formed graph validates clean: {result:?}"
);
assert!(!result.blocking());
}
#[test]
fn accessors_are_deterministic_across_repeated_builds() {
let mut three = Owned::new(3, "C", "todo");
three.with(Relationship::new(1, 3, RelationKind::Related, "C"));
let mut four = Owned::new(4, "D", "todo");
four.with(Relationship::new(2, 4, RelationKind::Duplicates, "D"));
let mut two = Owned::new(2, "B", "todo");
two.with(Relationship::new(2, 3, RelationKind::Blocks, "C"));
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Blocks, "B"));
one.with(Relationship::new(1, 4, RelationKind::Related, "D"));
let owned = vec![one, two, three, four];
let snapshot = |g: &RelationshipGraph<'_>| -> Vec<(RelationKind, u32, u32)> {
g.all_relationships()
.iter()
.map(|r| {
(
r.kind,
r.from.as_number().unwrap_or_default(),
r.to.as_number().unwrap_or_default(),
)
})
.collect()
};
let first = snapshot(&graph(&owned, "done"));
let second = snapshot(&graph(&owned, "done"));
assert_eq!(first, second, "repeated builds must be byte-comparable");
assert_eq!(
first,
vec![
(RelationKind::Blocks, 1, 2),
(RelationKind::Blocks, 2, 3),
(RelationKind::Duplicates, 2, 4),
(RelationKind::Related, 1, 3),
(RelationKind::Related, 1, 4),
]
);
}
#[test]
fn non_blocking_kinds_never_reach_the_blocked_adjacency() {
let four = Owned::new(4, "D", "todo");
let three = Owned::new(3, "C", "todo");
let two = Owned::new(2, "B", "todo");
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Related, "B"))
.with(Relationship::new(1, 3, RelationKind::Duplicates, "C"))
.blocks(&four);
let owned = vec![one, two, three, four];
let g = graph(&owned, "done");
assert_eq!(g.blocks(1), &[4]);
assert!(g.blocked_by(2).is_empty(), "related does not block");
assert!(g.blocked_by(3).is_empty(), "duplicates does not block");
assert!(g.related_to(1).contains(&2));
assert_eq!(g.duplicates_of(1), vec![3]);
assert!(!g.blocked_state(2).is_blocked);
}
#[test]
fn normalize_all_collapses_spellings_and_sorts() {
let edges = normalize_all([
(2, 3, RelationKind::Blocks),
(1, 2, RelationKind::Blocks),
(2, 1, RelationKind::BlockedBy),
]);
assert_eq!(
edges,
vec![
CanonicalEdge {
from: reference(1),
to: reference(2),
kind: RelationKind::Blocks
},
CanonicalEdge {
from: reference(2),
to: reference(3),
kind: RelationKind::Blocks
},
]
);
assert_eq!(
edges,
normalize_all(
edges
.iter()
.map(|e| (e.from, e.to, e.kind))
.collect::<Vec<_>>()
),
"normalization is idempotent"
);
}
#[test]
fn a_ten_thousand_edge_graph_validates_in_a_single_pass() {
const ISSUES: u32 = 2_000;
const PER_ISSUE: u32 = 6;
let mut owned: Vec<Owned> = (0..ISSUES)
.map(|n| Owned::new(n + 1, &format!("Issue {n}"), "todo"))
.collect();
for n in 0..ISSUES {
for k in 1..=PER_ISSUE {
let target = n + k;
if target < ISSUES {
owned[n as usize].with(Relationship::new(
n + 1,
target + 1,
RelationKind::Blocks,
"Target",
));
}
}
}
let g = graph(&owned, "done");
assert!(
g.edge_count() >= 10_000,
"expected >= 10,000 edges, built {}",
g.edge_count()
);
let result = g.validate_snapshot();
assert!(
result.is_clean(),
"an acyclic graph validates clean, found {:?}",
&result.errors[..result.errors.len().min(3)]
);
assert!(g.blocked_state(2).is_blocked, "issue 1 blocks issue 2");
assert!(
!g.blocked_state(1).is_blocked,
"nothing blocks the first issue"
);
}
#[test]
fn a_deep_chain_terminates_without_overflowing_the_stack() {
const DEPTH: u32 = 10_000;
let mut owned: Vec<Owned> = (0..DEPTH)
.map(|n| Owned::new(n + 1, &format!("Node {n}"), "todo"))
.collect();
for n in 0..DEPTH - 1 {
owned[n as usize].with(Relationship::new(
n + 1,
n + 2,
RelationKind::Blocks,
"Next",
));
}
let g = graph(&owned, "done");
assert!(g.validate_snapshot().is_clean());
let chain = g.dependency_chain(1);
assert!(chain.truncated, "the defensive node bound engages");
assert!(
chain.paths.iter().all(|p| p.len() < DEPTH as usize),
"traversal is bounded"
);
assert!(
g.validate_create(DEPTH, 1, RelationKind::Blocks)
.has(DiagnosticKind::Cycle)
);
}
#[test]
fn a_hand_edited_cyclic_graph_terminates() {
let mut two = Owned::new(2, "B", "todo");
let mut one = Owned::new(1, "A", "todo");
one.blocks(&two);
two.with(Relationship::new(2, 1, RelationKind::Blocks, "A"));
let owned = vec![one, two];
let g = graph(&owned, "done");
assert!(g.validate_snapshot().has(DiagnosticKind::Cycle));
let chain = g.dependency_chain(1);
assert!(
chain.paths.iter().all(|p| p.len() <= 2),
"the visited-set guard bounds every path"
);
}
#[test]
fn relationship_identity_survives_a_retarget_and_a_rename() {
let original = Relationship::new(1, 2, RelationKind::Blocks, "Original title");
let id = original.id.clone();
assert!(!id.is_empty());
let mut retargeted = original.clone();
retargeted.to = reference(9);
retargeted.refresh_display("New target");
assert_eq!(retargeted.id, id, "identity survives a retarget");
assert_eq!(retargeted.display, "blocks #9 New target");
let mut renamed = original;
renamed.refresh_display("Renamed title");
assert_eq!(renamed.id, id, "identity survives a rename");
assert_eq!(renamed.display, "blocks #2 Renamed title");
}
#[test]
fn generated_ids_are_unique_and_well_formed() {
let ids: std::collections::HashSet<String> =
(0..5_000).map(|_| generate_relationship_id()).collect();
assert_eq!(ids.len(), 5_000, "no collisions across 5,000 generations");
assert!(
ids.iter()
.all(|id| id.starts_with("rel_") && id.len() == 30)
);
}
#[test]
fn p1_related_reads_as_one_association_across_orientations() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Related, "B"));
let mut two = Owned::new(2, "B", "todo");
two.with(Relationship::new(2, 1, RelationKind::Related, "A"));
let owned = vec![one, two];
let g = graph(&owned, "done");
assert_eq!(g.related_to(1), vec![2], "one association, not two");
assert_eq!(g.related_to(2), vec![1]);
assert!(
g.validate_snapshot().is_clean(),
"a redundant spelling is not invalid data"
);
}
#[test]
fn d1_find_canonical_is_the_write_path_lookup() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Blocks, "B"));
one.with(Relationship::new(1, 3, RelationKind::Related, "C"));
let owned = vec![one, Owned::new(2, "B", "todo"), Owned::new(3, "C", "todo")];
let g = graph(&owned, "done");
let blocks = g
.find_canonical(1, 2, RelationKind::Blocks)
.expect("1 blocks 2");
assert_eq!(
(blocks.from, blocks.to, blocks.kind),
(reference(1), reference(2), RelationKind::Blocks)
);
assert_eq!(
g.find_canonical(2, 1, RelationKind::BlockedBy)
.map(|r| r.id.clone()),
Some(blocks.id.clone())
);
assert_eq!(
g.find_canonical(3, 1, RelationKind::Related)
.map(|r| r.id.clone()),
g.find_canonical(1, 3, RelationKind::Related)
.map(|r| r.id.clone()),
"related is order-normalized, so either orientation finds the row"
);
assert!(g.find_canonical(1, 99, RelationKind::Blocks).is_none());
assert!(g.find_canonical(1, 2, RelationKind::Related).is_none());
assert!(g.find_canonical(2, 1, RelationKind::Blocks).is_none());
}
#[test]
fn d1_find_canonical_agrees_with_validate_edit() {
let owned = chain_fixture();
let g = graph(&owned, "done");
for (from, to, kind) in [
(1, 2, RelationKind::Blocks),
(2, 1, RelationKind::BlockedBy),
(2, 3, RelationKind::Blocks),
] {
let found = g
.find_canonical(from, to, kind)
.unwrap_or_else(|| panic!("{from} {kind} {to} should resolve"))
.id
.clone();
let edit = g.validate_edit(&found, Some(to), Some(kind));
assert!(
!edit.has(DiagnosticKind::UnknownRelation),
"an id returned by find_canonical must not be unknown to validate_edit"
);
}
assert!(g.find_canonical(3, 1, RelationKind::Blocks).is_none());
assert!(g.find_canonical(1, 2, RelationKind::Duplicates).is_none());
}
#[test]
fn a3_both_stored_ids_keep_resolving() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Related, "B"));
let mut two = Owned::new(2, "B", "todo");
two.with(Relationship::new(2, 1, RelationKind::Related, "A"));
let owned = vec![one, two];
let g = graph(&owned, "done");
let ids: Vec<String> = g
.all_relationships()
.iter()
.filter(|r| r.kind == RelationKind::Related)
.map(|r| r.id.clone())
.collect();
assert_eq!(ids.len(), 2, "both rows retained");
for id in &ids {
assert!(g.find_relationship(id).is_some(), "{id} must still resolve");
assert!(
!g.validate_edit(id, None, None)
.has(DiagnosticKind::UnknownRelation),
"editing by a stored id must not report it unknown"
);
}
}
#[test]
fn a3_redundant_rows_do_not_repeat_in_the_adjacency() {
let mut one = Owned::new(1, "A", "todo");
for _ in 0..3 {
one.with(Relationship::new(1, 2, RelationKind::Blocks, "B"));
}
let owned = vec![one, Owned::new(2, "B", "todo")];
let g = graph(&owned, "done");
assert_eq!(g.blocks(1), &[2], "no repeated neighbour");
assert_eq!(g.blocked_by(2), &[1]);
assert_eq!(g.blocked_state(2).blockers.len(), 1);
assert_eq!(
g.blocked_state(2).reason,
"Blocked by #1 A",
"one blocker is named once, not once per stored row"
);
}
#[test]
fn c_an_unconfigured_done_column_fails_closed() {
let mut one = Owned::new(1, "A", "");
one.with(Relationship::new(1, 2, RelationKind::Blocks, "B"));
let owned = vec![one, Owned::new(2, "B", "todo")];
let g = graph(&owned, "");
assert!(
g.blocked_state(2).is_blocked,
"an empty status must not be read as completed"
);
assert_eq!(g.done_column(), "");
}
#[test]
fn e_a_mixed_kind_graph_validates_clean_in_one_pass() {
const N: u32 = 3_000;
let mut owned: Vec<Owned> = (0..N)
.map(|n| Owned::new(n + 1, &format!("I{n}"), "todo"))
.collect();
for n in 0..N {
let t = n + 1;
if t < N {
owned[n as usize].with(Relationship::new(n + 1, t + 1, RelationKind::Blocks, "x"));
}
if t + 1 < N {
owned[n as usize].with(Relationship::new(n + 1, t + 2, RelationKind::Related, "x"));
}
if t + 2 < N {
owned[n as usize].with(Relationship::new(
n + 1,
t + 3,
RelationKind::Duplicates,
"x",
));
}
}
let g = graph(&owned, "done");
assert!(g.edge_count() >= 8_900, "built {}", g.edge_count());
assert!(
g.validate_snapshot().is_clean(),
"a well-formed mixed-kind graph validates clean"
);
}
#[test]
fn p1_duplicates_keeps_both_orientations() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Duplicates, "B"));
let mut two = Owned::new(2, "B", "todo");
two.with(Relationship::new(2, 1, RelationKind::Duplicates, "A"));
let owned = vec![one, two];
let g = graph(&owned, "done");
assert_eq!(
g.edge_count(),
2,
"direction carries meaning for duplicates"
);
assert_eq!(g.duplicates_of(1), vec![2]);
assert_eq!(g.duplicates_of(2), vec![1]);
}
#[test]
fn p2_an_edit_cannot_duplicate_an_existing_relationship() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Blocks, "B"));
one.with(Relationship::new(1, 2, RelationKind::Related, "B"));
let owned = vec![one, Owned::new(2, "B", "todo")];
let g = graph(&owned, "done");
let blocks_id = g
.relationships_for(1)
.iter()
.find(|r| r.kind == RelationKind::Blocks)
.expect("blocks edge")
.id
.clone();
let res = g.validate_edit(&blocks_id, None, Some(RelationKind::Related));
assert!(
res.has(DiagnosticKind::ConflictingEdge),
"re-kinding onto an occupied triple must be rejected: {res:?}"
);
}
#[test]
fn p2_creating_an_existing_relationship_is_idempotent() {
let owned = chain_fixture();
let g = graph(&owned, "done");
assert!(
g.validate_create(1, 2, RelationKind::Blocks).is_clean(),
"running tix link twice must be a no-op, not a failure"
);
}
#[test]
fn p3_p4_accessors_return_the_other_endpoint_in_both_orientations() {
let mut two = Owned::new(2, "B", "todo");
two.with(Relationship::new(2, 1, RelationKind::Related, "A"));
two.with(Relationship::new(2, 1, RelationKind::Duplicates, "A"));
let owned = vec![Owned::new(1, "A", "todo"), two];
let g = graph(&owned, "done");
assert_eq!(g.related_to(1), vec![2], "not the subject");
assert_eq!(g.related_to(2), vec![1]);
assert!(
g.duplicates_of(1).is_empty(),
"a directional edge pointing at #1 does not mean #1 duplicates it: {:?}",
g.duplicates_of(1)
);
assert_eq!(g.duplicates_of(2), vec![1]);
}
#[test]
fn p3_p4_accessors_also_hold_when_the_subject_is_the_source() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Related, "B"));
one.with(Relationship::new(1, 2, RelationKind::Duplicates, "B"));
let owned = vec![one, Owned::new(2, "B", "todo")];
let g = graph(&owned, "done");
assert_eq!(g.related_to(1), vec![2]);
assert_eq!(g.related_to(2), vec![1], "related is non-directional");
assert_eq!(g.duplicates_of(1), vec![2], "1 declares the duplicate");
assert!(
g.duplicates_of(2).is_empty(),
"2 is the target, so it declares no duplicate of its own: {:?}",
g.duplicates_of(2)
);
}
#[test]
fn p5_blocked_adjacency_never_contains_the_subject() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 1, RelationKind::Blocks, "A"));
one.with(Relationship::new(1, 2, RelationKind::Blocks, "B"));
let owned = vec![one, Owned::new(2, "B", "todo")];
let g = graph(&owned, "done");
for n in [1, 2] {
assert!(
!g.blocked_by(n).contains(&n),
"issue #{n} must not be in its own inbound blocks adjacency"
);
}
}
#[test]
fn r1_display_tracks_the_current_target_title() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Blocks, "Old title"));
let owned = vec![one, Owned::new(2, "New title", "todo")];
let g = graph(&owned, "done");
assert_eq!(
g.relationships_for(1)[0].display,
"blocks #2 New title",
"a display name is a cache of the target title; a stale one is user-visible"
);
}
#[test]
fn r2_a_self_link_is_listed_once_and_kept_out_of_the_adjacency() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 1, RelationKind::Blocks, "A"));
let owned = vec![one];
let g = graph(&owned, "done");
assert_eq!(g.edge_count(), 1);
assert_eq!(
g.relationships_for(1).len(),
1,
"relationships_for and edge_count must agree"
);
assert!(g.blocks(1).is_empty(), "a self-link is not a real edge");
assert!(g.blocked_by(1).is_empty());
}
#[test]
fn r3_an_issue_is_never_blocked_by_itself() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 1, RelationKind::Blocks, "A"));
let owned = vec![one];
let g = graph(&owned, "done");
assert!(
g.validate_snapshot().has(DiagnosticKind::SelfLink),
"still reported as invalid data"
);
let state = g.blocked_state(1);
assert!(!state.is_blocked, "an issue must not block itself");
assert!(
!state.reason.contains("#1"),
"the shared move-warning text must never name the issue as its own blocker: {:?}",
state.reason
);
}
#[test]
fn r4_validate_edit_never_reports_clean_on_corrupt_data() {
let mut one = Owned::new(1, "A", "todo");
one.with(Relationship::new(1, 2, RelationKind::Blocks, "B"));
one.with(Relationship::new(1, 2, RelationKind::Related, "B"));
let owned = vec![one, Owned::new(2, "B", "todo")];
let g = graph(&owned, "done");
let blocks_id = g
.relationships_for(1)
.iter()
.find(|r| r.kind == RelationKind::Blocks)
.expect("a blocks edge")
.id
.clone();
let snapshot = g.validate_snapshot();
let edit = g.validate_edit(&blocks_id, None, None);
assert!(
snapshot.has(DiagnosticKind::ConflictingEdge) && edit.has(DiagnosticKind::ConflictingEdge),
"edit and snapshot must agree: snapshot={snapshot:?} edit={edit:?}"
);
}
#[test]
fn r4_an_edge_never_conflicts_with_itself() {
let owned = chain_fixture();
let g = graph(&owned, "done");
let id = g.relationships_for(1)[0].id.clone();
assert!(
g.validate_edit(&id, None, None).is_clean(),
"an identity edit is not a conflict: {:?}",
g.validate_edit(&id, None, None).errors
);
}
#[test]
fn an_unknown_issue_yields_empty_reads_rather_than_an_error() {
let owned = chain_fixture();
let g = graph(&owned, "done");
assert!(g.relationships_for(999).is_empty());
assert!(g.blocks(999).is_empty());
assert!(g.blocked_by(999).is_empty());
assert!(g.duplicates_of(999).is_empty());
assert!(g.related_to(999).is_empty());
assert!(!g.blocked_state(999).is_blocked);
assert!(g.dependency_chain(999).paths.is_empty());
assert!(g.find_relationship("rel_nope").is_none());
}