# Copyright (c) 2026 Patrick Motsch # All rights reserved. """Pure-Python unit tests for the orphan detection in ``serviceRedmineStats._countOrphans``. Snapshot in ``tests/fixtures/redmineSnapshot.json`` contains: - 1x Userstory (1001) -- root - 1x Feature (2001) related to 1001 -> reachable - 1x Acc.Crit (3001) parent=2001 -> reachable - 1x Bug (4001) blocks 2001 -> reachable via relation - 2x Task (5001, 5002) -> orphan (no link to any User Story) """ from __future__ import annotations from modules.features.redmine.serviceRedmineStats import _countOrphans from tests.fixtures.loadRedmineSnapshot import loadSnapshot class TestCountOrphans: def test_orphansFromSnapshot(self) -> None: schema, tickets = loadSnapshot() orphans = _countOrphans(tickets, schema.rootTrackerId) assert orphans == 2, "Two unrelated Tasks should be orphans" def test_emptyListReturnsZero(self) -> None: assert _countOrphans([], 1) == 0 def test_noRootTrackerCountsAllAsOrphan(self) -> None: schema, tickets = loadSnapshot() # Pretend there is no User Story tracker at all -- every ticket is orphan. assert _countOrphans(tickets, None) == len(tickets) def test_relationDirectionAgnostic(self) -> None: """A ticket reachable via the *target* side of a relation must not be counted as orphan -- _countOrphans walks both directions.""" _, tickets = loadSnapshot() bug = next(t for t in tickets if t.id == 4001) # Bug 4001 -[blocks]-> 2001; it is the source. Reverse it: 2001 -[blocks]-> 4001 bug.relations = [] # Attach the relation on 2001 instead. feature = next(t for t in tickets if t.id == 2001) from modules.features.redmine.datamodelRedmine import RedmineRelationDto feature.relations.append( RedmineRelationDto(id=999, issueId=2001, issueToId=4001, relationType="blocks", delay=None) ) orphans = _countOrphans(tickets, 1) assert orphans == 2 # Tasks remain orphans, Bug is still reachable