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:
+1
-1
@@ -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/)*
|
||||
|
||||
---
|
||||
|
||||
@@ -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
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user