import unittest from unittest.mock import patch from src.command_graph import CommandGraph, build_command_graph from src.models import PortingModule def _module(name: str, source_hint: str) -> PortingModule: return PortingModule(name=name, responsibility="stub", source_hint=source_hint) class CommandGraphTests(unittest.TestCase): # -- construction & immutability ------------------------------------------ def test_empty_graph(self) -> None: graph = CommandGraph(builtins=(), plugin_like=(), skill_like=()) self.assertEqual(graph.builtins, ()) self.assertEqual(graph.plugin_like, ()) self.assertEqual(graph.skill_like, ()) def test_fields_are_tuples(self) -> None: b = (_module("b1", "core/b1.ts"),) p = (_module("p1", "plugin/p1.ts"),) s = (_module("s1", "skills/s1.ts"),) graph = CommandGraph(builtins=b, plugin_like=p, skill_like=s) self.assertIsInstance(graph.builtins, tuple) self.assertIsInstance(graph.plugin_like, tuple) self.assertIsInstance(graph.skill_like, tuple) def test_frozen_dataclass_rejects_mutation(self) -> None: graph = CommandGraph(builtins=(), plugin_like=(), skill_like=()) with self.assertRaises(AttributeError): graph.builtins = () # type: ignore[misc] # -- flattened ------------------------------------------------------------- def test_flattened_combines_all_categories(self) -> None: b = (_module("b1", "core/b1.ts"),) p = (_module("p1", "plugin/p1.ts"),) s = (_module("s1", "skills/s1.ts"),) graph = CommandGraph(builtins=b, plugin_like=p, skill_like=s) self.assertEqual(graph.flattened(), b + p + s) def test_flattened_preserves_order(self) -> None: b1 = _module("b1", "core/b1.ts") b2 = _module("b2", "core/b2.ts") p1 = _module("p1", "plugin/p1.ts") s1 = _module("s1", "skills/s1.ts") graph = CommandGraph(builtins=(b1, b2), plugin_like=(p1,), skill_like=(s1,)) self.assertEqual(graph.flattened(), (b1, b2, p1, s1)) def test_flattened_of_empty_graph_returns_empty_tuple(self) -> None: graph = CommandGraph(builtins=(), plugin_like=(), skill_like=()) self.assertEqual(graph.flattened(), ()) def test_flattened_length_is_sum_of_categories(self) -> None: b = (_module("b1", "core/b1.ts"), _module("b2", "core/b2.ts")) p = (_module("p1", "plugin/p1.ts"),) s = () graph = CommandGraph(builtins=b, plugin_like=p, skill_like=s) self.assertEqual(len(graph.flattened()), 3) # -- as_markdown ----------------------------------------------------------- def test_as_markdown_includes_header(self) -> None: graph = CommandGraph(builtins=(), plugin_like=(), skill_like=()) md = graph.as_markdown() self.assertIn("# Command Graph", md) def test_as_markdown_includes_counts(self) -> None: b = (_module("b1", "core/b1.ts"), _module("b2", "core/b2.ts")) p = (_module("p1", "plugin/p1.ts"),) s = (_module("s1", "skills/s1.ts"), _module("s2", "skills/s2.ts"), _module("s3", "skills/s3.ts")) graph = CommandGraph(builtins=b, plugin_like=p, skill_like=s) md = graph.as_markdown() self.assertIn("Builtins: 2", md) self.assertIn("Plugin-like commands: 1", md) self.assertIn("Skill-like commands: 3", md) def test_as_markdown_empty_counts(self) -> None: graph = CommandGraph(builtins=(), plugin_like=(), skill_like=()) md = graph.as_markdown() self.assertIn("Builtins: 0", md) self.assertIn("Plugin-like commands: 0", md) self.assertIn("Skill-like commands: 0", md) def test_as_markdown_returns_string(self) -> None: graph = CommandGraph(builtins=(), plugin_like=(), skill_like=()) self.assertIsInstance(graph.as_markdown(), str) class BuildCommandGraphTests(unittest.TestCase): # -- return type ----------------------------------------------------------- @patch("src.command_graph.get_commands") def test_returns_command_graph(self, mock_get: unittest.mock.MagicMock) -> None: mock_get.return_value = () result = build_command_graph() self.assertIsInstance(result, CommandGraph) # -- categorization -------------------------------------------------------- @patch("src.command_graph.get_commands") def test_plugin_source_goes_to_plugin_like(self, mock_get: unittest.mock.MagicMock) -> None: p = _module("p1", "plugin/p1.ts") mock_get.return_value = (p,) graph = build_command_graph() self.assertIn(p, graph.plugin_like) self.assertNotIn(p, graph.builtins) self.assertNotIn(p, graph.skill_like) @patch("src.command_graph.get_commands") def test_skills_source_goes_to_skill_like(self, mock_get: unittest.mock.MagicMock) -> None: s = _module("s1", "skills/s1.ts") mock_get.return_value = (s,) graph = build_command_graph() self.assertIn(s, graph.skill_like) self.assertNotIn(s, graph.builtins) self.assertNotIn(s, graph.plugin_like) @patch("src.command_graph.get_commands") def test_plain_source_goes_to_builtins(self, mock_get: unittest.mock.MagicMock) -> None: b = _module("b1", "core/b1.ts") mock_get.return_value = (b,) graph = build_command_graph() self.assertIn(b, graph.builtins) self.assertNotIn(b, graph.plugin_like) self.assertNotIn(b, graph.skill_like) @patch("src.command_graph.get_commands") def test_case_insensitive_plugin_match(self, mock_get: unittest.mock.MagicMock) -> None: p = _module("p1", "Plugin/p1.ts") mock_get.return_value = (p,) graph = build_command_graph() self.assertIn(p, graph.plugin_like) @patch("src.command_graph.get_commands") def test_case_insensitive_skills_match(self, mock_get: unittest.mock.MagicMock) -> None: s = _module("s1", "Skills/s1.ts") mock_get.return_value = (s,) graph = build_command_graph() self.assertIn(s, graph.skill_like) @patch("src.command_graph.get_commands") def test_mixed_commands_are_sorted_correctly(self, mock_get: unittest.mock.MagicMock) -> None: b = _module("b1", "core/b1.ts") p = _module("p1", "plugin/p1.ts") s = _module("s1", "skills/s1.ts") mock_get.return_value = (b, p, s) graph = build_command_graph() self.assertEqual(graph.builtins, (b,)) self.assertEqual(graph.plugin_like, (p,)) self.assertEqual(graph.skill_like, (s,)) @patch("src.command_graph.get_commands") def test_empty_commands_yields_empty_graph(self, mock_get: unittest.mock.MagicMock) -> None: mock_get.return_value = () graph = build_command_graph() self.assertEqual(graph.builtins, ()) self.assertEqual(graph.plugin_like, ()) self.assertEqual(graph.skill_like, ()) @patch("src.command_graph.get_commands") def test_all_builtins(self, mock_get: unittest.mock.MagicMock) -> None: b1 = _module("b1", "core/b1.ts") b2 = _module("b2", "commands/b2.ts") mock_get.return_value = (b1, b2) graph = build_command_graph() self.assertEqual(len(graph.builtins), 2) self.assertEqual(graph.plugin_like, ()) self.assertEqual(graph.skill_like, ()) @patch("src.command_graph.get_commands") def test_builtins_plugin_skill_are_tuples_of_porting_module(self, mock_get: unittest.mock.MagicMock) -> None: b = _module("b1", "core/b1.ts") p = _module("p1", "plugin/p1.ts") s = _module("s1", "skills/s1.ts") mock_get.return_value = (b, p, s) graph = build_command_graph() for category in (graph.builtins, graph.plugin_like, graph.skill_like): self.assertIsInstance(category, tuple) for item in category: self.assertIsInstance(item, PortingModule) @patch("src.command_graph.get_commands") def test_flattened_matches_original_commands(self, mock_get: unittest.mock.MagicMock) -> None: modules = ( _module("b1", "core/b1.ts"), _module("p1", "plugin/p1.ts"), _module("s1", "skills/s1.ts"), ) mock_get.return_value = modules graph = build_command_graph() self.assertEqual(set(graph.flattened()), set(modules)) if __name__ == "__main__": unittest.main()