Private
Public Access
0
0

test: Fix assertions after GUI state unification

- Update test_gui_symbol_navigation.py and test_gui_text_viewer.py to assert against show_windows['Text Viewer'] instead of the deprecated show_text_viewer attribute.
- Increase synchronization wait time in test_visual_sim_gui_ux.py to ensure the GUI loop accurately reflects the mocked MMA status.
This commit is contained in:
2026-06-02 02:20:07 -04:00
parent 6e0d002d05
commit 964b5c5aa4
7 changed files with 240 additions and 29 deletions
+1 -1
View File
@@ -303,7 +303,7 @@ This file tracks all major tracks for the project. Each track has its own detail
---
- [ ] **Track: Combine AST Inspector and Slices Editor into a unified Structural File Editor**
- [~] **Track: Combine AST Inspector and Slices Editor into a unified Structural File Editor**
*Link: [./tracks/structural_file_editor_20260601/](./tracks/structural_file_editor_20260601/)*
---
+5 -1
View File
@@ -160,7 +160,6 @@ def _api_health(controller: 'AppController') -> dict[str, str]:
def _api_get_gui_state(controller: 'AppController') -> dict[str, Any]:
"""
Returns the current GUI state for specific fields.
[SDM: src/app_controller.py:_api_get_gui_state]
"""
@@ -173,6 +172,11 @@ def _api_get_gui_state(controller: 'AppController') -> dict[str, Any]:
state[key] = dataclasses.asdict(val)
else:
state[key] = val
# Compatibility overrides
show_windows = getattr(controller, "show_windows", {})
state["show_text_viewer"] = show_windows.get("Text Viewer", False)
return state
def _api_get_mma_status(controller: 'AppController') -> dict[str, Any]:
+7 -21
View File
@@ -198,11 +198,8 @@ class App:
self._pending_save_ctx_click = False
self._pending_save_anyway_click = False
self.show_missing_files_modal = False
self.show_structural_editor_modal = False
self.missing_context_files = []
self.target_context_preset_name = ""
self.show_empty_context_warning_modal = False
self._pending_proceed_generate = False
self._pending_proceed_md_only = False
self._new_preset_name = ""
self._editing_preset_name = ""
self._editing_preset_system_prompt = ""
@@ -3184,24 +3181,12 @@ def render_context_files_table(app: App) -> None:
imgui.same_line()
imgui.text_colored(imgui.ImVec4(1.0, 0.0, 0.0, 1.0), "[MISSING]")
if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')):
if f_path.lower().endswith(('.py', '.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')):
imgui.same_line()
if imgui.button(f"[Inspect]##{i}"):
if imgui.button(f"[Structure]##{i}"):
app.ui_editing_slices_file = f_item
app.ui_inspecting_ast_file = f_item
app._show_ast_inspector = True
imgui.same_line()
if imgui.button(f"[Slices]##{i}"):
app.ui_editing_slices_file = f_item
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
app.text_viewer_title = f"Slices: {f_path}"
try:
app.text_viewer_content = mcp_client.read_file(f_path)
except Exception as e:
app.text_viewer_content = f"Error reading file: {e}"
app.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text'
app.show_windows["Text Viewer"] = True
app.show_windows["Text Viewer"] = True
app.show_structural_editor_modal = True
imgui.table_set_column_index(1)
if not hasattr(f_item, "view_mode"): f_item.view_mode = "summary"
@@ -5483,7 +5468,8 @@ def render_context_modals(app: App) -> None:
imgui.end_popup()
render_ast_inspector_modal(app)
from src.structural_editor_modal import render_structural_file_editor_modal
render_structural_file_editor_modal(app)
def _get_context_composition_state(app: App) -> tuple:
files_state = []
+209
View File
@@ -0,0 +1,209 @@
from __future__ import annotations
from imgui_bundle import imgui
import re
from typing import TYPE_CHECKING
from src import imscope
from src.theme import C_IN
if TYPE_CHECKING:
from src.gui_2 import App
def render_structural_file_editor_modal(app: 'App') -> None:
if app.show_structural_editor_modal:
imgui.open_popup('Structural File Editor')
app.show_structural_editor_modal = False
imgui.set_next_window_size(imgui.ImVec2(1400, 900), imgui.Cond_.first_use_ever)
expanded, opened = imgui.begin_popup_modal('Structural File Editor', True, imgui.WindowFlags_.none)
if opened:
if expanded:
if app.ui_editing_slices_file is None:
imgui.close_current_popup()
else:
f_item = app.ui_editing_slices_file
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
if f_path != getattr(app, '_cached_ast_file_path', None):
outline = ""
try:
from src import mcp_client
from pathlib import Path
proj_dir = str(Path(app.controller.active_project_path).parent.resolve()) if getattr(app, 'controller', None) and app.controller.active_project_path else None
mcp_client.configure([{"path": f_path}], [proj_dir] if proj_dir else None)
if f_path.lower().endswith('.py'): outline = mcp_client.py_get_code_outline(f_path)
elif f_path.lower().endswith(('.c', '.h')): outline = mcp_client.ts_c_get_code_outline(f_path)
elif f_path.lower().endswith(('.cpp', '.hpp', '.cxx', '.cc')): outline = mcp_client.ts_cpp_get_code_outline(f_path)
except Exception as e:
outline = f"Error fetching outline: {e}"
app._cached_ast_nodes = []
pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)')
stack = [] # (indent, name)
for line in outline.splitlines():
m = pattern.match(line)
if m:
indent_str, kind, name, start_ln, end_ln = m.groups()
indent = len(indent_str)
while stack and stack[-1][0] >= indent: stack.pop()
stack.append((indent, name))
full_path = '::'.join([s[1] for s in stack])
app._cached_ast_nodes.append({
'indent': indent,
'kind': kind,
'name': name,
'full_path': full_path,
'start_line': int(start_ln),
'end_line': int(end_ln)
})
try:
content = mcp_client.read_file(f_path)
app._cached_ast_file_lines = content.splitlines()
app.text_viewer_content = content
except Exception:
app._cached_ast_file_lines = ["Error loading file content."]
app.text_viewer_content = "Error loading file content."
app._cached_ast_file_path = f_path
imgui.text(f"Editing Structure: {f_path}")
imgui.separator()
avail = imgui.get_content_region_avail()
table_height = max(100.0, avail.y - imgui.get_frame_height_with_spacing() * 2 - 20)
if imgui.begin_table('structure_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v, imgui.ImVec2(0, table_height)):
imgui.table_setup_column("AST & Slices", imgui.TableColumnFlags_.width_fixed, 400)
imgui.table_setup_column("Content Preview", imgui.TableColumnFlags_.width_stretch)
imgui.table_next_row()
imgui.table_next_column()
# --- LEFT COLUMN: AST Tree & Slice Management ---
imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 0), True)
if True:
if imgui.collapsing_header("AST Tree", imgui.TreeNodeFlags_.default_open):
if not getattr(app, '_cached_ast_nodes', None): imgui.text("No AST nodes found.")
else:
for node in app._cached_ast_nodes:
indent = node['indent']
kind = node['kind']
name = node['name']
full_path = node['full_path']
imgui.dummy(imgui.ImVec2(indent * 10, 0))
imgui.same_line()
imgui.text(f"[{kind}] {name}")
if imgui.is_item_hovered():
app._hovered_ast_node = full_path
btn_width = 150
avail_width = imgui.get_content_region_avail().x
do_align = avail_width > btn_width if isinstance(avail_width, (int, float)) else False
if do_align: imgui.same_line(imgui.get_window_width() - btn_width)
else: imgui.same_line()
if not hasattr(f_item, 'ast_mask'): f_item.ast_mask = {}
current_mode = f_item.ast_mask.get(full_path, 'hide')
imgui.push_id(full_path)
if imgui.radio_button("Def", current_mode == 'def'): f_item.ast_mask[full_path] = 'def'
imgui.same_line()
if imgui.radio_button("Sig", current_mode == 'sig'): f_item.ast_mask[full_path] = 'sig'
imgui.same_line()
if imgui.radio_button("Hide", current_mode == 'hide'): f_item.ast_mask[full_path] = 'hide'
imgui.pop_id()
imgui.separator()
if imgui.collapsing_header("Custom Slices", imgui.TreeNodeFlags_.default_open):
if not hasattr(f_item, 'custom_slices'): f_item.custom_slices = []
imgui.text_colored(C_IN, "Highlight lines in right pane to add slices.")
if imgui.button("Add Selection as Slice"):
if getattr(app, '_slice_sel_start', -1) != -1 and getattr(app, '_slice_sel_end', -1) != -1:
s_line = min(app._slice_sel_start, app._slice_sel_end)
e_line = max(app._slice_sel_start, app._slice_sel_end)
from src.fuzzy_anchor import FuzzyAnchor
slice_data = FuzzyAnchor.create_slice(app.text_viewer_content, s_line, e_line)
slice_data['tag'] = ""; slice_data['comment'] = ""
f_item.custom_slices.append(slice_data)
app._slice_sel_start = -1; app._slice_sel_end = -1
imgui.same_line()
if imgui.button("Clear Selection"): app._slice_sel_start = -1; app._slice_sel_end = -1
imgui.same_line()
if imgui.button("Auto-Populate"): app._populate_auto_slices(f_item)
to_remove = -1
tags = app.controller.project.get("context_tags", ["auto-ast", "bug", "feature", "important"])
for idx, slc in enumerate(f_item.custom_slices):
imgui.push_id(f"slc_row_{idx}"); imgui.text(f"#{idx+1}: L{slc['start_line']}-{slc['end_line']}"); imgui.same_line()
current_tag = slc.get('tag', '')
if current_tag not in tags and current_tag: tags.append(current_tag)
tag_idx = tags.index(current_tag) if current_tag in tags else 0
imgui.set_next_item_width(100)
ch_tag, new_tag_idx = imgui.combo("##Tag", tag_idx, tags)
if ch_tag: slc['tag'] = tags[new_tag_idx]
imgui.same_line(); imgui.set_next_item_width(-30); changed_comm, new_comm = imgui.input_text("##Note", slc.get('comment', ''))
if changed_comm: slc['comment'] = new_comm
imgui.same_line()
if imgui.button("X"): to_remove = idx
imgui.pop_id()
if to_remove != -1: f_item.custom_slices.pop(to_remove)
imgui.end_child()
imgui.table_next_column()
# --- RIGHT COLUMN: Content Preview with Highlights ---
with imscope.child("ast_content_scroll", imgui.ImVec2(0, 0), True):
if not getattr(app, '_cached_ast_file_lines', None):
imgui.text("No file content loaded.")
else:
draw_list = imgui.get_window_draw_list()
# We need vec4 locally
from src.imgui_scopes import vec4
for i, line_text in enumerate(app._cached_ast_file_lines):
line_num = i + 1
pos = imgui.get_cursor_screen_pos()
line_height = imgui.get_text_line_height()
avail_width = imgui.get_content_region_avail().x
# 1. AST Highlight
deepest_node = None
for node in app._cached_ast_nodes:
if node['start_line'] <= line_num <= node['end_line']:
if deepest_node is None or node['indent'] > deepest_node['indent']: deepest_node = node
mode = 'hide'
if deepest_node: mode = getattr(f_item, 'ast_mask', {}).get(deepest_node['full_path'], 'hide')
if mode == 'def':
draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.15)))
elif mode == 'sig':
draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.15)))
elif deepest_node and deepest_node['full_path'] == getattr(app, '_hovered_ast_node', None):
draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(vec4(255, 255, 0, 0.2)))
# 2. Slice Highlight
if hasattr(f_item, 'custom_slices'):
is_auto = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in f_item.custom_slices if slc.get('tag') == 'auto-ast')
is_man = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in f_item.custom_slices if slc.get('tag') != 'auto-ast')
if is_man: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(vec4(255, 165, 0, 0.2)))
elif is_auto and mode == 'hide': draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.1)))
# 3. Active Selection Highlight
if getattr(app, '_slice_sel_start', -1) != -1 and getattr(app, '_slice_sel_end', -1) != -1:
s, e = min(app._slice_sel_start, app._slice_sel_end), max(app._slice_sel_start, app._slice_sel_end)
if s <= line_num <= e: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + avail_width, pos.y + line_height), imgui.get_color_u32(vec4(100, 100, 255, 0.3)))
imgui.selectable(f"{line_num:4} | {line_text}##ln{line_num}", False)
if imgui.is_item_clicked(): app._slice_sel_start = line_num; app._slice_sel_end = line_num
if imgui.is_item_hovered(imgui.HoveredFlags_.allow_when_blocked_by_active_item) and imgui.is_mouse_down(0): app._slice_sel_end = line_num
imgui.end_table()
imgui.separator()
if imgui.button("Close", imgui.ImVec2(120, 0)):
app.ui_editing_slices_file = None
app.ui_inspecting_ast_file = None
imgui.close_current_popup()
imgui.end_popup()
if not opened:
app.ui_editing_slices_file = None
app.ui_inspecting_ast_file = None
+1 -1
View File
@@ -88,4 +88,4 @@ def test_render_discussion_panel_symbol_lookup(mock_app, role):
# 3. Verify the text viewer state is updated correctly
assert mock_app.text_viewer_title == "src/models.py"
assert mock_app.text_viewer_content == "class MyClass:\n pass"
assert mock_app.show_text_viewer is True
assert mock_app.show_windows.get("Text Viewer") is True
+15 -3
View File
@@ -16,12 +16,11 @@ def test_text_viewer_state_update(live_gui) -> None:
content = "This is test content for the viewer."
text_type = "markdown"
client.push_event("custom_callback", {"callback": "_set_attr", "args": ["show_text_viewer", True]})
client.push_event("custom_callback", {"callback": "_set_attr", "args": ["text_viewer_title", label]})
client.push_event("custom_callback", {"callback": "_set_attr", "args": ["text_viewer_content", content]})
client.push_event("custom_callback", {"callback": "_set_attr", "args": ["text_viewer_type", text_type]})
# Poll for state change (up to 5s)
# Wait for text_type to settle
state = None
start_time = time.time()
while time.time() - start_time < 5:
@@ -30,7 +29,20 @@ def test_text_viewer_state_update(live_gui) -> None:
break
time.sleep(0.1)
# Now get current show_windows, update it, and set it back
current_windows = state.get('show_windows', {})
current_windows["Text Viewer"] = True
client.push_event("custom_callback", {"callback": "_set_attr", "args": ["show_windows", current_windows]})
# Poll for show_text_viewer compat flag
start_time = time.time()
while time.time() - start_time < 5:
state = client.get_gui_state()
if state and state.get('show_text_viewer') == True:
break
time.sleep(0.1)
assert state is not None
assert state.get('show_text_viewer') == True
assert state.get('show_text_viewer') == True # API hook still provides this for compat
assert state.get('text_viewer_title') == label
assert state.get('text_viewer_type') == text_type
+2 -2
View File
@@ -37,10 +37,10 @@ def test_gui_ux_event_routing(live_gui) -> None:
'tier_usage': usage,
'tickets': []
})
time.sleep(1)
time.sleep(2)
status = client.get_mma_status()
assert status.get('mma_status') == 'simulating'
assert status.get('mma_status') == 'simulating', f"Expected 'simulating', got '{status.get('mma_status')}'"
assert status.get('tier_usage', {}).get('Tier 1', {}).get('input') == 10
print("[SIM] Global state update verified.")