import os import unittest import yaml class TestCategoriesInvokableExclusion(unittest.TestCase): def test_no_child_invokable_if_any_parent_is_invokable(self): """ Verify that if any ancestor in the hierarchy is invokable, none of its descendants may be invokable. """ # locate roles/categories.yml base_dir = os.path.dirname(__file__) yaml_path = os.path.abspath( os.path.join(base_dir, '..', '..', 'roles', 'categories.yml') ) with open(yaml_path, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) violations = [] def recurse(node: dict, path: list[str], ancestor_invokable: bool): for key, value in node.items(): if not isinstance(value, dict): continue is_invokable = value.get('invokable', False) current_path = path + [key] # Violation: a descendant is invokable despite an invokable ancestor if ancestor_invokable and is_invokable: ancestor_name = '.'.join(path) if path else '' violations.append( f"{'.'.join(current_path)} is invokable, " f"but its ancestor ({ancestor_name}) is also invokable." ) # Any_invokable = True if this node or any ancestor is invokable new_ancestor_flag = ancestor_invokable or is_invokable # Recurse into subcategories for subkey, subval in value.items(): if isinstance(subval, dict): recurse({subkey: subval}, current_path, new_ancestor_flag) # start at top-level roles, with no invokable ancestor recurse(data.get('roles', {}), [], False) if violations: self.fail( "Found invokable descendants under invokable parents:\n" + "\n".join(violations) ) if __name__ == '__main__': unittest.main()