Compare commits
5 Commits
e9d9cdeb28
...
beae82860a
| Author | SHA1 | Date | |
|---|---|---|---|
| beae82860a | |||
| 3f83063197 | |||
| a22603d136 | |||
| c56c8db6db | |||
| 035c74ed36 |
@@ -58,3 +58,4 @@ For deep implementation details when planning or implementing tracks, consult `d
|
|||||||
- **Gemini CLI Integration:** Allows using the `gemini` CLI as a headless backend provider. This enables leveraging Gemini subscriptions with advanced features like persistent sessions, while maintaining full "Human-in-the-Loop" safety through a dedicated bridge for synchronous tool call approvals within the Manual Slop GUI. Now features full functional parity with the direct API, including accurate token estimation, safety settings, and robust system instruction handling.
|
- **Gemini CLI Integration:** Allows using the `gemini` CLI as a headless backend provider. This enables leveraging Gemini subscriptions with advanced features like persistent sessions, while maintaining full "Human-in-the-Loop" safety through a dedicated bridge for synchronous tool call approvals within the Manual Slop GUI. Now features full functional parity with the direct API, including accurate token estimation, safety settings, and robust system instruction handling.
|
||||||
- **Context & Token Visualization:** Detailed UI panels for monitoring real-time token usage, history depth, and **visual cache awareness** (tracking specific files currently live in the provider's context cache).
|
- **Context & Token Visualization:** Detailed UI panels for monitoring real-time token usage, history depth, and **visual cache awareness** (tracking specific files currently live in the provider's context cache).
|
||||||
- **On-Demand Definition Lookup:** Allows developers to request specific class or function definitions during discussions using `@SymbolName` syntax. Injected definitions feature syntax highlighting, intelligent collapsing for long blocks, and a **[Source]** button for instant navigation to the full file.
|
- **On-Demand Definition Lookup:** Allows developers to request specific class or function definitions during discussions using `@SymbolName` syntax. Injected definitions feature syntax highlighting, intelligent collapsing for long blocks, and a **[Source]** button for instant navigation to the full file.
|
||||||
|
- **Manual Ticket Queue Management:** Provides a dedicated GUI panel for granular control over the implementation queue. Features include color-coded priority assignment (High, Medium, Low), multi-select bulk operations (Execute, Skip, Block), and interactive drag-and-drop reordering with real-time Directed Acyclic Graph (DAG) validation.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
|||||||
|
|
||||||
### Manual UX Controls
|
### Manual UX Controls
|
||||||
|
|
||||||
14. [ ] **Track: Manual Ticket Queue Management**
|
14. [~] **Track: Manual Ticket Queue Management**
|
||||||
*Link: [./tracks/ticket_queue_mgmt_20260306/](./tracks/ticket_queue_mgmt_20260306/)*
|
*Link: [./tracks/ticket_queue_mgmt_20260306/](./tracks/ticket_queue_mgmt_20260306/)*
|
||||||
|
|
||||||
15. [ ] **Track: Kill/Abort Running Workers**
|
15. [ ] **Track: Kill/Abort Running Workers**
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
## Phase 1: Priority Field
|
## Phase 1: Priority Field
|
||||||
Focus: Add priority to Ticket model
|
Focus: Add priority to Ticket model
|
||||||
|
|
||||||
- [ ] Task 1.1: Initialize MMA Environment
|
- [x] Task 1.1: Initialize MMA Environment
|
||||||
- Run `activate_skill mma-orchestrator` before starting
|
- Run `activate_skill mma-orchestrator` before starting
|
||||||
|
|
||||||
- [ ] Task 1.2: Add priority field to Ticket
|
- [x] Task 1.2: Add priority field to Ticket (035c74e)
|
||||||
- WHERE: `src/models.py` `Ticket` dataclass
|
- WHERE: `src/models.py` `Ticket` dataclass
|
||||||
- WHAT: Add `priority: str = "medium"` field
|
- WHAT: Add `priority: str = "medium"` field
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -20,7 +20,7 @@ Focus: Add priority to Ticket model
|
|||||||
```
|
```
|
||||||
- CODE STYLE: 1-space indentation
|
- CODE STYLE: 1-space indentation
|
||||||
|
|
||||||
- [ ] Task 1.3: Update Ticket serialization
|
- [x] Task 1.3: Update Ticket serialization (035c74e)
|
||||||
- WHERE: `src/models.py` `Ticket.to_dict()` and `from_dict()`
|
- WHERE: `src/models.py` `Ticket.to_dict()` and `from_dict()`
|
||||||
- WHAT: Include priority in serialization
|
- WHAT: Include priority in serialization
|
||||||
- HOW: Add `priority` to dict conversion
|
- HOW: Add `priority` to dict conversion
|
||||||
@@ -28,7 +28,7 @@ Focus: Add priority to Ticket model
|
|||||||
## Phase 2: Priority UI
|
## Phase 2: Priority UI
|
||||||
Focus: Add priority dropdown to ticket display
|
Focus: Add priority dropdown to ticket display
|
||||||
|
|
||||||
- [ ] Task 2.1: Add priority dropdown
|
- [x] Task 2.1: Add priority dropdown (a22603d)
|
||||||
- WHERE: `src/gui_2.py` ticket rendering
|
- WHERE: `src/gui_2.py` ticket rendering
|
||||||
- WHAT: Dropdown for priority selection
|
- WHAT: Dropdown for priority selection
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -42,7 +42,7 @@ Focus: Add priority dropdown to ticket display
|
|||||||
imgui.end_combo()
|
imgui.end_combo()
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Task 2.2: Add color coding
|
- [x] Task 2.2: Add color coding (a22603d)
|
||||||
- WHERE: `src/gui_2.py` ticket rendering
|
- WHERE: `src/gui_2.py` ticket rendering
|
||||||
- WHAT: Color-code priority display
|
- WHAT: Color-code priority display
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -54,7 +54,7 @@ Focus: Add priority dropdown to ticket display
|
|||||||
## Phase 3: Multi-Select
|
## Phase 3: Multi-Select
|
||||||
Focus: Enable ticket selection for bulk operations
|
Focus: Enable ticket selection for bulk operations
|
||||||
|
|
||||||
- [ ] Task 3.1: Add selection state
|
- [x] Task 3.1: Add selection state (a22603d)
|
||||||
- WHERE: `src/gui_2.py` or `src/app_controller.py`
|
- WHERE: `src/gui_2.py` or `src/app_controller.py`
|
||||||
- WHAT: Track selected ticket IDs
|
- WHAT: Track selected ticket IDs
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -62,7 +62,7 @@ Focus: Enable ticket selection for bulk operations
|
|||||||
self._selected_tickets: set[str] = set()
|
self._selected_tickets: set[str] = set()
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Task 3.2: Add checkbox per ticket
|
- [x] Task 3.2: Add checkbox per ticket (a22603d)
|
||||||
- WHERE: `src/gui_2.py` ticket list rendering
|
- WHERE: `src/gui_2.py` ticket list rendering
|
||||||
- WHAT: Checkbox for selection
|
- WHAT: Checkbox for selection
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -76,7 +76,7 @@ Focus: Enable ticket selection for bulk operations
|
|||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Task 3.3: Add select all/none buttons
|
- [x] Task 3.3: Add select all/none buttons (a22603d)
|
||||||
- WHERE: `src/gui_2.py` ticket list header
|
- WHERE: `src/gui_2.py` ticket list header
|
||||||
- WHAT: Buttons to select/deselect all
|
- WHAT: Buttons to select/deselect all
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -91,7 +91,7 @@ Focus: Enable ticket selection for bulk operations
|
|||||||
## Phase 4: Bulk Actions
|
## Phase 4: Bulk Actions
|
||||||
Focus: Execute bulk operations on selected tickets
|
Focus: Execute bulk operations on selected tickets
|
||||||
|
|
||||||
- [ ] Task 4.1: Add bulk action buttons
|
- [x] Task 4.1: Add bulk action buttons (a22603d)
|
||||||
- WHERE: `src/gui_2.py` ticket list area
|
- WHERE: `src/gui_2.py` ticket list area
|
||||||
- WHAT: Execute, Skip, Block buttons
|
- WHAT: Execute, Skip, Block buttons
|
||||||
- HOW:
|
- HOW:
|
||||||
@@ -112,7 +112,7 @@ Focus: Execute bulk operations on selected tickets
|
|||||||
## Phase 5: Drag-Drop (Optional)
|
## Phase 5: Drag-Drop (Optional)
|
||||||
Focus: Allow ticket reordering
|
Focus: Allow ticket reordering
|
||||||
|
|
||||||
- [ ] Task 5.1: Implement drag-drop reordering
|
- [x] Task 5.1: Implement drag-drop reordering (a22603d)
|
||||||
- WHERE: `src/gui_2.py` ticket list
|
- WHERE: `src/gui_2.py` ticket list
|
||||||
- WHAT: Drag tickets to reorder
|
- WHAT: Drag tickets to reorder
|
||||||
- HOW: Use imgui drag-drop API
|
- HOW: Use imgui drag-drop API
|
||||||
@@ -121,11 +121,11 @@ Focus: Allow ticket reordering
|
|||||||
## Phase 6: Testing
|
## Phase 6: Testing
|
||||||
Focus: Verify all functionality
|
Focus: Verify all functionality
|
||||||
|
|
||||||
- [ ] Task 6.1: Write unit tests
|
- [x] Task 6.1: Write unit tests (a22603d)
|
||||||
- WHERE: `tests/test_ticket_queue.py` (new file)
|
- WHERE: `tests/test_ticket_queue.py` (new file)
|
||||||
- WHAT: Test priority serialization, bulk operations
|
- WHAT: Test priority serialization, bulk operations
|
||||||
- HOW: Create mock tickets, verify state changes
|
- HOW: Create mock tickets, verify state changes
|
||||||
|
|
||||||
- [ ] Task 6.2: Conductor - Phase Verification
|
- [x] Task 6.2: Conductor - Phase Verification (a22603d)
|
||||||
- Run: `uv run pytest tests/test_ticket_queue.py -v`
|
- Run: `uv run pytest tests/test_ticket_queue.py -v`
|
||||||
- Manual: Verify UI controls work
|
- Manual: Verify UI controls work
|
||||||
|
|||||||
152
src/gui_2.py
152
src/gui_2.py
@@ -105,6 +105,8 @@ class App:
|
|||||||
self.node_editor_config = ed.Config()
|
self.node_editor_config = ed.Config()
|
||||||
self.node_editor_ctx = ed.create_editor(self.node_editor_config)
|
self.node_editor_ctx = ed.create_editor(self.node_editor_config)
|
||||||
self.ui_selected_ticket_id: Optional[str] = None
|
self.ui_selected_ticket_id: Optional[str] = None
|
||||||
|
self.ui_selected_tickets: set[str] = set()
|
||||||
|
self.ui_new_ticket_priority: str = "medium"
|
||||||
self._autofocus_response_tab = False
|
self._autofocus_response_tab = False
|
||||||
gui_cfg = self.config.get("gui", {})
|
gui_cfg = self.config.get("gui", {})
|
||||||
self.ui_separate_message_panel = gui_cfg.get("separate_message_panel", False)
|
self.ui_separate_message_panel = gui_cfg.get("separate_message_panel", False)
|
||||||
@@ -1930,6 +1932,137 @@ class App:
|
|||||||
self._scroll_tool_calls_to_bottom = False
|
self._scroll_tool_calls_to_bottom = False
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_calls_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_calls_panel")
|
||||||
|
|
||||||
|
def bulk_execute(self) -> None:
|
||||||
|
for tid in self.ui_selected_tickets:
|
||||||
|
t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None)
|
||||||
|
if t: t['status'] = 'ready'
|
||||||
|
self._push_mma_state_update()
|
||||||
|
|
||||||
|
def bulk_skip(self) -> None:
|
||||||
|
for tid in self.ui_selected_tickets:
|
||||||
|
t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None)
|
||||||
|
if t: t['status'] = 'completed'
|
||||||
|
self._push_mma_state_update()
|
||||||
|
|
||||||
|
def bulk_block(self) -> None:
|
||||||
|
for tid in self.ui_selected_tickets:
|
||||||
|
t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None)
|
||||||
|
if t: t['status'] = 'blocked'
|
||||||
|
self._push_mma_state_update()
|
||||||
|
|
||||||
|
def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None:
|
||||||
|
if src_idx == dst_idx: return
|
||||||
|
new_tickets = list(self.active_tickets)
|
||||||
|
ticket = new_tickets.pop(src_idx)
|
||||||
|
new_tickets.insert(dst_idx, ticket)
|
||||||
|
# Validate dependencies: a ticket cannot be placed before any of its dependencies
|
||||||
|
id_to_idx = {str(t.get('id', '')): i for i, t in enumerate(new_tickets)}
|
||||||
|
valid = True
|
||||||
|
for i, t in enumerate(new_tickets):
|
||||||
|
deps = t.get('depends_on', [])
|
||||||
|
for d_id in deps:
|
||||||
|
if d_id in id_to_idx and id_to_idx[d_id] >= i:
|
||||||
|
valid = False
|
||||||
|
break
|
||||||
|
if not valid: break
|
||||||
|
if valid:
|
||||||
|
self.active_tickets = new_tickets
|
||||||
|
self._push_mma_state_update()
|
||||||
|
|
||||||
|
def _render_ticket_queue(self) -> None:
|
||||||
|
imgui.text("Ticket Queue Management")
|
||||||
|
if not self.active_track:
|
||||||
|
imgui.text_disabled("No active track.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Select All / None
|
||||||
|
if imgui.button("Select All"):
|
||||||
|
self.ui_selected_tickets = {str(t.get('id', '')) for t in self.active_tickets}
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Select None"):
|
||||||
|
self.ui_selected_tickets.clear()
|
||||||
|
|
||||||
|
imgui.same_line()
|
||||||
|
imgui.spacing()
|
||||||
|
imgui.same_line()
|
||||||
|
|
||||||
|
# Bulk Actions
|
||||||
|
if imgui.button("Bulk Execute"):
|
||||||
|
self.bulk_execute()
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Bulk Skip"):
|
||||||
|
self.bulk_skip()
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.button("Bulk Block"):
|
||||||
|
self.bulk_block()
|
||||||
|
# Table
|
||||||
|
flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable | imgui.TableFlags_.scroll_y
|
||||||
|
if imgui.begin_table("ticket_queue_table", 5, flags, imgui.ImVec2(0, 300)):
|
||||||
|
imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40)
|
||||||
|
imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80)
|
||||||
|
imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100)
|
||||||
|
imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100)
|
||||||
|
imgui.table_setup_column("Description", imgui.TableColumnFlags_.width_stretch)
|
||||||
|
imgui.table_headers_row()
|
||||||
|
|
||||||
|
for i, t in enumerate(self.active_tickets):
|
||||||
|
tid = str(t.get('id', ''))
|
||||||
|
imgui.table_next_row()
|
||||||
|
|
||||||
|
# Select
|
||||||
|
imgui.table_next_column()
|
||||||
|
is_sel = tid in self.ui_selected_tickets
|
||||||
|
changed, is_sel = imgui.checkbox(f"##sel_{tid}", is_sel)
|
||||||
|
if changed:
|
||||||
|
if is_sel: self.ui_selected_tickets.add(tid)
|
||||||
|
else: self.ui_selected_tickets.discard(tid)
|
||||||
|
|
||||||
|
# ID
|
||||||
|
imgui.table_next_column()
|
||||||
|
is_selected = (tid == self.ui_selected_ticket_id)
|
||||||
|
opened, _ = imgui.selectable(f"{tid}##drag_{tid}", is_selected)
|
||||||
|
if opened:
|
||||||
|
self.ui_selected_ticket_id = tid
|
||||||
|
|
||||||
|
if imgui.begin_drag_drop_source():
|
||||||
|
imgui.set_drag_drop_payload("TICKET_REORDER", i)
|
||||||
|
imgui.text(f"Moving {tid}")
|
||||||
|
imgui.end_drag_drop_source()
|
||||||
|
|
||||||
|
if imgui.begin_drag_drop_target():
|
||||||
|
payload = imgui.accept_drag_drop_payload("TICKET_REORDER")
|
||||||
|
if payload:
|
||||||
|
src_idx = int(payload.data)
|
||||||
|
self._reorder_ticket(src_idx, i)
|
||||||
|
imgui.end_drag_drop_target()
|
||||||
|
|
||||||
|
# Priority
|
||||||
|
|
||||||
|
imgui.table_next_column()
|
||||||
|
prio = t.get('priority', 'medium')
|
||||||
|
p_col = vec4(180, 180, 180) # gray
|
||||||
|
if prio == 'high': p_col = vec4(255, 100, 100) # red
|
||||||
|
elif prio == 'medium': p_col = vec4(255, 255, 100) # yellow
|
||||||
|
|
||||||
|
imgui.push_style_color(imgui.Col_.text, p_col)
|
||||||
|
if imgui.begin_combo(f"##prio_{tid}", prio, imgui.ComboFlags_.height_small):
|
||||||
|
for p_opt in ['high', 'medium', 'low']:
|
||||||
|
if imgui.selectable(p_opt, p_opt == prio)[0]:
|
||||||
|
t['priority'] = p_opt
|
||||||
|
self._push_mma_state_update()
|
||||||
|
imgui.end_combo()
|
||||||
|
imgui.pop_style_color()
|
||||||
|
|
||||||
|
# Status
|
||||||
|
imgui.table_next_column()
|
||||||
|
imgui.text(t.get('status', 'todo'))
|
||||||
|
|
||||||
|
# Description
|
||||||
|
imgui.table_next_column()
|
||||||
|
imgui.text(t.get('description', ''))
|
||||||
|
|
||||||
|
imgui.end_table()
|
||||||
|
|
||||||
def _render_mma_dashboard(self) -> None:
|
def _render_mma_dashboard(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard")
|
||||||
# Task 5.3: Dense Summary Line
|
# Task 5.3: Dense Summary Line
|
||||||
@@ -2178,6 +2311,8 @@ class App:
|
|||||||
|
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
|
self._render_ticket_queue()
|
||||||
|
imgui.separator()
|
||||||
# 4. Task DAG Visualizer
|
# 4. Task DAG Visualizer
|
||||||
imgui.text("Task DAG")
|
imgui.text("Task DAG")
|
||||||
if self.active_track and self.node_editor_ctx:
|
if self.active_track and self.node_editor_ctx:
|
||||||
@@ -2292,11 +2427,19 @@ class App:
|
|||||||
_, self.ui_new_ticket_desc = imgui.input_text_multiline("Description##new_ticket", self.ui_new_ticket_desc, imgui.ImVec2(-1, 60))
|
_, self.ui_new_ticket_desc = imgui.input_text_multiline("Description##new_ticket", self.ui_new_ticket_desc, imgui.ImVec2(-1, 60))
|
||||||
_, self.ui_new_ticket_target = imgui.input_text("Target File##new_ticket", self.ui_new_ticket_target)
|
_, self.ui_new_ticket_target = imgui.input_text("Target File##new_ticket", self.ui_new_ticket_target)
|
||||||
_, self.ui_new_ticket_deps = imgui.input_text("Depends On (IDs, comma-separated)##new_ticket", self.ui_new_ticket_deps)
|
_, self.ui_new_ticket_deps = imgui.input_text("Depends On (IDs, comma-separated)##new_ticket", self.ui_new_ticket_deps)
|
||||||
|
imgui.text("Priority:")
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.begin_combo("##new_prio", self.ui_new_ticket_priority):
|
||||||
|
for p_opt in ['high', 'medium', 'low']:
|
||||||
|
if imgui.selectable(p_opt, p_opt == self.ui_new_ticket_priority)[0]:
|
||||||
|
self.ui_new_ticket_priority = p_opt
|
||||||
|
imgui.end_combo()
|
||||||
if imgui.button("Create"):
|
if imgui.button("Create"):
|
||||||
new_ticket = {
|
new_ticket = {
|
||||||
"id": self.ui_new_ticket_id,
|
"id": self.ui_new_ticket_id,
|
||||||
"description": self.ui_new_ticket_desc,
|
"description": self.ui_new_ticket_desc,
|
||||||
"status": "todo",
|
"status": "todo",
|
||||||
|
"priority": self.ui_new_ticket_priority,
|
||||||
"assigned_to": "tier3-worker",
|
"assigned_to": "tier3-worker",
|
||||||
"target_file": self.ui_new_ticket_target,
|
"target_file": self.ui_new_ticket_target,
|
||||||
"depends_on": [d.strip() for d in self.ui_new_ticket_deps.split(",") if d.strip()]
|
"depends_on": [d.strip() for d in self.ui_new_ticket_deps.split(",") if d.strip()]
|
||||||
@@ -2318,6 +2461,15 @@ class App:
|
|||||||
ticket = next((t for t in self.active_tickets if str(t.get('id', '')) == self.ui_selected_ticket_id), None)
|
ticket = next((t for t in self.active_tickets if str(t.get('id', '')) == self.ui_selected_ticket_id), None)
|
||||||
if ticket:
|
if ticket:
|
||||||
imgui.text(f"Status: {ticket.get('status', 'todo')}")
|
imgui.text(f"Status: {ticket.get('status', 'todo')}")
|
||||||
|
prio = ticket.get('priority', 'medium')
|
||||||
|
imgui.text("Priority:")
|
||||||
|
imgui.same_line()
|
||||||
|
if imgui.begin_combo(f"##edit_prio_{ticket.get('id')}", prio):
|
||||||
|
for p_opt in ['high', 'medium', 'low']:
|
||||||
|
if imgui.selectable(p_opt, p_opt == prio)[0]:
|
||||||
|
ticket['priority'] = p_opt
|
||||||
|
self._push_mma_state_update()
|
||||||
|
imgui.end_combo()
|
||||||
imgui.text(f"Target: {ticket.get('target_file', '')}")
|
imgui.text(f"Target: {ticket.get('target_file', '')}")
|
||||||
deps = ticket.get('depends_on', [])
|
deps = ticket.get('depends_on', [])
|
||||||
imgui.text(f"Depends on: {', '.join(deps)}")
|
imgui.text(f"Depends on: {', '.join(deps)}")
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class Ticket:
|
|||||||
description: str
|
description: str
|
||||||
status: str = "todo"
|
status: str = "todo"
|
||||||
assigned_to: str = "unassigned"
|
assigned_to: str = "unassigned"
|
||||||
|
priority: str = "medium"
|
||||||
target_file: Optional[str] = None
|
target_file: Optional[str] = None
|
||||||
target_symbols: List[str] = field(default_factory=list)
|
target_symbols: List[str] = field(default_factory=list)
|
||||||
context_requirements: List[str] = field(default_factory=list)
|
context_requirements: List[str] = field(default_factory=list)
|
||||||
@@ -90,6 +91,7 @@ class Ticket:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
"assigned_to": self.assigned_to,
|
"assigned_to": self.assigned_to,
|
||||||
|
"priority": self.priority,
|
||||||
"target_file": self.target_file,
|
"target_file": self.target_file,
|
||||||
"target_symbols": self.target_symbols,
|
"target_symbols": self.target_symbols,
|
||||||
"context_requirements": self.context_requirements,
|
"context_requirements": self.context_requirements,
|
||||||
@@ -105,7 +107,8 @@ class Ticket:
|
|||||||
id=data["id"],
|
id=data["id"],
|
||||||
description=data.get("description", ""),
|
description=data.get("description", ""),
|
||||||
status=data.get("status", "todo"),
|
status=data.get("status", "todo"),
|
||||||
assigned_to=data.get("assigned_to", ""),
|
assigned_to=data.get("assigned_to", "unassigned"),
|
||||||
|
priority=data.get("priority", "medium"),
|
||||||
target_file=data.get("target_file"),
|
target_file=data.get("target_file"),
|
||||||
target_symbols=data.get("target_symbols", []),
|
target_symbols=data.get("target_symbols", []),
|
||||||
context_requirements=data.get("context_requirements", []),
|
context_requirements=data.get("context_requirements", []),
|
||||||
|
|||||||
109
tests/test_ticket_queue.py
Normal file
109
tests/test_ticket_queue.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from src.models import Ticket
|
||||||
|
|
||||||
|
def test_ticket_priority_default():
|
||||||
|
ticket = Ticket(id="T1", description="Test ticket")
|
||||||
|
assert ticket.priority == "medium"
|
||||||
|
|
||||||
|
def test_ticket_priority_custom():
|
||||||
|
ticket_high = Ticket(id="T2", description="High priority", priority="high")
|
||||||
|
assert ticket_high.priority == "high"
|
||||||
|
|
||||||
|
ticket_low = Ticket(id="T3", description="Low priority", priority="low")
|
||||||
|
assert ticket_low.priority == "low"
|
||||||
|
|
||||||
|
def test_ticket_to_dict_priority():
|
||||||
|
ticket = Ticket(id="T4", description="To dict test", priority="high")
|
||||||
|
d = ticket.to_dict()
|
||||||
|
assert "priority" in d
|
||||||
|
assert d["priority"] == "high"
|
||||||
|
|
||||||
|
def test_ticket_from_dict_priority():
|
||||||
|
data = {
|
||||||
|
"id": "T5",
|
||||||
|
"description": "From dict test",
|
||||||
|
"priority": "low",
|
||||||
|
"status": "todo"
|
||||||
|
}
|
||||||
|
ticket = Ticket.from_dict(data)
|
||||||
|
assert ticket.priority == "low"
|
||||||
|
|
||||||
|
def test_ticket_from_dict_default_priority():
|
||||||
|
data = {
|
||||||
|
"id": "T6",
|
||||||
|
"description": "No priority in dict"
|
||||||
|
}
|
||||||
|
ticket = Ticket.from_dict(data)
|
||||||
|
assert ticket.priority == "medium"
|
||||||
|
|
||||||
|
class TestBulkOperations:
|
||||||
|
def test_bulk_execute(self, mock_app):
|
||||||
|
mock_app.active_tickets = [
|
||||||
|
{"id": "T1", "status": "todo"},
|
||||||
|
{"id": "T2", "status": "todo"},
|
||||||
|
{"id": "T3", "status": "todo"}
|
||||||
|
]
|
||||||
|
mock_app.ui_selected_tickets = {"T1", "T3"}
|
||||||
|
|
||||||
|
with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push:
|
||||||
|
mock_app.bulk_execute()
|
||||||
|
assert mock_app.active_tickets[0]["status"] == "ready"
|
||||||
|
assert mock_app.active_tickets[1]["status"] == "todo"
|
||||||
|
assert mock_app.active_tickets[2]["status"] == "ready"
|
||||||
|
mock_push.assert_called_once()
|
||||||
|
|
||||||
|
def test_bulk_skip(self, mock_app):
|
||||||
|
mock_app.active_tickets = [
|
||||||
|
{"id": "T1", "status": "todo"},
|
||||||
|
{"id": "T2", "status": "todo"}
|
||||||
|
]
|
||||||
|
mock_app.ui_selected_tickets = {"T1"}
|
||||||
|
|
||||||
|
with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push:
|
||||||
|
mock_app.bulk_skip()
|
||||||
|
assert mock_app.active_tickets[0]["status"] == "completed"
|
||||||
|
assert mock_app.active_tickets[1]["status"] == "todo"
|
||||||
|
mock_push.assert_called_once()
|
||||||
|
|
||||||
|
def test_bulk_block(self, mock_app):
|
||||||
|
mock_app.active_tickets = [
|
||||||
|
{"id": "T1", "status": "todo"},
|
||||||
|
{"id": "T2", "status": "todo"}
|
||||||
|
]
|
||||||
|
mock_app.ui_selected_tickets = {"T1", "T2"}
|
||||||
|
|
||||||
|
with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push:
|
||||||
|
mock_app.bulk_block()
|
||||||
|
assert mock_app.active_tickets[0]["status"] == "blocked"
|
||||||
|
assert mock_app.active_tickets[1]["status"] == "blocked"
|
||||||
|
mock_push.assert_called_once()
|
||||||
|
|
||||||
|
class TestReorder:
|
||||||
|
def test_reorder_ticket_valid(self, mock_app):
|
||||||
|
mock_app.active_tickets = [
|
||||||
|
{"id": "T1", "depends_on": []},
|
||||||
|
{"id": "T2", "depends_on": []},
|
||||||
|
{"id": "T3", "depends_on": ["T1"]}
|
||||||
|
]
|
||||||
|
with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push:
|
||||||
|
# Move T1 to index 1: [T2, T1, T3]. T3 depends on T1. T1 index 1 < T3 index 2. VALID.
|
||||||
|
mock_app._reorder_ticket(0, 1)
|
||||||
|
assert mock_app.active_tickets[0]["id"] == "T2"
|
||||||
|
assert mock_app.active_tickets[1]["id"] == "T1"
|
||||||
|
assert mock_app.active_tickets[2]["id"] == "T3"
|
||||||
|
mock_push.assert_called_once()
|
||||||
|
|
||||||
|
def test_reorder_ticket_invalid(self, mock_app):
|
||||||
|
mock_app.active_tickets = [
|
||||||
|
{"id": "T1", "depends_on": []},
|
||||||
|
{"id": "T2", "depends_on": ["T1"]}
|
||||||
|
]
|
||||||
|
with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push:
|
||||||
|
# Move T1 after T2: [T2, T1]. T2 depends on T1, but T1 is now at index 1 while T2 is at index 0.
|
||||||
|
# Violation: dependency T1 (index 1) is not before T2 (index 0).
|
||||||
|
mock_app._reorder_ticket(0, 1)
|
||||||
|
# Should NOT change
|
||||||
|
assert mock_app.active_tickets[0]["id"] == "T1"
|
||||||
|
assert mock_app.active_tickets[1]["id"] == "T2"
|
||||||
|
mock_push.assert_not_called()
|
||||||
Reference in New Issue
Block a user