from __future__ import annotations import unittest from docforge.errors import DocForgeError from docforge.treesitter_logic import ( TreeSitterLogicOwner, analyze_cpp_source, analyze_javascript_source, ) class JavaScriptLogicTests(unittest.TestCase): def test_branches_short_circuit_and_converge(self) -> None: source = """ function choose(enabled, ready) { if (enabled && ready()) { accept(); } else { reject(); } return enabled; } """.strip() projection = analyze_javascript_source( source, source_id="source.javascript", owners=(TreeSitterLogicOwner("js.symbol.choose", "choose", 1),), )[0] kinds = {node.kind for node in projection.nodes} labels = {node.label for node in projection.nodes} relations = {edge.relation for edge in projection.edges} self.assertIn("condition", kinds) self.assertIn("convergence", kinds) self.assertIn("Decision convergence", labels) self.assertIn("when_true", relations) self.assertIn("when_false", relations) self.assertIn("return", relations) def test_arrow_function_expression_is_a_returning_projection(self) -> None: source = "const choose = (enabled) => enabled ? accept() : reject();" projection = analyze_javascript_source( source, source_id="source.javascript", owners=(TreeSitterLogicOwner("js.symbol.choose", "choose", 1),), )[0] labels = {node.label for node in projection.nodes} self.assertIn("enabled", labels) self.assertIn("return accept()", labels) self.assertIn("return reject()", labels) def test_loops_switch_and_exception_paths_are_preserved(self) -> None: source = """ function process(items, mode) { for (const item of items) { if (!item.ready) continue; use(item); } switch (mode) { case 1: one(); break; default: fallback(); } try { risk(); } catch (error) { recover(error); } finally { clean(); } } """.strip() projection = analyze_javascript_source( source, source_id="source.javascript", owners=(TreeSitterLogicOwner("js.symbol.process", "process", 1),), )[0] kinds = {node.kind for node in projection.nodes} labels = {node.label for node in projection.nodes} relations = {edge.relation for edge in projection.edges} self.assertTrue({"loop", "continue", "case", "try", "except", "finally"} <= kinds) self.assertIn("Case convergence", labels) self.assertIn("Exception convergence", labels) self.assertTrue({"loop", "continue", "case", "exception"} <= relations) class CppLogicTests(unittest.TestCase): def test_cpp_function_branches_and_throws(self) -> None: source = """ int choose(bool enabled) { if (enabled) { return 1; } throw Error(); } """.strip() projection = analyze_cpp_source( source, source_id="source.cpp", owners=(TreeSitterLogicOwner("cpp.symbol.choose", "choose", 1),), )[0] kinds = {node.kind for node in projection.nodes} relations = {edge.relation for edge in projection.edges} self.assertTrue({"entry", "condition", "return", "raise", "exit"} <= kinds) self.assertTrue({"when_true", "when_false", "return", "raise"} <= relations) def test_cpp_qualified_method_owner_is_resolved(self) -> None: source = """ class Worker { public: int run(bool ready) { while (ready) { ready = tick(); } return 0; } }; """.strip() projection = analyze_cpp_source( source, source_id="source.cpp", owners=(TreeSitterLogicOwner("cpp.symbol.worker.run", "Worker.run", 3),), )[0] labels = {node.label for node in projection.nodes} self.assertIn("Loop exit", labels) self.assertIn("return 0", labels) def test_invalid_source_and_missing_owner_fail_closed(self) -> None: with self.assertRaises(DocForgeError) as invalid: analyze_cpp_source( "int broken( {", source_id="source.cpp", owners=(), ) self.assertEqual(invalid.exception.code, "invalid_logic_source") with self.assertRaises(DocForgeError) as missing: analyze_javascript_source( "function exists() {}", source_id="source.javascript", owners=(TreeSitterLogicOwner("js.symbol.missing", "missing", 1),), ) self.assertEqual(missing.exception.code, "missing_logic_owner") if __name__ == "__main__": unittest.main()