Compare commits
3 Commits
6999aac197
...
2ce7a87069
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ce7a87069 | |||
| a7903d3a4b | |||
| 8e57ae1247 |
@@ -6,8 +6,8 @@ Architecture reference: [docs/guide_architecture.md](../../docs/guide_architectu
|
|||||||
Focus: Make all 4 tier output streams visible and indicate pending approvals.
|
Focus: Make all 4 tier output streams visible and indicate pending approvals.
|
||||||
|
|
||||||
- [x] Task 1.1: Replace the single Tier 1 strategy text box in `_render_mma_dashboard` (gui_2.py:2700-2701) with four collapsible sections — one per tier. Each section uses `imgui.collapsing_header(f"Tier {N}: {label}")` wrapping a `begin_child` scrollable region (200px height). Tier 1 = "Strategy", Tier 2 = "Tech Lead", Tier 3 = "Workers", Tier 4 = "QA". Tier 3 should aggregate all `mma_streams` keys containing "Tier 3" with ticket ID sub-headers. Each section auto-scrolls to bottom when new content arrives (track previous scroll position, scroll only if user was at bottom).
|
- [x] Task 1.1: Replace the single Tier 1 strategy text box in `_render_mma_dashboard` (gui_2.py:2700-2701) with four collapsible sections — one per tier. Each section uses `imgui.collapsing_header(f"Tier {N}: {label}")` wrapping a `begin_child` scrollable region (200px height). Tier 1 = "Strategy", Tier 2 = "Tech Lead", Tier 3 = "Workers", Tier 4 = "QA". Tier 3 should aggregate all `mma_streams` keys containing "Tier 3" with ticket ID sub-headers. Each section auto-scrolls to bottom when new content arrives (track previous scroll position, scroll only if user was at bottom).
|
||||||
- [ ] Task 1.2: Add approval state indicators to the MMA dashboard. After the "Status:" line in `_render_mma_dashboard` (gui_2.py:2672-2676), check `self._pending_mma_spawn`, `self._pending_mma_approval`, and `self._pending_ask_dialog`. When any is active, render a colored blinking badge: `imgui.text_colored(ImVec4(1,0.3,0.3,1), "APPROVAL PENDING")` using `sin(time.time()*5)` for alpha pulse. Also add a `imgui.same_line()` button "Go to Approval" that scrolls/focuses the relevant dialog.
|
- [x] Task 1.2: Add approval state indicators to the MMA dashboard. After the "Status:" line in `_render_mma_dashboard` (gui_2.py:2672-2676), check `self._pending_mma_spawn`, `self._pending_mma_approval`, and `self._pending_ask_dialog`. When any is active, render a colored blinking badge: `imgui.text_colored(ImVec4(1,0.3,0.3,1), "APPROVAL PENDING")` using `sin(time.time()*5)` for alpha pulse. Also add a `imgui.same_line()` button "Go to Approval" that scrolls/focuses the relevant dialog.
|
||||||
- [ ] Task 1.3: Write unit tests verifying: (a) `mma_streams` with keys "Tier 1", "Tier 2 (Tech Lead)", "Tier 3: T-001", "Tier 4 (QA)" are all rendered (check by mocking `imgui.collapsing_header` calls); (b) approval indicators appear when `_pending_mma_spawn is not None`.
|
- [x] Task 1.3: Write unit tests verifying: (a) `mma_streams` with keys "Tier 1", "Tier 2 (Tech Lead)", "Tier 3: T-001", "Tier 4 (QA)" are all rendered (check by mocking `imgui.collapsing_header` calls); (b) approval indicators appear when `_pending_mma_spawn is not None`.
|
||||||
- [ ] Task 1.4: Conductor - User Manual Verification 'Phase 1: Tier Stream Panels & Approval Indicators' (Protocol in workflow.md)
|
- [ ] Task 1.4: Conductor - User Manual Verification 'Phase 1: Tier Stream Panels & Approval Indicators' (Protocol in workflow.md)
|
||||||
|
|
||||||
## Phase 2: Cost Tracking & Enhanced Token Table
|
## Phase 2: Cost Tracking & Enhanced Token Table
|
||||||
|
|||||||
121
gui_2.py
121
gui_2.py
@@ -208,6 +208,10 @@ class App:
|
|||||||
"Files & Media": True,
|
"Files & Media": True,
|
||||||
"AI Settings": True,
|
"AI Settings": True,
|
||||||
"MMA Dashboard": True,
|
"MMA Dashboard": True,
|
||||||
|
"Tier 1: Strategy": True,
|
||||||
|
"Tier 2: Tech Lead": True,
|
||||||
|
"Tier 3: Workers": True,
|
||||||
|
"Tier 4: QA": True,
|
||||||
"Discussion Hub": True,
|
"Discussion Hub": True,
|
||||||
"Operations Hub": True,
|
"Operations Hub": True,
|
||||||
"Theme": True,
|
"Theme": True,
|
||||||
@@ -1592,6 +1596,26 @@ class App:
|
|||||||
if exp:
|
if exp:
|
||||||
self._render_mma_dashboard()
|
self._render_mma_dashboard()
|
||||||
imgui.end()
|
imgui.end()
|
||||||
|
if self.show_windows.get("Tier 1: Strategy", False):
|
||||||
|
exp, self.show_windows["Tier 1: Strategy"] = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"])
|
||||||
|
if exp:
|
||||||
|
self._render_tier_stream_panel("Tier 1", "Tier 1")
|
||||||
|
imgui.end()
|
||||||
|
if self.show_windows.get("Tier 2: Tech Lead", False):
|
||||||
|
exp, self.show_windows["Tier 2: Tech Lead"] = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"])
|
||||||
|
if exp:
|
||||||
|
self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)")
|
||||||
|
imgui.end()
|
||||||
|
if self.show_windows.get("Tier 3: Workers", False):
|
||||||
|
exp, self.show_windows["Tier 3: Workers"] = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"])
|
||||||
|
if exp:
|
||||||
|
self._render_tier_stream_panel("Tier 3", None)
|
||||||
|
imgui.end()
|
||||||
|
if self.show_windows.get("Tier 4: QA", False):
|
||||||
|
exp, self.show_windows["Tier 4: QA"] = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"])
|
||||||
|
if exp:
|
||||||
|
self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)")
|
||||||
|
imgui.end()
|
||||||
if self.show_windows.get("Theme", False):
|
if self.show_windows.get("Theme", False):
|
||||||
self._render_theme_panel()
|
self._render_theme_panel()
|
||||||
if self.show_windows.get("Discussion Hub", False):
|
if self.show_windows.get("Discussion Hub", False):
|
||||||
@@ -2685,6 +2709,19 @@ class App:
|
|||||||
if self.active_tier:
|
if self.active_tier:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(C_VAL, f"| Active: {self.active_tier}")
|
imgui.text_colored(C_VAL, f"| Active: {self.active_tier}")
|
||||||
|
# Approval pending indicator
|
||||||
|
any_pending = (
|
||||||
|
self._pending_mma_spawn is not None or
|
||||||
|
self._pending_mma_approval is not None or
|
||||||
|
self._pending_ask_dialog
|
||||||
|
)
|
||||||
|
if any_pending:
|
||||||
|
alpha = abs(math.sin(time.time() * 5))
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_colored(imgui.ImVec4(1.0, 0.3, 0.3, alpha), " APPROVAL PENDING")
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Go to Approval"):
|
||||||
|
pass # scroll/focus handled by existing dialog rendering
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
# 2. Active Track Info
|
# 2. Active Track Info
|
||||||
if self.active_track:
|
if self.active_track:
|
||||||
@@ -2717,60 +2754,6 @@ class App:
|
|||||||
imgui.text(f"{stats.get('output', 0):,}")
|
imgui.text(f"{stats.get('output', 0):,}")
|
||||||
imgui.end_table()
|
imgui.end_table()
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
if imgui.collapsing_header("Tier 1: Strategy"):
|
|
||||||
content = self.mma_streams.get("Tier 1", "")
|
|
||||||
if imgui.begin_child("##tier1_scroll", imgui.ImVec2(-1, 200), True):
|
|
||||||
imgui.text_wrapped(content)
|
|
||||||
try:
|
|
||||||
at_bottom = imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10
|
|
||||||
if at_bottom and len(content) != self._tier_stream_last_len.get("Tier 1", -1):
|
|
||||||
imgui.set_scroll_here_y(1.0)
|
|
||||||
self._tier_stream_last_len["Tier 1"] = len(content)
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
pass
|
|
||||||
imgui.end_child()
|
|
||||||
if imgui.collapsing_header("Tier 2: Tech Lead"):
|
|
||||||
content = self.mma_streams.get("Tier 2 (Tech Lead)", "")
|
|
||||||
if imgui.begin_child("##tier2_scroll", imgui.ImVec2(-1, 200), True):
|
|
||||||
imgui.text_wrapped(content)
|
|
||||||
try:
|
|
||||||
at_bottom = imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10
|
|
||||||
if at_bottom and len(content) != self._tier_stream_last_len.get("Tier 2 (Tech Lead)", -1):
|
|
||||||
imgui.set_scroll_here_y(1.0)
|
|
||||||
self._tier_stream_last_len["Tier 2 (Tech Lead)"] = len(content)
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
pass
|
|
||||||
imgui.end_child()
|
|
||||||
if imgui.collapsing_header("Tier 3: Workers"):
|
|
||||||
tier3_keys = [k for k in self.mma_streams if "Tier 3" in k]
|
|
||||||
if not tier3_keys:
|
|
||||||
imgui.text_disabled("No worker output yet.")
|
|
||||||
else:
|
|
||||||
for key in tier3_keys:
|
|
||||||
ticket_id = key.split(": ", 1)[-1] if ": " in key else key
|
|
||||||
imgui.text(ticket_id)
|
|
||||||
if imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True):
|
|
||||||
imgui.text_wrapped(self.mma_streams[key])
|
|
||||||
try:
|
|
||||||
at_bottom = imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10
|
|
||||||
if at_bottom and len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1):
|
|
||||||
imgui.set_scroll_here_y(1.0)
|
|
||||||
self._tier_stream_last_len[key] = len(self.mma_streams[key])
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
pass
|
|
||||||
imgui.end_child()
|
|
||||||
if imgui.collapsing_header("Tier 4: QA"):
|
|
||||||
content = self.mma_streams.get("Tier 4 (QA)", "")
|
|
||||||
if imgui.begin_child("##tier4_scroll", imgui.ImVec2(-1, 200), True):
|
|
||||||
imgui.text_wrapped(content)
|
|
||||||
try:
|
|
||||||
at_bottom = imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10
|
|
||||||
if at_bottom and len(content) != self._tier_stream_last_len.get("Tier 4 (QA)", -1):
|
|
||||||
imgui.set_scroll_here_y(1.0)
|
|
||||||
self._tier_stream_last_len["Tier 4 (QA)"] = len(content)
|
|
||||||
except (TypeError, AttributeError):
|
|
||||||
pass
|
|
||||||
imgui.end_child()
|
|
||||||
# 4. Task DAG Visualizer
|
# 4. Task DAG Visualizer
|
||||||
imgui.text("Task DAG")
|
imgui.text("Task DAG")
|
||||||
if self.active_track:
|
if self.active_track:
|
||||||
@@ -2795,6 +2778,36 @@ class App:
|
|||||||
else:
|
else:
|
||||||
imgui.text_disabled("No active MMA track.")
|
imgui.text_disabled("No active MMA track.")
|
||||||
|
|
||||||
|
def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None:
|
||||||
|
if stream_key is not None:
|
||||||
|
content = self.mma_streams.get(stream_key, "")
|
||||||
|
if imgui.begin_child("##stream_content", imgui.ImVec2(-1, -1)):
|
||||||
|
imgui.text_wrapped(content)
|
||||||
|
try:
|
||||||
|
if len(content) != self._tier_stream_last_len.get(stream_key, -1):
|
||||||
|
imgui.set_scroll_here_y(1.0)
|
||||||
|
self._tier_stream_last_len[stream_key] = len(content)
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
pass
|
||||||
|
imgui.end_child()
|
||||||
|
else:
|
||||||
|
tier3_keys = [k for k in self.mma_streams if "Tier 3" in k]
|
||||||
|
if not tier3_keys:
|
||||||
|
imgui.text_disabled("No worker output yet.")
|
||||||
|
else:
|
||||||
|
for key in tier3_keys:
|
||||||
|
ticket_id = key.split(": ", 1)[-1] if ": " in key else key
|
||||||
|
imgui.text(ticket_id)
|
||||||
|
if imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True):
|
||||||
|
imgui.text_wrapped(self.mma_streams[key])
|
||||||
|
try:
|
||||||
|
if len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1):
|
||||||
|
imgui.set_scroll_here_y(1.0)
|
||||||
|
self._tier_stream_last_len[key] = len(self.mma_streams[key])
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
pass
|
||||||
|
imgui.end_child()
|
||||||
|
|
||||||
def _render_ticket_dag_node(self, ticket: Ticket, tickets_by_id: dict[str, Ticket], children_map: dict[str, list[str]], rendered: set[str]) -> None:
|
def _render_ticket_dag_node(self, ticket: Ticket, tickets_by_id: dict[str, Ticket], children_map: dict[str, list[str]], rendered: set[str]) -> None:
|
||||||
tid = ticket.get('id', '??')
|
tid = ticket.get('id', '??')
|
||||||
target = ticket.get('target_file', 'general')
|
target = ticket.get('target_file', 'general')
|
||||||
|
|||||||
51
scripts/tasks/gui_ux_1_1_panels_impl.toml
Normal file
51
scripts/tasks/gui_ux_1_1_panels_impl.toml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
role = "tier3-worker"
|
||||||
|
docs = ["conductor/workflow.md"]
|
||||||
|
prompt = """
|
||||||
|
Refactor the tier stream display in @gui_2.py: replace the 4 collapsing_header sections inside _render_mma_dashboard with 4 separate dockable imgui.begin() windows.
|
||||||
|
|
||||||
|
CHANGES NEEDED:
|
||||||
|
|
||||||
|
1. In __init__ (around line 207), in the _default_windows dict, add 4 new entries after "MMA Dashboard":
|
||||||
|
"Tier 1: Strategy": True,
|
||||||
|
"Tier 2: Tech Lead": True,
|
||||||
|
"Tier 3: Workers": True,
|
||||||
|
"Tier 4: QA": True,
|
||||||
|
|
||||||
|
2. In _render_mma_dashboard (find the 4 collapsing_header blocks added for Tier 1/2/3/4 and the _tier_stream_last_len auto-scroll logic), REMOVE all of it. The method should no longer render any tier stream content.
|
||||||
|
|
||||||
|
3. Add a new method _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) to App class (place it after _render_mma_dashboard). It renders content for one tier:
|
||||||
|
- If stream_key is not None (Tier 1, 2, 4): show self.mma_streams.get(stream_key, "") via imgui.text_wrapped inside a begin_child("##stream_content", ImVec2(-1, -1))
|
||||||
|
- If stream_key is None (Tier 3): aggregate all keys in self.mma_streams containing "Tier 3", show each with a ticket sub-header (imgui.text(ticket_id) then begin_child per ticket)
|
||||||
|
- Auto-scroll: if content length changed since last frame (tracked in self._tier_stream_last_len dict), call imgui.set_scroll_here_y(1.0)
|
||||||
|
|
||||||
|
4. In _gui_func, after the "MMA Dashboard" window block (around line 1592), add 4 new window blocks following the exact same pattern as other windows:
|
||||||
|
if self.show_windows.get("Tier 1: Strategy", False):
|
||||||
|
exp, self.show_windows["Tier 1: Strategy"] = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"])
|
||||||
|
if exp:
|
||||||
|
self._render_tier_stream_panel("Tier 1", "Tier 1")
|
||||||
|
imgui.end()
|
||||||
|
if self.show_windows.get("Tier 2: Tech Lead", False):
|
||||||
|
exp, self.show_windows["Tier 2: Tech Lead"] = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"])
|
||||||
|
if exp:
|
||||||
|
self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)")
|
||||||
|
imgui.end()
|
||||||
|
if self.show_windows.get("Tier 3: Workers", False):
|
||||||
|
exp, self.show_windows["Tier 3: Workers"] = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"])
|
||||||
|
if exp:
|
||||||
|
self._render_tier_stream_panel("Tier 3", None)
|
||||||
|
imgui.end()
|
||||||
|
if self.show_windows.get("Tier 4: QA", False):
|
||||||
|
exp, self.show_windows["Tier 4: QA"] = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"])
|
||||||
|
if exp:
|
||||||
|
self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)")
|
||||||
|
imgui.end()
|
||||||
|
|
||||||
|
5. Update @tests/test_mma_dashboard_streams.py — replace the 3 existing tests with new ones that test _render_tier_stream_panel instead:
|
||||||
|
- test_tier1_renders_stream_content: call App._render_tier_stream_panel(app, "Tier 1", "Tier 1") with mma_streams={"Tier 1": "hello"}, assert imgui.text_wrapped called with "hello"
|
||||||
|
- test_tier3_renders_worker_subheaders: call App._render_tier_stream_panel(app, "Tier 3", None) with mma_streams={"Tier 3 (Worker): T-001": "out1", "Tier 3 (Worker): T-002": "out2"}, assert imgui.text called with args containing "T-001" and "T-002"
|
||||||
|
- test_mma_dashboard_no_longer_has_strategy_box: call App._render_mma_dashboard(app), assert imgui.collapsing_header NOT called with any string containing "Tier"
|
||||||
|
|
||||||
|
Also confirm @tests/test_mma_approval_indicators.py still passes unchanged.
|
||||||
|
|
||||||
|
Use exactly 1-space indentation for Python code.
|
||||||
|
"""
|
||||||
34
scripts/tasks/gui_ux_1_2_impl.toml
Normal file
34
scripts/tasks/gui_ux_1_2_impl.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
role = "tier3-worker"
|
||||||
|
docs = ["conductor/workflow.md"]
|
||||||
|
prompt = """
|
||||||
|
Implement Task 1.2 in @gui_2.py: add a blinking "APPROVAL PENDING" badge in _render_mma_dashboard.
|
||||||
|
|
||||||
|
LOCATION: In _render_mma_dashboard, after the block that renders the Status line and active_tier text (around line 2686-2690 after this existing code):
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text(f"Status: {self.mma_status.upper()}")
|
||||||
|
if self.active_tier:
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_colored(C_VAL, f"| Active: {self.active_tier}")
|
||||||
|
|
||||||
|
ADD immediately after (before the next imgui.separator()):
|
||||||
|
# Approval pending indicator
|
||||||
|
any_pending = (
|
||||||
|
self._pending_mma_spawn is not None or
|
||||||
|
self._pending_mma_approval is not None or
|
||||||
|
self._pending_ask_dialog
|
||||||
|
)
|
||||||
|
if any_pending:
|
||||||
|
alpha = abs(math.sin(time.time() * 5))
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.text_colored(imgui.ImVec4(1.0, 0.3, 0.3, alpha), " APPROVAL PENDING")
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Go to Approval"):
|
||||||
|
pass # scroll/focus handled by existing dialog rendering
|
||||||
|
|
||||||
|
Also ensure `import math` is present at the top of gui_2.py (check if it's already imported — if not, add it alongside the other stdlib imports).
|
||||||
|
|
||||||
|
TESTS that must pass: @tests/test_mma_approval_indicators.py
|
||||||
|
Also confirm @tests/test_mma_dashboard_streams.py still passes.
|
||||||
|
|
||||||
|
Use exactly 1-space indentation for Python code.
|
||||||
|
"""
|
||||||
44
scripts/tasks/gui_ux_1_2_tests.toml
Normal file
44
scripts/tasks/gui_ux_1_2_tests.toml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
role = "tier3-worker"
|
||||||
|
docs = ["conductor/workflow.md"]
|
||||||
|
prompt = """
|
||||||
|
Create tests/test_mma_approval_indicators.py — failing tests for Task 1.2 of comprehensive_gui_ux track.
|
||||||
|
|
||||||
|
CONTEXT: Task 1.2 adds a blinking "APPROVAL PENDING" badge after the Status line in _render_mma_dashboard in @gui_2.py. It checks self._pending_mma_spawn, self._pending_mma_approval, and self._pending_ask_dialog.
|
||||||
|
|
||||||
|
TESTS TO WRITE (all should FAIL against current code — test new behaviour):
|
||||||
|
|
||||||
|
Use the same _make_app / _make_imgui_mock pattern as @tests/test_mma_dashboard_streams.py but add these attributes:
|
||||||
|
app._pending_mma_spawn = None
|
||||||
|
app._pending_mma_approval = None
|
||||||
|
app._pending_ask_dialog = False
|
||||||
|
|
||||||
|
1. test_no_approval_badge_when_idle:
|
||||||
|
- All three pending attrs are None/False
|
||||||
|
- Patch gui_2.imgui, call App._render_mma_dashboard(app)
|
||||||
|
- Assert imgui.text_colored was NOT called with any string containing "APPROVAL PENDING"
|
||||||
|
|
||||||
|
2. test_approval_badge_shown_when_spawn_pending:
|
||||||
|
- app._pending_mma_spawn = {"ticket_id": "T-001"}
|
||||||
|
- Patch gui_2.imgui, patch 'gui_2.math' (so math.sin returns 0.8)
|
||||||
|
- Call App._render_mma_dashboard(app)
|
||||||
|
- Collect all text_colored call args as a single joined string
|
||||||
|
- Assert "APPROVAL PENDING" appears in that string
|
||||||
|
|
||||||
|
3. test_approval_badge_shown_when_mma_approval_pending:
|
||||||
|
- app._pending_mma_approval = {"step": "test"}
|
||||||
|
- Same patch pattern
|
||||||
|
- Assert "APPROVAL PENDING" in text_colored calls
|
||||||
|
|
||||||
|
4. test_approval_badge_shown_when_ask_dialog_pending:
|
||||||
|
- app._pending_ask_dialog = True
|
||||||
|
- Same patch pattern
|
||||||
|
- Assert "APPROVAL PENDING" in text_colored calls
|
||||||
|
|
||||||
|
IMPLEMENTATION DETAILS:
|
||||||
|
- Import: import pytest, math, from unittest.mock import patch, MagicMock, call
|
||||||
|
- Use the _make_app helper from test_mma_dashboard_streams (copy it or import it — prefer copying to keep tests self-contained)
|
||||||
|
- For _make_imgui_mock: same as test_mma_dashboard_streams, also set imgui_mock.ImVec4.return_value = MagicMock()
|
||||||
|
- Patch 'gui_2.math' where needed: with patch('gui_2.math') as mock_math: mock_math.sin.return_value = 0.8
|
||||||
|
- Use 1-space indentation throughout.
|
||||||
|
- All 4 tests should FAIL against current gui_2.py (which has no approval badge logic yet).
|
||||||
|
"""
|
||||||
102
tests/test_mma_approval_indicators.py
Normal file
102
tests/test_mma_approval_indicators.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import math
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch, MagicMock, call
|
||||||
|
|
||||||
|
from gui_2 import App
|
||||||
|
|
||||||
|
|
||||||
|
def _make_app(**kwargs):
|
||||||
|
app = MagicMock(spec=App)
|
||||||
|
app.mma_streams = kwargs.get("mma_streams", {})
|
||||||
|
app.mma_tier_usage = kwargs.get("mma_tier_usage", {
|
||||||
|
"Tier 1": {"input": 0, "output": 0},
|
||||||
|
"Tier 2": {"input": 0, "output": 0},
|
||||||
|
"Tier 3": {"input": 0, "output": 0},
|
||||||
|
"Tier 4": {"input": 0, "output": 0},
|
||||||
|
})
|
||||||
|
app.tracks = kwargs.get("tracks", [])
|
||||||
|
app.active_track = kwargs.get("active_track", None)
|
||||||
|
app.active_tickets = kwargs.get("active_tickets", [])
|
||||||
|
app.mma_status = kwargs.get("mma_status", "idle")
|
||||||
|
app.active_tier = kwargs.get("active_tier", None)
|
||||||
|
app.mma_step_mode = kwargs.get("mma_step_mode", False)
|
||||||
|
app._pending_mma_spawn = kwargs.get("_pending_mma_spawn", None)
|
||||||
|
app._pending_mma_approval = kwargs.get("_pending_mma_approval", None)
|
||||||
|
app._pending_ask_dialog = kwargs.get("_pending_ask_dialog", False)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def _make_imgui_mock():
|
||||||
|
m = MagicMock()
|
||||||
|
m.begin_table.return_value = False
|
||||||
|
m.begin_child.return_value = False
|
||||||
|
m.checkbox.return_value = (False, False)
|
||||||
|
m.collapsing_header.return_value = False
|
||||||
|
m.ImVec2.return_value = MagicMock()
|
||||||
|
m.ImVec4.return_value = MagicMock()
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_text_colored_args(imgui_mock):
|
||||||
|
"""Return a single joined string of all text_colored second-arg strings."""
|
||||||
|
parts = []
|
||||||
|
for c in imgui_mock.text_colored.call_args_list:
|
||||||
|
args = c.args
|
||||||
|
if len(args) >= 2:
|
||||||
|
parts.append(str(args[1]))
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMMAApprovalIndicators:
|
||||||
|
|
||||||
|
def test_no_approval_badge_when_idle(self):
|
||||||
|
"""No 'APPROVAL PENDING' badge when all pending attrs are None/False."""
|
||||||
|
app = _make_app(
|
||||||
|
_pending_mma_spawn=None,
|
||||||
|
_pending_mma_approval=None,
|
||||||
|
_pending_ask_dialog=False,
|
||||||
|
)
|
||||||
|
imgui_mock = _make_imgui_mock()
|
||||||
|
with patch("gui_2.imgui", imgui_mock):
|
||||||
|
App._render_mma_dashboard(app)
|
||||||
|
combined = _collect_text_colored_args(imgui_mock)
|
||||||
|
assert "APPROVAL PENDING" not in combined, (
|
||||||
|
"text_colored called with 'APPROVAL PENDING' when no approval is pending"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_approval_badge_shown_when_spawn_pending(self):
|
||||||
|
"""'APPROVAL PENDING' badge must appear when _pending_mma_spawn is set."""
|
||||||
|
app = _make_app(_pending_mma_spawn={"ticket_id": "T-001"})
|
||||||
|
imgui_mock = _make_imgui_mock()
|
||||||
|
with patch("gui_2.imgui", imgui_mock), patch("gui_2.math") as mock_math:
|
||||||
|
mock_math.sin.return_value = 0.8
|
||||||
|
App._render_mma_dashboard(app)
|
||||||
|
combined = _collect_text_colored_args(imgui_mock)
|
||||||
|
assert "APPROVAL PENDING" in combined, (
|
||||||
|
"text_colored not called with 'APPROVAL PENDING' when _pending_mma_spawn is set"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_approval_badge_shown_when_mma_approval_pending(self):
|
||||||
|
"""'APPROVAL PENDING' badge must appear when _pending_mma_approval is set."""
|
||||||
|
app = _make_app(_pending_mma_approval={"step": "test"})
|
||||||
|
imgui_mock = _make_imgui_mock()
|
||||||
|
with patch("gui_2.imgui", imgui_mock), patch("gui_2.math") as mock_math:
|
||||||
|
mock_math.sin.return_value = 0.8
|
||||||
|
App._render_mma_dashboard(app)
|
||||||
|
combined = _collect_text_colored_args(imgui_mock)
|
||||||
|
assert "APPROVAL PENDING" in combined, (
|
||||||
|
"text_colored not called with 'APPROVAL PENDING' when _pending_mma_approval is set"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_approval_badge_shown_when_ask_dialog_pending(self):
|
||||||
|
"""'APPROVAL PENDING' badge must appear when _pending_ask_dialog is True."""
|
||||||
|
app = _make_app(_pending_ask_dialog=True)
|
||||||
|
imgui_mock = _make_imgui_mock()
|
||||||
|
with patch("gui_2.imgui", imgui_mock), patch("gui_2.math") as mock_math:
|
||||||
|
mock_math.sin.return_value = 0.8
|
||||||
|
App._render_mma_dashboard(app)
|
||||||
|
combined = _collect_text_colored_args(imgui_mock)
|
||||||
|
assert "APPROVAL PENDING" in combined, (
|
||||||
|
"text_colored not called with 'APPROVAL PENDING' when _pending_ask_dialog is True"
|
||||||
|
)
|
||||||
@@ -20,6 +20,10 @@ def _make_app(**kwargs):
|
|||||||
app.mma_status = kwargs.get("mma_status", "idle")
|
app.mma_status = kwargs.get("mma_status", "idle")
|
||||||
app.active_tier = kwargs.get("active_tier", None)
|
app.active_tier = kwargs.get("active_tier", None)
|
||||||
app.mma_step_mode = kwargs.get("mma_step_mode", False)
|
app.mma_step_mode = kwargs.get("mma_step_mode", False)
|
||||||
|
app._pending_mma_spawn = kwargs.get("_pending_mma_spawn", None)
|
||||||
|
app._pending_mma_approval = kwargs.get("_pending_mma_approval", None)
|
||||||
|
app._pending_ask_dialog = kwargs.get("_pending_ask_dialog", False)
|
||||||
|
app._tier_stream_last_len = {}
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
@@ -35,46 +39,38 @@ def _make_imgui_mock():
|
|||||||
|
|
||||||
class TestMMADashboardStreams:
|
class TestMMADashboardStreams:
|
||||||
|
|
||||||
def test_all_four_tier_headers_rendered(self):
|
def test_tier1_renders_stream_content(self):
|
||||||
"""collapsing_header must be called for all four tiers (new behaviour)."""
|
"""_render_tier_stream_panel for Tier 1 must call text_wrapped with the stream content."""
|
||||||
app = _make_app(mma_streams={
|
app = _make_app(mma_streams={"Tier 1": "hello"})
|
||||||
"Tier 1": "strat",
|
|
||||||
"Tier 2 (Tech Lead)": "lead",
|
|
||||||
"Tier 3 (Worker): T-001": "work",
|
|
||||||
"Tier 4 (QA)": "qa",
|
|
||||||
})
|
|
||||||
imgui_mock = _make_imgui_mock()
|
imgui_mock = _make_imgui_mock()
|
||||||
with patch("gui_2.imgui", imgui_mock):
|
|
||||||
App._render_mma_dashboard(app)
|
|
||||||
header_args = " ".join(str(c) for c in imgui_mock.collapsing_header.call_args_list)
|
|
||||||
assert "Tier 1" in header_args, "collapsing_header not called with 'Tier 1'"
|
|
||||||
assert "Tier 2" in header_args, "collapsing_header not called with 'Tier 2'"
|
|
||||||
assert "Tier 3" in header_args, "collapsing_header not called with 'Tier 3'"
|
|
||||||
assert "Tier 4" in header_args, "collapsing_header not called with 'Tier 4'"
|
|
||||||
|
|
||||||
def test_tier3_aggregates_multiple_worker_streams(self):
|
|
||||||
"""Tier 3 section must render a sub-header for each worker stream key."""
|
|
||||||
app = _make_app(mma_streams={
|
|
||||||
"Tier 3 (Worker): T-001": "output1",
|
|
||||||
"Tier 3 (Worker): T-002": "output2",
|
|
||||||
})
|
|
||||||
imgui_mock = _make_imgui_mock()
|
|
||||||
imgui_mock.collapsing_header.return_value = True
|
|
||||||
imgui_mock.begin_child.return_value = True
|
imgui_mock.begin_child.return_value = True
|
||||||
with patch("gui_2.imgui", imgui_mock):
|
with patch("gui_2.imgui", imgui_mock):
|
||||||
App._render_mma_dashboard(app)
|
App._render_tier_stream_panel(app, "Tier 1", "Tier 1")
|
||||||
|
text_wrapped_args = " ".join(str(c) for c in imgui_mock.text_wrapped.call_args_list)
|
||||||
|
assert "hello" in text_wrapped_args, "text_wrapped not called with stream content 'hello'"
|
||||||
|
|
||||||
|
def test_tier3_renders_worker_subheaders(self):
|
||||||
|
"""_render_tier_stream_panel for Tier 3 must render a sub-header for each worker stream key."""
|
||||||
|
app = _make_app(mma_streams={
|
||||||
|
"Tier 3 (Worker): T-001": "out1",
|
||||||
|
"Tier 3 (Worker): T-002": "out2",
|
||||||
|
})
|
||||||
|
imgui_mock = _make_imgui_mock()
|
||||||
|
imgui_mock.begin_child.return_value = True
|
||||||
|
with patch("gui_2.imgui", imgui_mock):
|
||||||
|
App._render_tier_stream_panel(app, "Tier 3", None)
|
||||||
text_args = " ".join(str(c) for c in imgui_mock.text.call_args_list)
|
text_args = " ".join(str(c) for c in imgui_mock.text.call_args_list)
|
||||||
assert "T-001" in text_args, "imgui.text not called with 'T-001' worker sub-header"
|
assert "T-001" in text_args, "imgui.text not called with 'T-001' worker sub-header"
|
||||||
assert "T-002" in text_args, "imgui.text not called with 'T-002' worker sub-header"
|
assert "T-002" in text_args, "imgui.text not called with 'T-002' worker sub-header"
|
||||||
|
|
||||||
def test_old_single_strategy_box_removed(self):
|
def test_mma_dashboard_no_longer_has_strategy_box(self):
|
||||||
"""input_text_multiline must NOT be called with '##mma_strategy' in the new design."""
|
"""_render_mma_dashboard must NOT call collapsing_header with any 'Tier' string."""
|
||||||
app = _make_app(mma_streams={"Tier 1": "strategy text"})
|
app = _make_app(mma_streams={"Tier 1": "strategy text"})
|
||||||
imgui_mock = _make_imgui_mock()
|
imgui_mock = _make_imgui_mock()
|
||||||
with patch("gui_2.imgui", imgui_mock):
|
with patch("gui_2.imgui", imgui_mock):
|
||||||
App._render_mma_dashboard(app)
|
App._render_mma_dashboard(app)
|
||||||
for c in imgui_mock.input_text_multiline.call_args_list:
|
for c in imgui_mock.collapsing_header.call_args_list:
|
||||||
first_arg = c.args[0] if c.args else None
|
first_arg = c.args[0] if c.args else ""
|
||||||
assert first_arg != "##mma_strategy", (
|
assert "Tier" not in str(first_arg), (
|
||||||
"input_text_multiline called with '##mma_strategy' — old single strategy box must be removed"
|
f"collapsing_header called with 'Tier' string — tier panels must be separate windows now"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user