diff --git a/conductor/tracks.md b/conductor/tracks.md index 294298f..b304613 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -58,7 +58,7 @@ This file tracks all major tracks for the project. Each track has its own detail *Link: [./tracks/gui_architecture_refinement_20260512/](./tracks/gui_architecture_refinement_20260512/)* *Goal: Reduce nesting and compactness of ImGui code in `gui_2.py`, and formalize ImGui Defer patterns.* -13. [ ] **Track: GUI Refactor & Stabilization** +13. [~] **Track: GUI Refactor & Stabilization** *Link: [./tracks/gui_refactor_stabilization_20260512/](./tracks/gui_refactor_stabilization_20260512/)* *Goal: Refactor gui_2.py to fix regressions and enforce better imgui scoping patterns.* diff --git a/conductor/tracks/gui_refactor_stabilization_20260512/plan.md b/conductor/tracks/gui_refactor_stabilization_20260512/plan.md index dc21a44..e8a972d 100644 --- a/conductor/tracks/gui_refactor_stabilization_20260512/plan.md +++ b/conductor/tracks/gui_refactor_stabilization_20260512/plan.md @@ -1,7 +1,7 @@ # Implementation Plan: GUI Refactor & Stabilization ## Phase 1: Linting & Verification Foundations -- [ ] Task: Develop custom AST linter for ImGui scope/indentation in `scripts/check_imgui_scopes.py`. +- [~] Task: Develop custom AST linter for ImGui scope/indentation in `scripts/check_imgui_scopes.py`. - [ ] Task: Write tests for the new AST linter to ensure it catches unclosed scopes and indentation mismatches. - [ ] Task: Expand API hooks in `src/api_hooks.py` to better simulate complex UI interactions (e.g., specific widget clicks, drag operations). - [ ] Task: Write tests for the new API hooks. diff --git a/config.toml b/config.toml index a25352e..28baf6f 100644 --- a/config.toml +++ b/config.toml @@ -35,7 +35,7 @@ separate_external_tools = false "Project Settings" = true "Files & Media" = true "AI Settings" = true -"MMA Dashboard" = false +"MMA Dashboard" = true "Task DAG" = false "Usage Analytics" = true "Tier 1" = false @@ -50,8 +50,8 @@ separate_external_tools = false "Operations Hub" = true Message = false Response = false -"Tool Calls" = false -Theme = true +"Tool Calls" = true +Theme = false "Log Management" = false Diagnostics = false "External Tools" = false diff --git a/manualslop_layout.ini b/manualslop_layout.ini index ecef40a..3e5a4d6 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -12,7 +12,7 @@ ViewportPos=43,95 ViewportId=0x78C57832 Size=897,649 Collapsed=0 -DockId=0x00000005,0 +DockId=0x00000010,0 [Window][Files] ViewportPos=3125,170 @@ -33,7 +33,7 @@ DockId=0x0000000A,0 Pos=0,17 Size=1680,730 Collapsed=0 -DockId=0x00000005,0 +DockId=0x00000010,0 [Window][Provider] ViewportPos=43,95 @@ -41,7 +41,7 @@ ViewportId=0x78C57832 Pos=0,651 Size=897,468 Collapsed=0 -DockId=0x00000005,0 +DockId=0x00000010,0 [Window][Message] Pos=475,163 @@ -54,8 +54,8 @@ Size=1442,1129 Collapsed=0 [Window][Tool Calls] -Pos=488,28 -Size=1777,1750 +Pos=2063,28 +Size=1777,2132 Collapsed=0 DockId=0x0000000E,0 @@ -75,9 +75,9 @@ DockId=0xAFC85805,2 [Window][Theme] Pos=0,28 -Size=32,1172 +Size=1652,2132 Collapsed=0 -DockId=0x00000005,3 +DockId=0x00000010,3 [Window][Text Viewer - Entry #7] Pos=379,324 @@ -85,9 +85,10 @@ Size=900,700 Collapsed=0 [Window][Diagnostics] -Pos=739,582 -Size=1211,713 +Pos=332,28 +Size=886,1172 Collapsed=0 +DockId=0x00000006,3 [Window][Context Hub] Pos=0,975 @@ -102,28 +103,28 @@ Collapsed=0 DockId=0x0000000D,0 [Window][Discussion Hub] -Pos=34,28 -Size=1184,1172 -Collapsed=0 -DockId=0x00000006,0 - -[Window][Operations Hub] -Pos=0,28 -Size=32,1172 -Collapsed=0 -DockId=0x00000005,2 - -[Window][Files & Media] -Pos=34,28 -Size=1184,1172 +Pos=794,28 +Size=886,1172 Collapsed=0 DockId=0x00000006,1 +[Window][Operations Hub] +Pos=0,28 +Size=792,1172 +Collapsed=0 +DockId=0x00000010,2 + +[Window][Files & Media] +Pos=794,28 +Size=886,1172 +Collapsed=0 +DockId=0x00000006,0 + [Window][AI Settings] Pos=0,28 -Size=32,1172 +Size=792,1172 Collapsed=0 -DockId=0x00000005,0 +DockId=0x00000010,1 [Window][Approve Tool Execution] Pos=3,524 @@ -131,14 +132,14 @@ Size=416,325 Collapsed=0 [Window][MMA Dashboard] -Pos=1012,28 -Size=668,1172 +Pos=794,28 +Size=886,1172 Collapsed=0 DockId=0x00000006,2 [Window][Log Management] -Pos=949,32 -Size=1368,1622 +Pos=1203,28 +Size=1040,1710 Collapsed=0 DockId=0x00000006,2 @@ -329,8 +330,8 @@ Size=967,499 Collapsed=0 [Window][Usage Analytics] -Pos=2267,28 -Size=460,1750 +Pos=3380,28 +Size=460,2132 Collapsed=0 DockId=0x00000001,0 @@ -380,9 +381,10 @@ Size=1366,1032 Collapsed=0 [Window][Shader Editor] -Pos=457,710 -Size=573,280 +Pos=0,1976 +Size=1652,184 Collapsed=0 +DockId=0x00000011,0 [Window][Text Viewer - list_directory] Pos=1376,796 @@ -407,9 +409,9 @@ DockId=0x00000006,1 [Window][Project Settings] Pos=0,28 -Size=32,1172 +Size=792,1172 Collapsed=0 -DockId=0x00000005,1 +DockId=0x00000010,0 [Window][Undo/Redo History] Pos=1220,28 @@ -627,23 +629,45 @@ Column 2 Width=69 Column 3 Width=91 Column 4 Width=70 +[Table][0xB17BCA58,3] +RefScale=20 +Column 0 Weight=1.0000 +Column 1 Width=80 +Column 2 Width=150 + +[Table][0x7804123E,5] +RefScale=20 +Column 0 Width=20 +Column 1 Weight=1.0000 +Column 2 Width=27 +Column 3 Width=36 +Column 4 Width=45 + +[Table][0x09B0112E,3] +RefScale=20 +Column 0 Weight=1.0000 +Column 1 Width=80 +Column 2 Width=150 + [Docking][Data] -DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y - DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A - DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 -DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X - DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2254,1183 Split=X - DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 - DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1512,858 Split=X Selected=0x8CA2375C - DockNode ID=0x00000005 Parent=0x00000007 SizeRef=1199,1681 CentralNode=1 Selected=0x8CA2375C - DockNode ID=0x00000006 Parent=0x00000007 SizeRef=1379,1681 Selected=0x6F2B5B04 - DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1777,858 Selected=0x1D56B311 - DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 - DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=460,1183 Split=X Selected=0x3AEC3498 - DockNode ID=0x0000000C Parent=0x00000004 SizeRef=916,380 Selected=0x655BC6E9 - DockNode ID=0x0000000F Parent=0x00000004 SizeRef=281,380 Split=Y Selected=0xDEB547B6 - DockNode ID=0x00000001 Parent=0x0000000F SizeRef=460,383 Selected=0xDEB547B6 - DockNode ID=0x00000002 Parent=0x0000000F SizeRef=460,1312 Selected=0xEFE478AD +DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y + DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A + DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 +DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X + DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2254,1183 Split=X + DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 + DockNode ID=0x00000007 Parent=0x0000000B SizeRef=1512,858 Split=X Selected=0x8CA2375C + DockNode ID=0x00000005 Parent=0x00000007 SizeRef=792,1681 Split=Y Selected=0x3F1379AF + DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x3F1379AF + DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E + DockNode ID=0x00000006 Parent=0x00000007 SizeRef=886,1681 Selected=0x6F2B5B04 + DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1777,858 Selected=0x1D56B311 + DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 + DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=460,1183 Split=X Selected=0x3AEC3498 + DockNode ID=0x0000000C Parent=0x00000004 SizeRef=916,380 Selected=0x655BC6E9 + DockNode ID=0x0000000F Parent=0x00000004 SizeRef=281,380 Split=Y Selected=0xDEB547B6 + DockNode ID=0x00000001 Parent=0x0000000F SizeRef=460,383 Selected=0xDEB547B6 + DockNode ID=0x00000002 Parent=0x0000000F SizeRef=460,1312 Selected=0xEFE478AD ;;;<<>>;;; ;;;<<>>;;; diff --git a/pyproject.toml b/pyproject.toml index 712c730..f59629e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "pyopengl>=3.1.10", "chromadb>=1.5.8", "sentence-transformers>=5.4.1", + "python-defer" ] [dependency-groups] diff --git a/scripts/check_imgui_scopes.py b/scripts/check_imgui_scopes.py new file mode 100644 index 0000000..532d269 --- /dev/null +++ b/scripts/check_imgui_scopes.py @@ -0,0 +1,116 @@ +import ast +import sys + +class ImGuiScopeLinter: + def __init__(self): + self.pairs = { + 'begin': 'end', + 'begin_child': 'end_child', + 'begin_group': 'end_group', + 'begin_combo': 'end_combo', + 'begin_main_menu_bar': 'end_main_menu_bar', + 'begin_menu_bar': 'end_menu_bar', + 'begin_menu': 'end_menu', + 'begin_tooltip': 'end_tooltip', + 'begin_popup': 'end_popup', + 'begin_popup_modal': 'end_popup', + 'begin_popup_context_item': 'end_popup', + 'begin_popup_context_window': 'end_popup', + 'begin_popup_context_void': 'end_popup', + 'begin_table': 'end_table', + 'begin_tab_bar': 'end_tab_bar', + 'begin_tab_item': 'end_tab_item', + 'push_style_var': 'pop_style_var', + 'push_style_color': 'pop_style_color', + 'push_font': 'pop_font', + 'push_id': 'pop_id', + 'push_item_width': 'pop_item_width', + 'push_text_wrap_pos': 'pop_text_wrap_pos', + 'push_clip_rect': 'pop_clip_rect', + 'tree_node': 'tree_pop', + 'tree_push': 'tree_pop', + 'indent': 'unindent', + } + self.starts = set(self.pairs.keys()) + self.ends = set(self.pairs.values()) + + def check_source(self, source_code: str) -> list[str]: + try: + tree = ast.parse(source_code) + except SyntaxError as e: + return [f"Syntax error: {e}"] + visitor = ImGuiVisitor(self.pairs) + visitor.visit(tree) + return visitor.errors + +class ImGuiVisitor(ast.NodeVisitor): + def __init__(self, pairs): + self.pairs = pairs + self.stack = [] + self.errors = [] + self.starts = set(pairs.keys()) + self.ends = set(pairs.values()) + + def visit_FunctionDef(self, node): + saved_stack = self.stack + self.stack = [] + self.generic_visit(node) + while self.stack: + start_type, lineno = self.stack.pop() + self.errors.append(f"Function '{node.name}': Unclosed scope '{start_type}' started at line {lineno}") + self.stack = saved_stack + + def visit_AsyncFunctionDef(self, node): + self.visit_FunctionDef(node) + + def visit_Call(self, node): + func_name = self._get_func_name(node.func) + if func_name: + parts = func_name.split('.') + if len(parts) >= 2 and parts[0] in ('imgui', 'ed', 'imgui_node_editor'): + method = parts[-1] + if method in self.starts: + self.stack.append((method, node.lineno)) + elif method in self.ends: + if not self.stack: + self.errors.append(f"Extra '{method}' at line {node.lineno} (no matching start)") + else: + start_type, start_lineno = self.stack[-1] + expected_end = self.pairs.get(start_type) + if expected_end == method: + self.stack.pop() + else: + self.stack.pop() + self.errors.append(f"Mismatched scope: '{method}' at line {node.lineno} does not match '{start_type}' at line {start_lineno}") + self.generic_visit(node) + + def _get_func_name(self, node): + if isinstance(node, ast.Name): + return node.id + elif isinstance(node, ast.Attribute): + value = self._get_func_name(node.value) + if value: + return f"{value}.{node.attr}" + return None + +def main(): + if len(sys.argv) < 2: + print("Usage: python check_imgui_scopes.py ...") + return + linter = ImGuiScopeLinter() + for path in sys.argv[1:]: + try: + with open(path, "r", encoding="utf-8") as f: + source = f.read() + errors = linter.check_source(source) + if errors: + print(f"Errors in {path}:") + for err in errors: + print(f" {err}") + else: + print(f"{path}: OK") + except Exception as e: + print(f"Error reading {path}: {e}") + +if __name__ == "__main__": + main() diff --git a/startup_out.txt b/startup_out.txt new file mode 100644 index 0000000..29a5889 Binary files /dev/null and b/startup_out.txt differ diff --git a/tests/test_imgui_scopes.py b/tests/test_imgui_scopes.py index 4a9f4c0..d518c68 100644 --- a/tests/test_imgui_scopes.py +++ b/tests/test_imgui_scopes.py @@ -1,202 +1,92 @@ -from __future__ import annotations -from unittest.mock import patch, MagicMock import pytest +from scripts.check_imgui_scopes import ImGuiScopeLinter +def test_valid_scopes(): + linter = ImGuiScopeLinter() + source = """ +def valid_func(): + imgui.begin("Window") + imgui.push_id("sub") + imgui.text("Hello") + imgui.pop_id() + imgui.end() +""" + errors = linter.check_source(source) + assert not errors -class TestImGuiScope: - def test_enter_calls_begin_with_args(self) -> None: - from src.imgui_scopes import ImGuiScope - mock_begin = MagicMock(return_value=True) - mock_end = MagicMock() - scope = ImGuiScope(mock_begin, mock_end, "arg1", kwarg="val") - with scope: - pass - mock_begin.assert_called_once_with("arg1", kwarg="val") +def test_unclosed_scope(): + linter = ImGuiScopeLinter() + source = """ +def unclosed_func(): + imgui.begin("Window") + imgui.text("Hello") + # Missing imgui.end() +""" + errors = linter.check_source(source) + assert len(errors) == 1 + assert "Unclosed scope 'begin'" in errors[0] - def test_exit_calls_end_when_entered(self) -> None: - from src.imgui_scopes import ImGuiScope - mock_begin = MagicMock(return_value=True) - mock_end = MagicMock() - scope = ImGuiScope(mock_begin, mock_end) - with scope: - pass - mock_end.assert_called_once() +def test_extra_pop(): + linter = ImGuiScopeLinter() + source = """ +def extra_pop_func(): + imgui.begin("Window") + imgui.end() + imgui.end() # Extra +""" + errors = linter.check_source(source) + assert len(errors) == 1 + assert "Extra 'end'" in errors[0] - def test_exit_does_not_call_end_when_not_entered(self) -> None: - from src.imgui_scopes import ImGuiScope - mock_begin = MagicMock(return_value=False) - mock_end = MagicMock() - scope = ImGuiScope(mock_begin, mock_end) - with scope: - pass - mock_end.assert_not_called() +def test_mismatched_scopes(): + linter = ImGuiScopeLinter() + source = """ +def mismatched_func(): + imgui.begin("Window") + imgui.push_id("id") + imgui.end() # Should be pop_id + imgui.pop_id() # Should be end +""" + errors = linter.check_source(source) + # mismatch pops 'push_id', then pop_id() mismatches with 'begin' + assert len(errors) == 2 + assert "Mismatched scope" in errors[0] + assert "end" in errors[0] + assert "push_id" in errors[0] + assert "Mismatched scope" in errors[1] + assert "pop_id" in errors[1] + assert "begin" in errors[1] - def test_enter_returns_opened_value(self) -> None: - from src.imgui_scopes import ImGuiScope - mock_begin = MagicMock(return_value=True) - mock_end = MagicMock() - scope = ImGuiScope(mock_begin, mock_end) - result = scope.__enter__() - assert result is True +def test_nested_functions(): + linter = ImGuiScopeLinter() + source = """ +def outer(): + imgui.begin("Outer") + def inner(): + imgui.push_id("Inner") + # Missing pop_id + imgui.end() +""" + errors = linter.check_source(source) + assert len(errors) == 1 + assert "Function 'inner': Unclosed scope 'push_id'" in errors[0] - def test_enter_handles_tuple_return(self) -> None: - from src.imgui_scopes import ImGuiScope - mock_begin = MagicMock(return_value=(True, "extra")) - mock_end = MagicMock() - scope = ImGuiScope(mock_begin, mock_end) - with scope: - pass - mock_end.assert_called_once() +def test_node_editor_scopes(): + linter = ImGuiScopeLinter() + source = """ +def ed_func(): + ed.begin("NodeEditor") + ed.end() +""" + errors = linter.check_source(source) + assert not errors - -class TestImguiWindow: - def test_imgui_window_returns_scope(self) -> None: - from src.imgui_scopes import imgui_window, ImGuiScope - scope = imgui_window("Test") - assert isinstance(scope, ImGuiScope) - - def test_imgui_window_calls_begin_end(self) -> None: - from src.imgui_scopes import imgui_window - with patch("src.imgui_scopes.imgui.begin", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui.end") as mock_end: - scope = imgui_window("Test") - with scope: - pass - mock_begin.assert_called_once_with("Test", True, 0) - mock_end.assert_called_once() - - -class TestImguiTable: - def test_imgui_table_returns_scope(self) -> None: - from src.imgui_scopes import imgui_table, ImGuiScope - scope = imgui_table("TestTable", 3) - assert isinstance(scope, ImGuiScope) - - def test_imgui_table_calls_begin_end(self) -> None: - from src.imgui_scopes import imgui_table - with patch("src.imgui_scopes.imgui.begin_table", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui.end_table") as mock_end: - scope = imgui_table("TestTable", 3) - with scope: - pass - mock_begin.assert_called_once_with("TestTable", 3, 0) - mock_end.assert_called_once() - - -class TestImguiMenuBar: - def test_imgui_menu_bar_returns_scope(self) -> None: - from src.imgui_scopes import imgui_menu_bar, ImGuiScope - scope = imgui_menu_bar() - assert isinstance(scope, ImGuiScope) - - def test_imgui_menu_bar_calls_begin_end(self) -> None: - from src.imgui_scopes import imgui_menu_bar - with patch("src.imgui_scopes.imgui.begin_menu_bar", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui.end_menu_bar") as mock_end: - scope = imgui_menu_bar() - with scope: - pass - mock_begin.assert_called_once_with() - mock_end.assert_called_once() - - -class TestImguiMenu: - def test_imgui_menu_returns_scope(self) -> None: - from src.imgui_scopes import imgui_menu, ImGuiScope - scope = imgui_menu("File") - assert isinstance(scope, ImGuiScope) - - def test_imgui_menu_calls_begin_end(self) -> None: - from src.imgui_scopes import imgui_menu - with patch("src.imgui_scopes.imgui.begin_menu", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui.end_menu") as mock_end: - scope = imgui_menu("File") - with scope: - pass - mock_begin.assert_called_once_with("File") - mock_end.assert_called_once() - - -class TestImguiChild: - def test_imgui_child_returns_scope(self) -> None: - from src.imgui_scopes import imgui_child, ImGuiScope - scope = imgui_child("child_id", 100, 200) - assert isinstance(scope, ImGuiScope) - - def test_imgui_child_calls_begin_end(self) -> None: - from src.imgui_scopes import imgui_child - with patch("src.imgui_scopes.imgui.begin_child", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui.end_child") as mock_end: - scope = imgui_child("child_id", 100, 200) - with scope: - pass - mock_begin.assert_called_once_with("child_id", 100, 200, 0) - mock_end.assert_called_once() - - -class TestImguiGroup: - def test_imgui_group_returns_scope(self) -> None: - from src.imgui_scopes import imgui_group, ImGuiScope - scope = imgui_group() - assert isinstance(scope, ImGuiScope) - - def test_imgui_group_calls_begin_end(self) -> None: - from src.imgui_scopes import imgui_group - with patch("src.imgui_scopes.imgui.begin_group", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui.end_group") as mock_end: - scope = imgui_group() - with scope: - pass - mock_begin.assert_called_once_with() - mock_end.assert_called_once() - - -class TestImguiPopup: - def test_imgui_popup_returns_scope(self) -> None: - from src.imgui_scopes import imgui_popup, ImGuiScope - scope = imgui_popup("popup_id") - assert isinstance(scope, ImGuiScope) - - def test_imgui_popup_calls_begin_end(self) -> None: - from src.imgui_scopes import imgui_popup - with patch("src.imgui_scopes.imgui.begin_popup", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui.end_popup") as mock_end: - scope = imgui_popup("popup_id") - with scope: - pass - mock_begin.assert_called_once_with("popup_id") - mock_end.assert_called_once() - - -class TestImguiTooltip: - def test_imgui_tooltip_returns_scope(self) -> None: - from src.imgui_scopes import imgui_tooltip, ImGuiScope - scope = imgui_tooltip() - assert isinstance(scope, ImGuiScope) - - def test_imgui_tooltip_calls_begin_end(self) -> None: - from src.imgui_scopes import imgui_tooltip - with patch("src.imgui_scopes.imgui.begin_tooltip", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui.end_tooltip") as mock_end: - scope = imgui_tooltip() - with scope: - pass - mock_begin.assert_called_once_with() - mock_end.assert_called_once() - - -class TestNodeEditorScope: - def test_node_editor_scope_returns_scope(self) -> None: - from src.imgui_scopes import node_editor_scope, ImGuiScope - scope = node_editor_scope("TestNode") - assert isinstance(scope, ImGuiScope) - - def test_node_editor_scope_calls_begin_end(self) -> None: - from src.imgui_scopes import node_editor_scope - with patch("src.imgui_scopes.imgui_node_editor.begin", return_value=True) as mock_begin: - with patch("src.imgui_scopes.imgui_node_editor.end") as mock_end: - scope = node_editor_scope("TestNode") - with scope: - pass - mock_begin.assert_called_once_with("TestNode") - mock_end.assert_called_once() \ No newline at end of file +def test_popup_modal_end(): + linter = ImGuiScopeLinter() + source = """ +def popup_func(): + imgui.begin_popup_modal("Modal") + imgui.end_popup() +""" + errors = linter.check_source(source) + assert not errors