refactor(sdm): Global pass with refined 'External Only' SDM tags. Pruned redundant internal references and fixed indentation logic in injector. Verified full project compilation.
This commit is contained in:
+4
-4
@@ -1,6 +1,6 @@
|
||||
[ai]
|
||||
provider = "minimax"
|
||||
model = "MiniMax-M2.5"
|
||||
provider = "gemini"
|
||||
model = "gemini-2.5-flash-lite"
|
||||
temperature = 0.0
|
||||
top_p = 1.0
|
||||
max_tokens = 32000
|
||||
@@ -24,7 +24,7 @@ bg_shader_enabled = false
|
||||
crt_filter_enabled = false
|
||||
separate_task_dag = false
|
||||
separate_usage_analytics = false
|
||||
separate_tier1 = false
|
||||
separate_tier1 = true
|
||||
separate_tier2 = false
|
||||
separate_tier3 = false
|
||||
separate_tier4 = false
|
||||
@@ -36,7 +36,7 @@ separate_external_tools = false
|
||||
"AI Settings" = true
|
||||
"MMA Dashboard" = true
|
||||
"Task DAG" = false
|
||||
"Usage Analytics" = false
|
||||
"Usage Analytics" = true
|
||||
"Tier 1" = false
|
||||
"Tier 2" = false
|
||||
"Tier 3" = false
|
||||
|
||||
@@ -9,5 +9,5 @@ active = "main"
|
||||
|
||||
[discussions.main]
|
||||
git_commit = ""
|
||||
last_updated = "2026-05-09T12:24:27"
|
||||
last_updated = "2026-05-09T14:29:48"
|
||||
history = []
|
||||
|
||||
Binary file not shown.
@@ -3,6 +3,9 @@ from simulation.sim_base import BaseSimulation, run_sim
|
||||
|
||||
class AISettingsSimulation(BaseSimulation):
|
||||
def run(self) -> None:
|
||||
"""
|
||||
[C: simulation/sim_base.py:run_sim, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
"""
|
||||
print("\n--- Running AI Settings Simulation (Gemini Only) ---")
|
||||
# 1. Verify initial model
|
||||
provider = self.client.get_value("current_provider")
|
||||
@@ -28,4 +31,4 @@ class AISettingsSimulation(BaseSimulation):
|
||||
assert final_model == target_model, f"Expected {target_model}, got {final_model}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_sim(AISettingsSimulation)
|
||||
run_sim(AISettingsSimulation)
|
||||
+17
-2
@@ -62,6 +62,9 @@ class BaseSimulation:
|
||||
self.project_path = None
|
||||
|
||||
def setup(self, project_name: str = "SimProject") -> None:
|
||||
"""
|
||||
[C: simulation/sim_execution.py:ExecutionSimulation.setup, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_sim_base.py:test_base_simulation_setup]
|
||||
"""
|
||||
print("\n[BaseSim] Connecting to GUI...")
|
||||
if not self.client.wait_for_server(timeout=5):
|
||||
raise RuntimeError("Could not connect to GUI. Ensure it is running with --enable-test-hooks")
|
||||
@@ -88,6 +91,9 @@ class BaseSimulation:
|
||||
time.sleep(1.5)
|
||||
|
||||
def teardown(self) -> None:
|
||||
"""
|
||||
[C: tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live]
|
||||
"""
|
||||
if self.project_path and os.path.exists(self.project_path):
|
||||
# We keep it for debugging if it failed, but usually we'd clean up
|
||||
# os.remove(self.project_path)
|
||||
@@ -95,9 +101,15 @@ class BaseSimulation:
|
||||
print("[BaseSim] Teardown complete.")
|
||||
|
||||
def get_value(self, tag: str) -> Any:
|
||||
"""
|
||||
[C: simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/smoke_status_hook.py:test_status_hook, tests/smoke_status_hook.py:wait_for_value, tests/test_auto_switch_sim.py:test_auto_switch_sim, tests/test_deepseek_infra.py:test_gui_provider_list_via_hooks, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_gui2_parity.py:test_gui2_click_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_rag_visual_sim.py:test_rag_full_lifecycle_sim, tests/test_rag_visual_sim.py:test_rag_settings_persistence_sim, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_system_prompt_sim.py:test_system_prompt_sim, tests/test_undo_redo_sim.py:test_undo_redo_context_mutation, tests/test_undo_redo_sim.py:test_undo_redo_discussion_mutation, tests/test_undo_redo_sim.py:test_undo_redo_lifecycle, tests/test_visual_mma.py:test_visual_mma_components, tests/test_workspace_profiles_sim.py:test_workspace_profiles_restoration]
|
||||
"""
|
||||
return self.client.get_value(tag)
|
||||
|
||||
def wait_for_event(self, event_type: str, timeout: int = 5) -> Optional[dict]:
|
||||
"""
|
||||
[C: simulation/sim_execution.py:ExecutionSimulation.run, tests/test_z_negative_flows.py:test_mock_error_result, tests/test_z_negative_flows.py:test_mock_malformed_json, tests/test_z_negative_flows.py:test_mock_timeout]
|
||||
"""
|
||||
return self.client.wait_for_event(event_type, timeout)
|
||||
|
||||
def assert_panel_visible(self, panel_tag: str, msg: str = None) -> None:
|
||||
@@ -115,7 +127,10 @@ class BaseSimulation:
|
||||
return False
|
||||
|
||||
def run_sim(sim_class: type) -> None:
|
||||
"""Helper to run a simulation class standalone."""
|
||||
"""
|
||||
Helper to run a simulation class standalone.
|
||||
[C: simulation/sim_context.py:module, simulation/sim_execution.py:module, simulation/sim_tools.py:module]
|
||||
"""
|
||||
sim = sim_class()
|
||||
try:
|
||||
sim.setup()
|
||||
@@ -127,4 +142,4 @@ def run_sim(sim_class: type) -> None:
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
sim.teardown()
|
||||
sim.teardown()
|
||||
@@ -4,6 +4,9 @@ from simulation.sim_base import BaseSimulation, run_sim
|
||||
|
||||
class ContextSimulation(BaseSimulation):
|
||||
def run(self) -> None:
|
||||
"""
|
||||
[C: tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
"""
|
||||
print("\n--- Running Context & Chat Simulation ---")
|
||||
# 1. Skip Discussion Creation, use 'main'
|
||||
print("[Sim] Using existing 'main' discussion")
|
||||
@@ -70,4 +73,4 @@ class ContextSimulation(BaseSimulation):
|
||||
assert len(chat_entries) == 2, f"Expected exactly 2 chat entries after truncation, found {len(chat_entries)}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_sim(ContextSimulation)
|
||||
run_sim(ContextSimulation)
|
||||
@@ -4,11 +4,17 @@ from simulation.sim_base import BaseSimulation, run_sim
|
||||
|
||||
class ExecutionSimulation(BaseSimulation):
|
||||
def setup(self, project_name: str = "SimProject") -> None:
|
||||
"""
|
||||
[C: tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_sim_base.py:test_base_simulation_setup]
|
||||
"""
|
||||
super().setup(project_name)
|
||||
if os.path.exists("hello.ps1"):
|
||||
os.remove("hello.ps1")
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
[C: tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
"""
|
||||
print("\n--- Running Execution & Modals Simulation ---")
|
||||
# 1. Trigger script generation (Async so we don't block on the wait loop)
|
||||
msg = "Create a hello.ps1 script that prints 'Simulation Test' and execute it."
|
||||
@@ -65,4 +71,4 @@ class ExecutionSimulation(BaseSimulation):
|
||||
print(f"[Sim] Final check: approved {approved_count} scripts.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_sim(ExecutionSimulation)
|
||||
run_sim(ExecutionSimulation)
|
||||
@@ -3,6 +3,9 @@ from simulation.sim_base import BaseSimulation, run_sim
|
||||
|
||||
class ToolsSimulation(BaseSimulation):
|
||||
def run(self) -> None:
|
||||
"""
|
||||
[C: tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
"""
|
||||
print("\n--- Running Tools Simulation ---")
|
||||
# 1. Trigger list_directory tool
|
||||
msg = "List the files in the current directory."
|
||||
@@ -39,4 +42,4 @@ class ToolsSimulation(BaseSimulation):
|
||||
print(f"[Sim] Final AI Response: {last_ai_msg[:100]}...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_sim(ToolsSimulation)
|
||||
run_sim(ToolsSimulation)
|
||||
@@ -22,16 +22,25 @@ class UserSimAgent:
|
||||
return max(0.5, min(5.0, delay))
|
||||
|
||||
def wait_to_read(self, text: str) -> None:
|
||||
"""
|
||||
[C: simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response]
|
||||
"""
|
||||
if self.enable_delays:
|
||||
delay = self.calculate_reading_delay(text)
|
||||
time.sleep(delay)
|
||||
|
||||
def wait_to_think(self, probability: float = 0.2, min_delay: float = 2.0, max_delay: float = 5.0) -> None:
|
||||
"""
|
||||
[C: simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response]
|
||||
"""
|
||||
if self.enable_delays and random.random() < probability:
|
||||
delay = random.uniform(min_delay, max_delay)
|
||||
time.sleep(delay)
|
||||
|
||||
def simulate_typing(self, text: str, jitter_range: tuple[float, float] = (0.01, 0.05), batch_typing: bool = False) -> None:
|
||||
"""
|
||||
[C: simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async]
|
||||
"""
|
||||
if not self.enable_delays:
|
||||
return
|
||||
if batch_typing or self.batch_typing:
|
||||
@@ -50,8 +59,10 @@ class UserSimAgent:
|
||||
|
||||
def generate_response(self, conversation_history: list[dict]) -> str:
|
||||
"""
|
||||
Generates a human-like response based on the conversation history.
|
||||
conversation_history: list of dicts with 'role' and 'content'
|
||||
|
||||
Generates a human-like response based on the conversation history.
|
||||
conversation_history: list of dicts with 'role' and 'content'
|
||||
[C: simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async]
|
||||
"""
|
||||
last_ai_msg = ""
|
||||
for entry in reversed(conversation_history):
|
||||
@@ -67,9 +78,11 @@ class UserSimAgent:
|
||||
|
||||
def perform_action_with_delay(self, action_func: Callable, *args: Any, **kwargs: Any) -> Any:
|
||||
"""
|
||||
Executes an action with a human-like delay if enabled.
|
||||
|
||||
Executes an action with a human-like delay if enabled.
|
||||
[C: tests/test_user_agent.py:test_perform_action_with_delay]
|
||||
"""
|
||||
if self.enable_delays:
|
||||
delay = random.uniform(0.5, 2.0)
|
||||
time.sleep(delay)
|
||||
return action_func(*args, **kwargs)
|
||||
return action_func(*args, **kwargs)
|
||||
@@ -49,6 +49,9 @@ class WorkflowSimulator:
|
||||
self.user_agent = UserSimAgent(hook_client)
|
||||
|
||||
def setup_new_project(self, name: str, git_dir: str, project_path: str = None) -> None:
|
||||
"""
|
||||
[C: tests/test_workflow_sim.py:test_setup_new_project]
|
||||
"""
|
||||
print(f"Setting up new project: {name}")
|
||||
if project_path:
|
||||
self.client.click("btn_project_new_automated", user_data=project_path)
|
||||
@@ -62,6 +65,9 @@ class WorkflowSimulator:
|
||||
self.client.set_value("auto_add_history", True)
|
||||
|
||||
def create_discussion(self, name: str) -> None:
|
||||
"""
|
||||
[C: tests/test_workflow_sim.py:test_discussion_switching]
|
||||
"""
|
||||
print(f"Creating discussion: {name}")
|
||||
self.client.set_value("disc_new_name_input", name)
|
||||
self.client.click("btn_disc_create")
|
||||
@@ -69,6 +75,9 @@ class WorkflowSimulator:
|
||||
time.sleep(2)
|
||||
|
||||
def switch_discussion(self, name: str) -> None:
|
||||
"""
|
||||
[C: tests/test_workflow_sim.py:test_discussion_switching]
|
||||
"""
|
||||
print(f"Switching to discussion: {name}")
|
||||
self.client.select_list_item("disc_listbox", name)
|
||||
time.sleep(1)
|
||||
@@ -81,6 +90,9 @@ class WorkflowSimulator:
|
||||
time.sleep(1)
|
||||
|
||||
def truncate_history(self, pairs: int) -> None:
|
||||
"""
|
||||
[C: tests/test_workflow_sim.py:test_history_truncation]
|
||||
"""
|
||||
print(f"Truncating history to {pairs} pairs")
|
||||
self.client.set_value("disc_truncate_pairs", pairs)
|
||||
self.client.click("btn_disc_truncate")
|
||||
@@ -185,4 +197,4 @@ class WorkflowSimulator:
|
||||
print("\nTimeout waiting for AI")
|
||||
active_disc = self.client.get_value("active_discussion")
|
||||
print(f"[DEBUG] Active discussion in GUI at timeout: {active_disc}")
|
||||
return None
|
||||
return None
|
||||
+39
-30
@@ -62,8 +62,9 @@ def resolve_paths(base_dir: Path, entry: str) -> list[Path]:
|
||||
def build_discussion_section(history: list[Any]) -> str:
|
||||
"""
|
||||
|
||||
Builds a markdown section for discussion history.
|
||||
Handles both legacy list[str] and new list[dict].
|
||||
|
||||
Builds a markdown section for discussion history.
|
||||
Handles both legacy list[str] and new list[dict].
|
||||
"""
|
||||
sections = []
|
||||
for i, entry in enumerate(history, start=1):
|
||||
@@ -78,7 +79,7 @@ def build_discussion_section(history: list[Any]) -> str:
|
||||
|
||||
def build_files_section(base_dir: Path, files: list[str | dict[str, Any]]) -> str:
|
||||
"""
|
||||
[C: tests/test_tiered_context.py:test_build_files_section_with_dicts]
|
||||
[C: tests/test_tiered_context.py:test_build_files_section_with_dicts]
|
||||
"""
|
||||
sections = []
|
||||
for entry_raw in files:
|
||||
@@ -125,18 +126,19 @@ def build_screenshots_section(base_dir: Path, screenshots: list[str]) -> str:
|
||||
def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
|
||||
Return a list of dicts describing each file, for use by ai_client when it
|
||||
wants to upload individual files rather than inline everything as markdown.
|
||||
|
||||
Each dict has:
|
||||
path : Path (resolved absolute path)
|
||||
entry : str (original config entry string)
|
||||
content : str (file text, or error string)
|
||||
error : bool
|
||||
mtime : float (last modification time, for skip-if-unchanged optimization)
|
||||
tier : int | None (optional tier for context management)
|
||||
auto_aggregate : bool
|
||||
force_full : bool
|
||||
|
||||
Return a list of dicts describing each file, for use by ai_client when it
|
||||
wants to upload individual files rather than inline everything as markdown.
|
||||
|
||||
Each dict has:
|
||||
path : Path (resolved absolute path)
|
||||
entry : str (original config entry string)
|
||||
content : str (file text, or error string)
|
||||
error : bool
|
||||
mtime : float (last modification time, for skip-if-unchanged optimization)
|
||||
tier : int | None (optional tier for context management)
|
||||
auto_aggregate : bool
|
||||
force_full : bool
|
||||
[C: src/app_controller.py:AppController._bg_task, src/orchestrator_pm.py:module, tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_aggregate_flags.py:test_force_full, tests/test_tiered_context.py:test_build_file_items_with_tiers]
|
||||
"""
|
||||
with get_monitor().scope("build_file_items"):
|
||||
@@ -182,8 +184,9 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
|
||||
def build_summary_section(base_dir: Path, files: list[str | dict[str, Any]]) -> str:
|
||||
"""
|
||||
|
||||
Build a compact summary section using summarize.py — one short block per file.
|
||||
Used as the initial <context> block instead of full file contents.
|
||||
|
||||
Build a compact summary section using summarize.py — one short block per file.
|
||||
Used as the initial <context> block instead of full file contents.
|
||||
"""
|
||||
with get_monitor().scope("build_summary_section"):
|
||||
items = build_file_items(base_dir, files)
|
||||
@@ -191,7 +194,8 @@ def build_summary_section(base_dir: Path, files: list[str | dict[str, Any]]) ->
|
||||
|
||||
def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str:
|
||||
"""
|
||||
Build the files markdown section from pre-read file items (avoids double I/O).
|
||||
|
||||
Build the files markdown section from pre-read file items (avoids double I/O).
|
||||
[C: tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_ui_summary_only_removal.py:test_aggregate_from_items_respects_auto_aggregate]
|
||||
"""
|
||||
sections = []
|
||||
@@ -212,7 +216,7 @@ def _build_files_section_from_items(file_items: list[dict[str, Any]]) -> str:
|
||||
|
||||
def build_beads_section(base_dir: Path) -> str:
|
||||
"""
|
||||
[C: tests/test_aggregate_beads.py:test_build_beads_compaction]
|
||||
[C: tests/test_aggregate_beads.py:test_build_beads_compaction]
|
||||
"""
|
||||
client = beads_client.BeadsClient(base_dir)
|
||||
if not client.is_initialized():
|
||||
@@ -261,14 +265,16 @@ def build_markdown_from_items(file_items: list[dict[str, Any]], screenshot_base_
|
||||
|
||||
def build_markdown_no_history(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], summary_only: bool = False, aggregation_strategy: str = "auto") -> str:
|
||||
"""
|
||||
Build markdown with only files + screenshots (no history). Used for stable caching.
|
||||
|
||||
Build markdown with only files + screenshots (no history). Used for stable caching.
|
||||
[C: src/app_controller.py:AppController._do_generate, tests/test_history_management.py:test_aggregate_blacklist]
|
||||
"""
|
||||
return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history=[], summary_only=summary_only, aggregation_strategy=aggregation_strategy)
|
||||
|
||||
def build_discussion_text(history: list[str]) -> str:
|
||||
"""
|
||||
Build just the discussion history section text. Returns empty string if no history.
|
||||
|
||||
Build just the discussion history section text. Returns empty string if no history.
|
||||
[C: src/app_controller.py:AppController._do_generate, tests/test_history_management.py:test_aggregate_includes_segregated_history]
|
||||
"""
|
||||
if not history:
|
||||
@@ -278,8 +284,9 @@ def build_discussion_text(history: list[str]) -> str:
|
||||
def build_tier1_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str]) -> str:
|
||||
"""
|
||||
|
||||
Tier 1 Context: Strategic/Orchestration.
|
||||
Full content for core conductor files and files with tier=1, summaries for others.
|
||||
|
||||
Tier 1 Context: Strategic/Orchestration.
|
||||
Full content for core conductor files and files with tier=1, summaries for others.
|
||||
[C: tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_aggregate_flags.py:test_force_full, tests/test_tiered_context.py:test_build_tier1_context_exists, tests/test_tiered_context.py:test_tiered_context_by_tier_field]
|
||||
"""
|
||||
core_files = {"product.md", "tech-stack.md", "workflow.md", "tracks.md"}
|
||||
@@ -311,8 +318,9 @@ def build_tier1_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
|
||||
def build_tier2_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str]) -> str:
|
||||
"""
|
||||
|
||||
Tier 2 Context: Architectural/Tech Lead.
|
||||
Full content for all files (standard behavior).
|
||||
|
||||
Tier 2 Context: Architectural/Tech Lead.
|
||||
Full content for all files (standard behavior).
|
||||
[C: tests/test_tiered_context.py:test_build_tier2_context_exists]
|
||||
"""
|
||||
return build_markdown_from_items(file_items, screenshot_base_dir, screenshots, history, summary_only=False)
|
||||
@@ -320,8 +328,9 @@ def build_tier2_context(file_items: list[dict[str, Any]], screenshot_base_dir: P
|
||||
def build_tier3_context(file_items: list[dict[str, Any]], screenshot_base_dir: Path, screenshots: list[str], history: list[str], focus_files: list[str]) -> str:
|
||||
"""
|
||||
|
||||
Tier 3 Context: Execution/Worker.
|
||||
Full content for focus_files and files with tier=3, summaries/skeletons for others.
|
||||
|
||||
Tier 3 Context: Execution/Worker.
|
||||
Full content for focus_files and files with tier=3, summaries/skeletons for others.
|
||||
[C: tests/test_aggregate_flags.py:test_auto_aggregate_skip, tests/test_aggregate_flags.py:test_force_full, tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_tiered_context.py:test_build_tier3_context_ast_skeleton, tests/test_tiered_context.py:test_build_tier3_context_exists, tests/test_tiered_context.py:test_tiered_context_by_tier_field]
|
||||
"""
|
||||
with get_monitor().scope("build_tier3_context"):
|
||||
@@ -388,7 +397,7 @@ def build_markdown(base_dir: Path, files: list[str | dict[str, Any]], screenshot
|
||||
|
||||
def run(config: dict[str, Any], aggregation_strategy: str = "auto") -> tuple[str, Path, list[dict[str, Any]]]:
|
||||
"""
|
||||
[C: simulation/sim_base.py:run_sim, src/ai_client.py:_send_anthropic, src/ai_client.py:_send_deepseek, src/ai_client.py:_send_gemini, src/ai_client.py:_send_gemini_cli, src/ai_client.py:_send_minimax, src/app_controller.py:AppController._cb_start_track, src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._process_event_queue, src/app_controller.py:AppController._start_track_logic, src/external_editor.py:_find_vscode_in_registry, src/gui_2.py:App._render_snapshot_tab, src/gui_2.py:App.run, src/gui_2.py:main, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/project_manager.py:get_git_log, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
[C: simulation/sim_base.py:run_sim, src/ai_client.py:_send_anthropic, src/ai_client.py:_send_deepseek, src/ai_client.py:_send_gemini, src/ai_client.py:_send_gemini_cli, src/ai_client.py:_send_minimax, src/app_controller.py:AppController._cb_start_track, src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._process_event_queue, src/app_controller.py:AppController._start_track_logic, src/external_editor.py:_find_vscode_in_registry, src/gui_2.py:App._render_snapshot_tab, src/gui_2.py:App.run, src/gui_2.py:main, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/project_manager.py:get_git_log, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
"""
|
||||
namespace = config.get("project", {}).get("name")
|
||||
if not namespace:
|
||||
@@ -414,7 +423,7 @@ def run(config: dict[str, Any], aggregation_strategy: str = "auto") -> tuple[str
|
||||
def main() -> None:
|
||||
# Load global config to find active project
|
||||
"""
|
||||
[C: simulation/live_walkthrough.py:module, simulation/ping_pong.py:module, src/api_hooks.py:WebSocketServer._run_loop, src/gui_2.py:module, tests/mock_concurrent_mma.py:module, tests/mock_gemini_cli.py:module, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_allow_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_deny_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_unreachable_hook_server, tests/test_cli_tool_bridge.py:module, tests/test_cli_tool_bridge_mapping.py:TestCliToolBridgeMapping.test_mapping_from_api_format, tests/test_cli_tool_bridge_mapping.py:module, tests/test_discussion_takes.py:module, tests/test_external_editor_gui.py:module, tests/test_headless_service.py:TestHeadlessStartup.test_headless_flag_triggers_run, tests/test_headless_service.py:TestHeadlessStartup.test_normal_startup_calls_app_run, tests/test_mma_skeleton.py:module, tests/test_orchestrator_pm.py:module, tests/test_orchestrator_pm_history.py:module, tests/test_post_process.py:module, tests/test_presets.py:module, tests/test_project_serialization.py:module, tests/test_run_worker_lifecycle_abort.py:module, tests/test_symbol_lookup.py:module, tests/test_system_prompt_exposure.py:module, tests/test_theme_nerv_fx.py:module]
|
||||
[C: simulation/live_walkthrough.py:module, simulation/ping_pong.py:module, src/api_hooks.py:WebSocketServer._run_loop, src/gui_2.py:module, tests/mock_concurrent_mma.py:module, tests/mock_gemini_cli.py:module, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_allow_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_deny_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_unreachable_hook_server, tests/test_cli_tool_bridge.py:module, tests/test_cli_tool_bridge_mapping.py:TestCliToolBridgeMapping.test_mapping_from_api_format, tests/test_cli_tool_bridge_mapping.py:module, tests/test_discussion_takes.py:module, tests/test_external_editor_gui.py:module, tests/test_headless_service.py:TestHeadlessStartup.test_headless_flag_triggers_run, tests/test_headless_service.py:TestHeadlessStartup.test_normal_startup_calls_app_run, tests/test_mma_skeleton.py:module, tests/test_orchestrator_pm.py:module, tests/test_orchestrator_pm_history.py:module, tests/test_post_process.py:module, tests/test_presets.py:module, tests/test_project_serialization.py:module, tests/test_run_worker_lifecycle_abort.py:module, tests/test_symbol_lookup.py:module, tests/test_system_prompt_exposure.py:module, tests/test_theme_nerv_fx.py:module]
|
||||
"""
|
||||
from src.paths import get_config_path
|
||||
config_path = get_config_path()
|
||||
@@ -436,4 +445,4 @@ def main() -> None:
|
||||
print(f"Written: {output_file}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
+144
-17
@@ -51,7 +51,10 @@ _history_trunc_limit: int = 8000
|
||||
events: EventEmitter = EventEmitter()
|
||||
|
||||
def set_model_params(temp: float, max_tok: int, trunc_limit: int = 8000, top_p: float = 1.0) -> None:
|
||||
"""Sets global generation parameters like temperature and max tokens."""
|
||||
"""
|
||||
Sets global generation parameters like temperature and max tokens.
|
||||
[C: src/app_controller.py:AppController._handle_request_event, src/app_controller.py:AppController.generate, tests/test_history_management.py:test_get_history_bleed_stats_basic]
|
||||
"""
|
||||
global _temperature, _max_tokens, _history_trunc_limit, _top_p
|
||||
_temperature = temp
|
||||
_max_tokens = max_tok
|
||||
@@ -111,21 +114,33 @@ _local_storage = threading.local()
|
||||
_tool_approval_modes: dict[str, str] = {}
|
||||
|
||||
def get_current_tier() -> Optional[str]:
|
||||
"""Returns the current tier from thread-local storage."""
|
||||
"""
|
||||
Returns the current tier from thread-local storage.
|
||||
[C: src/app_controller.py:AppController._on_tool_log, tests/test_ai_client_concurrency.py:intercepted_append]
|
||||
"""
|
||||
return getattr(_local_storage, "current_tier", None)
|
||||
|
||||
def set_current_tier(tier: Optional[str]) -> None:
|
||||
"""Sets the current tier in thread-local storage."""
|
||||
"""
|
||||
Sets the current tier in thread-local storage.
|
||||
[C: src/app_controller.py:AppController._handle_request_event, src/conductor_tech_lead.py:generate_tickets, src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_mma_agent_focus_phase1.py:reset_tier, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_none_when_unset, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_set_when_current_tier_set, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_tier2]
|
||||
"""
|
||||
_local_storage.current_tier = tier
|
||||
|
||||
def get_comms_log_callback() -> Optional[Callable[[dict[str, Any]], None]]:
|
||||
"""Returns the comms log callback (thread-local with global fallback)."""
|
||||
"""
|
||||
Returns the comms log callback (thread-local with global fallback).
|
||||
[C: src/multi_agent_conductor.py:run_worker_lifecycle]
|
||||
"""
|
||||
tl_cb = getattr(_local_storage, "comms_log_callback", None)
|
||||
if tl_cb: return tl_cb
|
||||
return comms_log_callback
|
||||
|
||||
def set_comms_log_callback(cb: Optional[Callable[[dict[str, Any]], None]]) -> None:
|
||||
"""Sets the comms log callback (both global and thread-local)."""
|
||||
"""
|
||||
Sets the comms log callback (both global and thread-local).
|
||||
[C: src/app_controller.py:AppController._init_ai_and_hooks, src/multi_agent_conductor.py:run_worker_lifecycle]
|
||||
"""
|
||||
global comms_log_callback
|
||||
comms_log_callback = cb
|
||||
_local_storage.comms_log_callback = cb
|
||||
@@ -161,19 +176,31 @@ _use_default_base_system_prompt: bool = True
|
||||
_project_context_marker: str = ""
|
||||
|
||||
def set_custom_system_prompt(prompt: str) -> None:
|
||||
"""Sets a custom system prompt to be combined with the default instructions."""
|
||||
"""
|
||||
Sets a custom system prompt to be combined with the default instructions.
|
||||
[C: simulation/user_agent.py:UserSimAgent.generate_response, src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._handle_request_event, src/app_controller.py:AppController.generate, src/conductor_tech_lead.py:generate_tickets, src/multi_agent_conductor.py:run_worker_lifecycle, src/orchestrator_pm.py:generate_tracks, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.setUp]
|
||||
"""
|
||||
global _custom_system_prompt
|
||||
_custom_system_prompt = prompt
|
||||
|
||||
def set_base_system_prompt(prompt: str) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._handle_request_event, src/app_controller.py:AppController.generate, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.setUp, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_ai_client_get_combined_respects_use_default, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_ai_client_set_base_overrides_when_default_false]
|
||||
"""
|
||||
global _base_system_prompt_override
|
||||
_base_system_prompt_override = prompt
|
||||
|
||||
def set_use_default_base_prompt(use_default: bool) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._handle_request_event, src/app_controller.py:AppController.generate, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.setUp, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_ai_client_get_combined_respects_use_default, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_ai_client_set_base_overrides_when_default_false]
|
||||
"""
|
||||
global _use_default_base_system_prompt
|
||||
_use_default_base_system_prompt = use_default
|
||||
|
||||
def set_project_context_marker(marker: str) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._handle_request_event, src/app_controller.py:AppController.generate]
|
||||
"""
|
||||
global _project_context_marker
|
||||
_project_context_marker = marker
|
||||
|
||||
@@ -181,6 +208,9 @@ def _get_context_marker() -> str:
|
||||
return _project_context_marker if _project_context_marker.strip() else "[SYSTEM: FILES UPDATED]"
|
||||
|
||||
def _get_combined_system_prompt(preset: Optional[ToolPreset] = None, bias: Optional[BiasProfile] = None) -> str:
|
||||
"""
|
||||
[C: tests/test_bias_efficacy.py:test_bias_efficacy_prompt_generation, tests/test_bias_integration.py:test_system_prompt_biasing, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_ai_client_get_combined_respects_use_default, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_ai_client_set_base_overrides_when_default_false]
|
||||
"""
|
||||
if preset is None: preset = _active_tool_preset
|
||||
if bias is None: bias = _active_bias_profile
|
||||
if _use_default_base_system_prompt:
|
||||
@@ -196,6 +226,9 @@ def _get_combined_system_prompt(preset: Optional[ToolPreset] = None, bias: Optio
|
||||
return base
|
||||
|
||||
def get_combined_system_prompt(preset: Optional[ToolPreset] = None, bias: Optional[BiasProfile] = None) -> str:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._do_generate, src/app_controller.py:AppController._handle_request_event]
|
||||
"""
|
||||
return _get_combined_system_prompt(preset, bias)
|
||||
|
||||
from collections import deque
|
||||
@@ -205,6 +238,9 @@ _comms_log: deque[dict[str, Any]] = deque(maxlen=1000)
|
||||
COMMS_CLAMP_CHARS: int = 300
|
||||
|
||||
def _append_comms(direction: str, kind: str, payload: dict[str, Any]) -> None:
|
||||
"""
|
||||
[C: tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_mma_agent_focus_phase1.py:test_append_comms_has_source_tier_key, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_none_when_unset, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_set_when_current_tier_set, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_tier2]
|
||||
"""
|
||||
entry: dict[str, Any] = {
|
||||
"ts": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
"direction": direction,
|
||||
@@ -221,15 +257,27 @@ def _append_comms(direction: str, kind: str, payload: dict[str, Any]) -> None:
|
||||
_cb(entry)
|
||||
|
||||
def get_comms_log() -> list[dict[str, Any]]:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._bg_task, src/app_controller.py:AppController._recalculate_session_usage, src/app_controller.py:AppController._start_track_logic, src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_mma_agent_focus_phase1.py:test_append_comms_has_source_tier_key, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_none_when_unset, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_set_when_current_tier_set, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_tier2, tests/test_token_usage.py:test_token_usage_tracking]
|
||||
"""
|
||||
return list(_comms_log)
|
||||
|
||||
def clear_comms_log() -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._handle_reset_session, src/gui_2.py:App._render_comms_history_panel, src/gui_2.py:App._show_menus, tests/test_ai_client_concurrency.py:test_ai_client_tier_isolation, tests/test_token_usage.py:test_token_usage_tracking]
|
||||
"""
|
||||
_comms_log.clear()
|
||||
|
||||
def get_credentials_path() -> Path:
|
||||
"""
|
||||
[C: src/mcp_client.py:_is_allowed]
|
||||
"""
|
||||
return Path(os.environ.get("SLOP_CREDENTIALS", str(Path(__file__).parent.parent / "credentials.toml")))
|
||||
|
||||
def _load_credentials() -> dict[str, Any]:
|
||||
"""
|
||||
[C: tests/test_deepseek_infra.py:test_credentials_error_mentions_deepseek, tests/test_minimax_provider.py:test_minimax_credentials_template]
|
||||
"""
|
||||
cred_path = get_credentials_path()
|
||||
try:
|
||||
with open(cred_path, "rb") as f:
|
||||
@@ -247,12 +295,18 @@ def _load_credentials() -> dict[str, Any]:
|
||||
|
||||
class ProviderError(Exception):
|
||||
def __init__(self, kind: str, provider: str, original: Exception) -> None:
|
||||
"""
|
||||
[C: src/api_hooks.py:HookServerInstance.__init__, src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.kind = kind
|
||||
self.provider = provider
|
||||
self.original = original
|
||||
super().__init__(str(original))
|
||||
|
||||
def ui_message(self) -> str:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._handle_request_event, src/app_controller.py:AppController.generate]
|
||||
"""
|
||||
labels = {
|
||||
"quota": "QUOTA EXHAUSTED",
|
||||
"rate_limit": "RATE LIMITED",
|
||||
@@ -382,7 +436,10 @@ def _classify_minimax_error(exc: Exception) -> ProviderError:
|
||||
return ProviderError("unknown", "minimax", Exception(body))
|
||||
|
||||
def set_provider(provider: str, model: str) -> None:
|
||||
"""Updates the active LLM provider and model name."""
|
||||
"""
|
||||
Updates the active LLM provider and model name.
|
||||
[C: src/app_controller.py:AppController._handle_reset_session, src/app_controller.py:AppController._init_ai_and_hooks, src/app_controller.py:AppController.current_model, src/app_controller.py:AppController.current_provider, src/app_controller.py:AppController.do_fetch, src/multi_agent_conductor.py:run_worker_lifecycle, src/orchestrator_pm.py:generate_tracks, tests/conftest.py:reset_ai_client, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_completion_logic, tests/test_deepseek_provider.py:test_deepseek_model_selection, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoning_logic, tests/test_deepseek_provider.py:test_deepseek_streaming, tests/test_deepseek_provider.py:test_deepseek_tool_calling, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_loop_termination, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_cli_parity_regression.py:test_get_history_bleed_stats, tests/test_gemini_cli_parity_regression.py:test_send_invokes_adapter_send, tests/test_gui2_mcp.py:test_mcp_tool_call_is_dispatched, tests/test_history_management.py:test_get_history_bleed_stats_basic, tests/test_minimax_provider.py:test_minimax_default_model, tests/test_minimax_provider.py:test_minimax_history_bleed_stats, tests/test_minimax_provider.py:test_minimax_model_selection, tests/test_mma_agent_focus_phase1.py:test_append_comms_has_source_tier_key, tests/test_rag_integration.py:test_rag_integration, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_tier4_interceptor.py:test_gemini_provider_passes_qa_callback_to_run_script, tests/test_token_usage.py:test_token_usage_tracking, tests/test_token_viz.py:test_get_history_bleed_stats_returns_all_keys_unknown_provider]
|
||||
"""
|
||||
global _provider, _model
|
||||
_provider = provider
|
||||
if provider == "gemini_cli":
|
||||
@@ -401,11 +458,17 @@ def set_provider(provider: str, model: str) -> None:
|
||||
_model = model
|
||||
|
||||
def get_provider() -> str:
|
||||
"""Returns the current active provider name."""
|
||||
"""
|
||||
Returns the current active provider name.
|
||||
[C: src/multi_agent_conductor.py:run_worker_lifecycle]
|
||||
"""
|
||||
return _provider
|
||||
|
||||
def cleanup() -> None:
|
||||
"""Performs cleanup operations like deleting server-side Gemini caches."""
|
||||
"""
|
||||
Performs cleanup operations like deleting server-side Gemini caches.
|
||||
[C: src/app_controller.py:AppController.clear_cache, src/app_controller.py:AppController.shutdown, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking_cleanup, tests/test_log_registry.py:TestLogRegistry.tearDown, tests/test_project_serialization.py:TestProjectSerialization.tearDown]
|
||||
"""
|
||||
global _gemini_client, _gemini_cache, _gemini_cached_file_paths
|
||||
if _gemini_client and _gemini_cache:
|
||||
try:
|
||||
@@ -415,7 +478,10 @@ def cleanup() -> None:
|
||||
_gemini_cached_file_paths = []
|
||||
|
||||
def reset_session() -> None:
|
||||
"""Clears conversation history and resets provider-specific session state."""
|
||||
"""
|
||||
Clears conversation history and resets provider-specific session state.
|
||||
[C: src/app_controller.py:AppController._handle_reset_session, src/app_controller.py:AppController.current_model, src/app_controller.py:AppController.current_provider, src/app_controller.py:AppController.init_state, src/gui_2.py:App._render_provider_panel, src/gui_2.py:App._show_menus, src/multi_agent_conductor.py:run_worker_lifecycle, tests/conftest.py:live_gui, tests/conftest.py:reset_ai_client, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_metrics.py:test_get_gemini_cache_stats_with_mock_client, tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_minimax_provider.py:test_minimax_history_bleed_stats, tests/test_mma_agent_focus_phase1.py:test_append_comms_has_source_tier_key, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_none_when_unset, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_set_when_current_tier_set, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_tier2, tests/test_session_logger_reset.py:test_reset_session, tests/test_token_usage.py:test_token_usage_tracking]
|
||||
"""
|
||||
global _gemini_client, _gemini_chat, _gemini_cache
|
||||
global _gemini_cache_md_hash, _gemini_cache_created_at, _gemini_cached_file_paths
|
||||
global _anthropic_client, _anthropic_history
|
||||
@@ -454,6 +520,9 @@ def reset_session() -> None:
|
||||
file_cache.reset_client()
|
||||
|
||||
def get_gemini_cache_stats() -> dict[str, Any]:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._recalculate_session_usage, src/app_controller.py:AppController._update_cached_stats, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_gemini_metrics.py:test_get_gemini_cache_stats_with_mock_client]
|
||||
"""
|
||||
_ensure_gemini_client()
|
||||
if not _gemini_client:
|
||||
return {"cache_count": 0, "total_size_bytes": 0, "cached_files": []}
|
||||
@@ -467,6 +536,9 @@ def get_gemini_cache_stats() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
def list_models(provider: str) -> list[str]:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController.do_fetch, tests/test_agent_capabilities.py:test_agent_capabilities_listing, tests/test_ai_client_list_models.py:test_list_models_gemini_cli, tests/test_deepseek_infra.py:test_deepseek_model_listing, tests/test_minimax_provider.py:test_minimax_list_models]
|
||||
"""
|
||||
creds = _load_credentials()
|
||||
if provider == "gemini":
|
||||
return _list_gemini_models(creds["gemini"]["api_key"])
|
||||
@@ -526,14 +598,20 @@ TOOL_NAME: str = "run_powershell"
|
||||
_agent_tools: dict[str, bool] = {}
|
||||
|
||||
def set_agent_tools(tools: dict[str, bool]) -> None:
|
||||
"""Configures which tools are enabled for the AI agent."""
|
||||
"""
|
||||
Configures which tools are enabled for the AI agent.
|
||||
[C: src/app_controller.py:AppController._handle_request_event, src/app_controller.py:AppController.generate, tests/test_agent_tools_wiring.py:test_build_anthropic_tools_conversion, tests/test_agent_tools_wiring.py:test_set_agent_tools, tests/test_tool_access_exclusion.py:test_build_anthropic_tools_excludes_disabled, tests/test_tool_access_exclusion.py:test_build_deepseek_tools_excludes_disabled, tests/test_tool_access_exclusion.py:test_gemini_tool_declaration_excludes_disabled, tests/test_tool_access_exclusion.py:test_set_agent_tools_clears_caches]
|
||||
"""
|
||||
global _agent_tools, _CACHED_ANTHROPIC_TOOLS, _CACHED_DEEPSEEK_TOOLS
|
||||
_agent_tools = tools
|
||||
_CACHED_ANTHROPIC_TOOLS = None
|
||||
_CACHED_DEEPSEEK_TOOLS = None
|
||||
|
||||
def set_tool_preset(preset_name: Optional[str]) -> None:
|
||||
"""Loads a tool preset and applies it via set_agent_tools."""
|
||||
"""
|
||||
Loads a tool preset and applies it via set_agent_tools.
|
||||
[C: src/app_controller.py:AppController.init_state, src/gui_2.py:App._render_persona_selector_panel, src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_bias_integration.py:test_set_tool_preset_with_objects, tests/test_tool_preset_env.py:test_tool_preset_env_loading, tests/test_tool_preset_env.py:test_tool_preset_env_no_var, tests/test_tool_presets_execution.py:test_tool_ask_approval, tests/test_tool_presets_execution.py:test_tool_auto_approval, tests/test_tool_presets_execution.py:test_tool_rejection]
|
||||
"""
|
||||
global _agent_tools, _CACHED_ANTHROPIC_TOOLS, _CACHED_DEEPSEEK_TOOLS, _tool_approval_modes, _active_tool_preset
|
||||
_tool_approval_modes = {}
|
||||
if not preset_name or preset_name == "None":
|
||||
@@ -564,7 +642,10 @@ def set_tool_preset(preset_name: Optional[str]) -> None:
|
||||
_CACHED_DEEPSEEK_TOOLS = None
|
||||
|
||||
def set_bias_profile(profile_name: Optional[str]) -> None:
|
||||
"""Sets the active tool bias profile for tuning model behavior."""
|
||||
"""
|
||||
Sets the active tool bias profile for tuning model behavior.
|
||||
[C: src/app_controller.py:AppController.init_state, src/gui_2.py:App._render_agent_tools_panel, src/gui_2.py:App._render_persona_selector_panel, src/multi_agent_conductor.py:run_worker_lifecycle]
|
||||
"""
|
||||
global _active_bias_profile
|
||||
if not profile_name or profile_name == "None":
|
||||
_active_bias_profile = None
|
||||
@@ -584,6 +665,9 @@ def get_bias_profile() -> Optional[str]:
|
||||
return _active_bias_profile.name if _active_bias_profile else None
|
||||
|
||||
def _build_anthropic_tools() -> list[dict[str, Any]]:
|
||||
"""
|
||||
[C: tests/test_agent_tools_wiring.py:test_build_anthropic_tools_conversion, tests/test_tool_access_exclusion.py:test_build_anthropic_tools_excludes_disabled]
|
||||
"""
|
||||
raw_tools: list[dict[str, Any]] = []
|
||||
for spec in mcp_client.get_tool_schemas():
|
||||
if _agent_tools.get(spec["name"], True):
|
||||
@@ -622,12 +706,18 @@ def _build_anthropic_tools() -> list[dict[str, Any]]:
|
||||
_CACHED_ANTHROPIC_TOOLS: Optional[list[dict[str, Any]]] = None
|
||||
|
||||
def _get_anthropic_tools() -> list[dict[str, Any]]:
|
||||
"""
|
||||
[C: tests/test_bias_efficacy.py:test_bias_efficacy_prompt_generation, tests/test_bias_efficacy.py:test_bias_parameter_nudging, tests/test_bias_integration.py:test_tool_declaration_biasing_anthropic]
|
||||
"""
|
||||
global _CACHED_ANTHROPIC_TOOLS
|
||||
if _CACHED_ANTHROPIC_TOOLS is None:
|
||||
_CACHED_ANTHROPIC_TOOLS = _build_anthropic_tools()
|
||||
return _CACHED_ANTHROPIC_TOOLS
|
||||
|
||||
def _gemini_tool_declaration() -> Optional[types.Tool]:
|
||||
"""
|
||||
[C: tests/test_tool_access_exclusion.py:test_gemini_tool_declaration_excludes_disabled]
|
||||
"""
|
||||
raw_tools: list[dict[str, Any]] = []
|
||||
for spec in mcp_client.get_tool_schemas():
|
||||
if _agent_tools.get(spec["name"], True):
|
||||
@@ -691,8 +781,10 @@ async def _execute_tool_calls_concurrently(
|
||||
patch_callback: Optional[Callable[[str, str], Optional[str]]] = None
|
||||
) -> list[tuple[str, str, str, str]]: # tool_name, call_id, output, original_name
|
||||
"""
|
||||
Executes multiple tool calls concurrently using asyncio.gather.
|
||||
Returns a list of (tool_name, call_id, output, original_name).
|
||||
|
||||
Executes multiple tool calls concurrently using asyncio.gather.
|
||||
Returns a list of (tool_name, call_id, output, original_name).
|
||||
[C: tests/test_async_tools.py:test_execute_tool_calls_concurrently_exception_handling, tests/test_async_tools.py:test_execute_tool_calls_concurrently_timing]
|
||||
"""
|
||||
monitor = performance_monitor.get_monitor()
|
||||
if monitor.enabled: monitor.start_component("ai_client._execute_tool_calls_concurrently")
|
||||
@@ -739,6 +831,9 @@ async def _execute_single_tool_call_async(
|
||||
tier: str | None = None,
|
||||
patch_callback: Optional[Callable[[str, str], Optional[str]]] = None
|
||||
) -> tuple[str, str, str, str]:
|
||||
"""
|
||||
[C: tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call, tests/test_external_mcp_hitl.py:test_external_mcp_hitl_approval, tests/test_external_mcp_hitl.py:test_external_mcp_hitl_rejection, tests/test_tool_presets_execution.py:test_tool_ask_approval, tests/test_tool_presets_execution.py:test_tool_auto_approval, tests/test_tool_presets_execution.py:test_tool_rejection]
|
||||
"""
|
||||
if tier:
|
||||
set_current_tier(tier)
|
||||
out = ""
|
||||
@@ -870,6 +965,9 @@ def _build_file_diff_text(changed_items: list[dict[str, Any]]) -> str:
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
def _build_deepseek_tools() -> list[dict[str, Any]]:
|
||||
"""
|
||||
[C: tests/test_tool_access_exclusion.py:test_build_deepseek_tools_excludes_disabled]
|
||||
"""
|
||||
raw_tools: list[dict[str, Any]] = []
|
||||
for spec in mcp_client.get_tool_schemas():
|
||||
if _agent_tools.get(spec["name"], True):
|
||||
@@ -936,6 +1034,9 @@ def _content_block_to_dict(block: Any) -> dict[str, Any]:
|
||||
return {"type": "text", "text": str(block)}
|
||||
|
||||
def _ensure_gemini_client() -> None:
|
||||
"""
|
||||
[C: src/rag_engine.py:GeminiEmbeddingProvider.embed]
|
||||
"""
|
||||
global _gemini_client
|
||||
if _gemini_client is None:
|
||||
creds = _load_credentials()
|
||||
@@ -959,6 +1060,9 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str,
|
||||
enable_tools: bool = True,
|
||||
stream_callback: Optional[Callable[[str], None]] = None,
|
||||
patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> str:
|
||||
"""
|
||||
[C: tests/test_tier4_interceptor.py:test_gemini_provider_passes_qa_callback_to_run_script]
|
||||
"""
|
||||
global _gemini_chat, _gemini_cache, _gemini_cache_md_hash, _gemini_cache_created_at, _gemini_cached_file_paths
|
||||
monitor = performance_monitor.get_monitor()
|
||||
if monitor.enabled: monitor.start_component("ai_client._send_gemini")
|
||||
@@ -1399,6 +1503,9 @@ def _ensure_anthropic_client() -> None:
|
||||
)
|
||||
|
||||
def _chunk_text(text: str, chunk_size: int) -> list[str]:
|
||||
"""
|
||||
[C: src/rag_engine.py:RAGEngine._chunk_code, src/rag_engine.py:RAGEngine.index_file]
|
||||
"""
|
||||
return [text[i:i + chunk_size] for i in range(0, len(text), chunk_size)]
|
||||
|
||||
def _build_chunked_context_blocks(md_content: str) -> list[dict[str, Any]]:
|
||||
@@ -2144,6 +2251,9 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str,
|
||||
|
||||
|
||||
def run_tier4_analysis(stderr: str) -> str:
|
||||
"""
|
||||
[C: src/native_orchestrator.py:NativeOrchestrator.analyze_error]
|
||||
"""
|
||||
if not stderr or not stderr.strip():
|
||||
return ""
|
||||
try:
|
||||
@@ -2189,6 +2299,9 @@ def run_tier4_patch_callback(stderr: str, base_dir: str) -> Optional[str]:
|
||||
|
||||
|
||||
def run_tier4_patch_generation(error: str, file_context: str) -> str:
|
||||
"""
|
||||
[C: src/gui_2.py:App.request_patch_from_tier4, src/native_orchestrator.py:NativeOrchestrator.run_tier4_patch, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_calls_ai, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_empty_error, tests/test_tier4_patch_generation.py:test_run_tier4_patch_generation_returns_diff]
|
||||
"""
|
||||
if not error or not error.strip():
|
||||
return ""
|
||||
try:
|
||||
@@ -2215,6 +2328,9 @@ def run_tier4_patch_generation(error: str, file_context: str) -> str:
|
||||
return f"[PATCH GENERATION FAILED] {e}"
|
||||
|
||||
def get_token_stats(md_content: str) -> dict[str, Any]:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._refresh_api_metrics]
|
||||
"""
|
||||
global _provider, _gemini_client, _model, _CHARS_PER_TOKEN
|
||||
total_tokens = 0
|
||||
if _provider == "gemini":
|
||||
@@ -2261,6 +2377,9 @@ def send(
|
||||
patch_callback: Optional[Callable[[str, str], Optional[str]]] = None,
|
||||
rag_engine: Optional[Any] = None,
|
||||
) -> str:
|
||||
"""
|
||||
[C: simulation/user_agent.py:UserSimAgent.generate_response, src/api_hooks.py:WebSocketServer._handler, src/api_hooks.py:WebSocketServer.broadcast, src/app_controller.py:AppController._handle_request_event, src/app_controller.py:AppController.generate, src/conductor_tech_lead.py:generate_tickets, src/multi_agent_conductor.py:run_worker_lifecycle, src/native_orchestrator.py:NativeOrchestrator.execute_ticket, src/orchestrator_pm.py:generate_tracks, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_completion_logic, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoning_logic, tests/test_deepseek_provider.py:test_deepseek_streaming, tests/test_deepseek_provider.py:test_deepseek_tool_calling, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_full_flow_integration, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_captures_usage_metadata, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_handles_tool_use_events, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_parses_jsonl_output, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_starts_subprocess_with_correct_args, tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_send_parses_tool_calls_from_streaming_json, tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_send_starts_subprocess_with_model, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_context_bleed_prevention, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_loop_termination, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_cli_parity_regression.py:test_get_history_bleed_stats, tests/test_gemini_cli_parity_regression.py:test_send_invokes_adapter_send, tests/test_gui2_mcp.py:test_mcp_tool_call_is_dispatched, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_token_usage.py:test_token_usage_tracking, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
monitor = performance_monitor.get_monitor()
|
||||
if monitor.enabled: monitor.start_component("ai_client.send")
|
||||
|
||||
@@ -2307,6 +2426,9 @@ def send(
|
||||
return res
|
||||
|
||||
def _add_bleed_derived(d: dict[str, Any], sys_tok: int = 0, tool_tok: int = 0) -> dict[str, Any]:
|
||||
"""
|
||||
[C: tests/test_token_viz.py:test_add_bleed_derived_aliases, tests/test_token_viz.py:test_add_bleed_derived_breakdown, tests/test_token_viz.py:test_add_bleed_derived_headroom, tests/test_token_viz.py:test_add_bleed_derived_headroom_clamped_to_zero, tests/test_token_viz.py:test_add_bleed_derived_history_clamped_to_zero, tests/test_token_viz.py:test_add_bleed_derived_would_trim_false, tests/test_token_viz.py:test_add_bleed_derived_would_trim_true, tests/test_token_viz.py:test_would_trim_boundary_exact, tests/test_token_viz.py:test_would_trim_just_above_threshold, tests/test_token_viz.py:test_would_trim_just_below_threshold]
|
||||
"""
|
||||
cur = d.get("current", 0)
|
||||
lim = d.get("limit", 0)
|
||||
d["estimated_prompt_tokens"] = cur
|
||||
@@ -2326,6 +2448,9 @@ if os.environ.get("SLOP_TOOL_PRESET"):
|
||||
pass
|
||||
|
||||
def get_history_bleed_stats(md_content: Optional[str] = None) -> dict[str, Any]:
|
||||
"""
|
||||
[C: tests/test_gemini_cli_parity_regression.py:test_get_history_bleed_stats, tests/test_history_management.py:test_get_history_bleed_stats_basic, tests/test_minimax_provider.py:test_minimax_history_bleed_stats, tests/test_token_viz.py:test_get_history_bleed_stats_returns_all_keys_unknown_provider]
|
||||
"""
|
||||
if _provider == "anthropic":
|
||||
with _anthropic_history_lock:
|
||||
history_snapshot = list(_anthropic_history)
|
||||
@@ -2475,7 +2600,10 @@ def get_history_bleed_stats(md_content: Optional[str] = None) -> dict[str, Any]:
|
||||
})
|
||||
|
||||
def run_subagent_summarization(file_path: str, content: str, is_code: bool, outline: str) -> str:
|
||||
"""Performs a stateless summarization request using a sub-agent prompt."""
|
||||
"""
|
||||
Performs a stateless summarization request using a sub-agent prompt.
|
||||
[C: src/summarize.py:summarise_file, tests/test_subagent_summarization.py:test_run_subagent_summarization_anthropic, tests/test_subagent_summarization.py:test_run_subagent_summarization_gemini]
|
||||
"""
|
||||
prompt_tmpl = mma_prompts.TIER4_SUMMARIZE_CODE_PROMPT if is_code else mma_prompts.TIER4_SUMMARIZE_TEXT_PROMPT
|
||||
prompt = prompt_tmpl.format(file_path=file_path, outline=outline, content=content)
|
||||
if _provider == "gemini":
|
||||
@@ -2522,4 +2650,3 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl
|
||||
resp_data = adapter.send(prompt, model=_model)
|
||||
return resp_data.get("text", "")
|
||||
return "ERROR: Unsupported provider for sub-agent summarization"
|
||||
|
||||
|
||||
+138
-35
@@ -38,11 +38,17 @@ from typing import Any
|
||||
|
||||
class ApiHookClient:
|
||||
def __init__(self, base_url: str = "http://127.0.0.1:8999", api_key: str | None = None):
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_key = api_key
|
||||
|
||||
def _make_request(self, method: str, path: str, data: dict | None = None, timeout: float = 5.0) -> dict[str, Any] | None:
|
||||
"""Helper to make HTTP requests to the hook server."""
|
||||
"""
|
||||
Helper to make HTTP requests to the hook server.
|
||||
[C: tests/test_api_hook_client.py:test_unsupported_method_error]
|
||||
"""
|
||||
url = f"{self.base_url}{path}"
|
||||
headers = {}
|
||||
if self.api_key:
|
||||
@@ -64,7 +70,10 @@ class ApiHookClient:
|
||||
return None
|
||||
|
||||
def wait_for_server(self, timeout: int = 15) -> bool:
|
||||
"""Polls the health endpoint until the server responds or timeout occurs."""
|
||||
"""
|
||||
Polls the health endpoint until the server responds or timeout occurs.
|
||||
[C: simulation/live_walkthrough.py:main, simulation/ping_pong.py:main, simulation/sim_base.py:BaseSimulation.setup, tests/smoke_status_hook.py:test_status_hook, tests/test_ai_settings_layout.py:test_change_provider_via_hook, tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_auto_switch_sim.py:test_auto_switch_sim, tests/test_conductor_api_hook_integration.py:test_conductor_integrates_api_hook_client_for_verification, tests/test_deepseek_infra.py:test_gui_provider_list_via_hooks, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:test_button_click_is_received, tests/test_external_editor_gui.py:test_patch_modal_shows_with_configured_editor, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui2_parity.py:test_gui2_click_hook_works, tests/test_gui2_parity.py:test_gui2_custom_callback_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_gui_context_presets.py:test_gui_context_preset_save_load, tests/test_hooks.py:test_live_hook_server_responses, tests/test_live_workflow.py:test_full_live_workflow, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow, tests/test_patch_modal_gui.py:test_patch_apply_modal_workflow, tests/test_patch_modal_gui.py:test_patch_modal_appears_on_trigger, tests/test_preset_windows_layout.py:test_api_hook_under_load, tests/test_preset_windows_layout.py:test_preset_windows_opening, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_rag_visual_sim.py:test_rag_full_lifecycle_sim, tests/test_rag_visual_sim.py:test_rag_settings_persistence_sim, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_system_prompt_sim.py:test_system_prompt_sim, tests/test_tool_management_layout.py:test_tool_management_gettable_fields, tests/test_tool_management_layout.py:test_tool_management_state_updates, tests/test_ui_cache_controls_sim.py:test_ui_cache_controls, tests/test_undo_redo_sim.py:test_undo_redo_context_mutation, tests/test_undo_redo_sim.py:test_undo_redo_discussion_mutation, tests/test_undo_redo_sim.py:test_undo_redo_lifecycle, tests/test_visual_orchestration.py:test_mma_epic_lifecycle, tests/test_visual_sim_gui_ux.py:test_gui_track_creation, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing, tests/test_visual_sim_mma_v2.py:test_mma_complete_lifecycle, tests/test_workspace_profiles_sim.py:test_workspace_profiles_restoration, tests/test_z_negative_flows.py:test_mock_error_result, tests/test_z_negative_flows.py:test_mock_malformed_json, tests/test_z_negative_flows.py:test_mock_timeout]
|
||||
"""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
status = self.get_status()
|
||||
@@ -74,7 +83,10 @@ class ApiHookClient:
|
||||
return False
|
||||
|
||||
def get_status(self) -> dict[str, Any]:
|
||||
"""Checks the health of the hook server."""
|
||||
"""
|
||||
Checks the health of the hook server.
|
||||
[C: tests/test_api_hook_client.py:test_get_status_success, tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_hooks.py:test_live_hook_server_responses, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_preset_windows_layout.py:make_request, tests/test_preset_windows_layout.py:test_preset_windows_opening, tests/test_ui_cache_controls_sim.py:test_ui_cache_controls]
|
||||
"""
|
||||
res = self._make_request('GET', '/status')
|
||||
if res is None:
|
||||
# For backward compatibility with tests expecting ConnectionError
|
||||
@@ -83,19 +95,31 @@ class ApiHookClient:
|
||||
return res
|
||||
|
||||
def post_project(self, project_data: dict) -> dict[str, Any]:
|
||||
"""Updates the current project configuration."""
|
||||
"""
|
||||
Updates the current project configuration.
|
||||
[C: simulation/sim_context.py:ContextSimulation.run]
|
||||
"""
|
||||
return self._make_request('POST', '/api/project', data=project_data) or {}
|
||||
|
||||
def get_project(self) -> dict[str, Any]:
|
||||
"""Retrieves the current project state."""
|
||||
"""
|
||||
Retrieves the current project state.
|
||||
[C: simulation/sim_context.py:ContextSimulation.run, tests/test_api_hook_client.py:test_get_project_success, tests/test_gui_context_presets.py:test_gui_context_preset_save_load, tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_hooks.py:test_live_hook_server_responses, tests/test_live_workflow.py:test_full_live_workflow]
|
||||
"""
|
||||
return self._make_request('GET', '/api/project') or {}
|
||||
|
||||
def get_session(self) -> dict[str, Any]:
|
||||
"""Retrieves the current discussion session history."""
|
||||
"""
|
||||
Retrieves the current discussion session history.
|
||||
[C: simulation/ping_pong.py:main, simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/sim_tools.py:ToolsSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/test_api_hook_client.py:test_get_session_success, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_live_workflow.py:test_full_live_workflow, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim]
|
||||
"""
|
||||
return self._make_request('GET', '/api/session') or {}
|
||||
|
||||
def post_session(self, session_entries: list[dict]) -> dict[str, Any]:
|
||||
"""Updates the session history."""
|
||||
"""
|
||||
Updates the session history.
|
||||
[C: tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_live_workflow.py:test_full_live_workflow]
|
||||
"""
|
||||
return self._make_request('POST', '/api/session', data={"session": {"entries": session_entries}}) or {}
|
||||
|
||||
def get_events(self) -> list[dict[str, Any]]:
|
||||
@@ -104,11 +128,17 @@ class ApiHookClient:
|
||||
return res.get("events", []) if res else []
|
||||
|
||||
def clear_events(self) -> list[dict[str, Any]]:
|
||||
"""Retrieves and clears the event queue."""
|
||||
"""
|
||||
Retrieves and clears the event queue.
|
||||
[C: simulation/sim_base.py:BaseSimulation.setup]
|
||||
"""
|
||||
return self.get_events()
|
||||
|
||||
|
||||
def wait_for_event(self, event_type: str, timeout: int = 5) -> dict[str, Any] | None:
|
||||
"""
|
||||
[C: simulation/sim_base.py:BaseSimulation.wait_for_event, simulation/sim_execution.py:ExecutionSimulation.run, tests/test_z_negative_flows.py:test_mock_error_result, tests/test_z_negative_flows.py:test_mock_malformed_json, tests/test_z_negative_flows.py:test_mock_timeout]
|
||||
"""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
events = self.get_events()
|
||||
@@ -119,35 +149,59 @@ class ApiHookClient:
|
||||
return None
|
||||
|
||||
def post_gui(self, payload: dict) -> dict[str, Any]:
|
||||
"""Pushes an event to the GUI's AsyncEventQueue via the /api/gui endpoint."""
|
||||
"""
|
||||
Pushes an event to the GUI's AsyncEventQueue via the /api/gui endpoint.
|
||||
[C: tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_api_hook_client.py:test_post_gui_success, tests/test_gui2_parity.py:test_gui2_custom_callback_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_visual_mma.py:test_visual_mma_components]
|
||||
"""
|
||||
return self._make_request('POST', '/api/gui', data=payload) or {}
|
||||
|
||||
def push_event(self, action: str, payload: dict) -> dict[str, Any]:
|
||||
"""Convenience to push a GUI task."""
|
||||
"""
|
||||
Convenience to push a GUI task.
|
||||
[C: tests/test_auto_switch_sim.py:test_auto_switch_sim, tests/test_auto_switch_sim.py:trigger_tier, tests/test_external_editor_gui.py:test_button_click_is_received, tests/test_external_editor_gui.py:test_patch_modal_shows_with_configured_editor, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_context_presets.py:test_gui_context_preset_save_load, tests/test_gui_text_viewer.py:test_text_viewer_state_update, tests/test_patch_modal_gui.py:test_patch_apply_modal_workflow, tests/test_patch_modal_gui.py:test_patch_modal_appears_on_trigger, tests/test_preset_windows_layout.py:test_preset_windows_opening, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_saved_presets_sim.py:test_preset_switching, tests/test_tool_management_layout.py:test_tool_management_state_updates, tests/test_tool_presets_sim.py:test_tool_preset_switching, tests/test_visual_mma.py:test_visual_mma_components, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing, tests/test_workspace_profiles_sim.py:test_workspace_profiles_restoration, tests/test_z_negative_flows.py:test_mock_error_result, tests/test_z_negative_flows.py:test_mock_malformed_json, tests/test_z_negative_flows.py:test_mock_timeout]
|
||||
"""
|
||||
return self.post_gui({"action": action, **payload})
|
||||
|
||||
def click(self, item: str, user_data: Any = None) -> dict[str, Any]:
|
||||
"""Simulates a button click."""
|
||||
"""
|
||||
Simulates a button click.
|
||||
[C: simulation/live_walkthrough.py:main, simulation/ping_pong.py:main, simulation/sim_base.py:BaseSimulation.setup, simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.create_discussion, simulation/workflow_sim.py:WorkflowSimulator.load_prior_log, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.setup_new_project, simulation/workflow_sim.py:WorkflowSimulator.truncate_history, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/test_external_editor_gui.py:test_button_click_is_received, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui2_parity.py:test_gui2_click_hook_works, tests/test_live_workflow.py:test_full_live_workflow, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_rag_visual_sim.py:test_rag_full_lifecycle_sim, tests/test_system_prompt_sim.py:test_system_prompt_sim, tests/test_ui_cache_controls_sim.py:test_ui_cache_controls, tests/test_undo_redo_sim.py:test_undo_redo_context_mutation, tests/test_undo_redo_sim.py:test_undo_redo_discussion_mutation, tests/test_undo_redo_sim.py:test_undo_redo_lifecycle, tests/test_visual_orchestration.py:test_mma_epic_lifecycle, tests/test_visual_sim_gui_ux.py:test_gui_track_creation, tests/test_visual_sim_mma_v2.py:_drain_approvals, tests/test_visual_sim_mma_v2.py:test_mma_complete_lifecycle, tests/test_z_negative_flows.py:test_mock_error_result, tests/test_z_negative_flows.py:test_mock_malformed_json, tests/test_z_negative_flows.py:test_mock_timeout]
|
||||
"""
|
||||
return self.post_gui({"action": "click", "item": item, "user_data": user_data})
|
||||
|
||||
def set_value(self, item: str, value: Any) -> dict[str, Any]:
|
||||
"""Sets the value of a GUI widget."""
|
||||
"""
|
||||
Sets the value of a GUI widget.
|
||||
[C: simulation/live_walkthrough.py:main, simulation/ping_pong.py:main, simulation/sim_ai_settings.py:AISettingsSimulation.run, simulation/sim_base.py:BaseSimulation.setup, simulation/workflow_sim.py:WorkflowSimulator.create_discussion, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.setup_new_project, simulation/workflow_sim.py:WorkflowSimulator.truncate_history, tests/smoke_status_hook.py:test_status_hook, tests/test_ai_settings_layout.py:test_change_provider_via_hook, tests/test_auto_switch_sim.py:test_auto_switch_sim, tests/test_deepseek_infra.py:test_gui_provider_list_via_hooks, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_gui2_parity.py:test_gui2_click_hook_works, tests/test_gui2_performance.py:test_performance_benchmarking, tests/test_live_gui_integration_v2.py:test_api_gui_state_live, tests/test_live_workflow.py:test_full_live_workflow, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_rag_visual_sim.py:test_rag_full_lifecycle_sim, tests/test_rag_visual_sim.py:test_rag_settings_persistence_sim, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_system_prompt_sim.py:test_system_prompt_sim, tests/test_task_dag_popout_sim.py:test_task_dag_popout, tests/test_tool_presets_sim.py:test_tool_preset_switching, tests/test_undo_redo_sim.py:test_undo_redo_context_mutation, tests/test_undo_redo_sim.py:test_undo_redo_discussion_mutation, tests/test_undo_redo_sim.py:test_undo_redo_lifecycle, tests/test_usage_analytics_popout_sim.py:test_usage_analytics_popout, tests/test_visual_orchestration.py:test_mma_epic_lifecycle, tests/test_visual_sim_gui_ux.py:test_gui_track_creation, tests/test_visual_sim_mma_v2.py:test_mma_complete_lifecycle, tests/test_workspace_profiles_sim.py:test_workspace_profiles_restoration, tests/test_z_negative_flows.py:test_mock_error_result, tests/test_z_negative_flows.py:test_mock_malformed_json, tests/test_z_negative_flows.py:test_mock_timeout]
|
||||
"""
|
||||
return self.post_gui({"action": "set_value", "item": item, "value": value})
|
||||
|
||||
def select_tab(self, item: str, value: str) -> dict[str, Any]:
|
||||
"""Selects a specific tab in a tab bar."""
|
||||
"""
|
||||
Selects a specific tab in a tab bar.
|
||||
[C: simulation/live_walkthrough.py:main, tests/test_api_hook_extensions.py:test_select_tab_integration]
|
||||
"""
|
||||
return self.set_value(item, value)
|
||||
|
||||
def select_list_item(self, item: str, value: str) -> dict[str, Any]:
|
||||
"""Selects an item in a listbox or combo."""
|
||||
"""
|
||||
Selects an item in a listbox or combo.
|
||||
[C: simulation/workflow_sim.py:WorkflowSimulator.create_discussion, simulation/workflow_sim.py:WorkflowSimulator.switch_discussion, tests/test_api_hook_extensions.py:test_select_list_item_integration, tests/test_live_workflow.py:test_full_live_workflow]
|
||||
"""
|
||||
return self.set_value(item, value)
|
||||
|
||||
def get_gui_state(self) -> dict[str, Any]:
|
||||
"""Returns the full GUI state available via the hook API."""
|
||||
"""
|
||||
Returns the full GUI state available via the hook API.
|
||||
[C: tests/test_ai_settings_layout.py:test_change_provider_via_hook, tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_conductor_api_hook_integration.py:simulate_conductor_phase_completion, tests/test_external_editor_gui.py:test_button_click_is_received, tests/test_external_editor_gui.py:test_patch_modal_shows_with_configured_editor, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_text_viewer.py:test_text_viewer_state_update, tests/test_hooks.py:test_live_hook_server_responses, tests/test_live_gui_integration_v2.py:test_api_gui_state_live, tests/test_live_workflow.py:test_full_live_workflow, tests/test_live_workflow.py:wait_for_value, tests/test_patch_modal_gui.py:test_patch_apply_modal_workflow, tests/test_patch_modal_gui.py:test_patch_modal_appears_on_trigger, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_saved_presets_sim.py:test_preset_switching, tests/test_task_dag_popout_sim.py:test_task_dag_popout, tests/test_tool_management_layout.py:test_tool_management_gettable_fields, tests/test_tool_management_layout.py:test_tool_management_state_updates, tests/test_tool_presets_sim.py:test_tool_preset_switching, tests/test_usage_analytics_popout_sim.py:test_usage_analytics_popout]
|
||||
"""
|
||||
return self._make_request('GET', '/api/gui/state') or {}
|
||||
|
||||
def get_value(self, item: str) -> Any:
|
||||
"""Gets the value of a GUI item via its mapped field."""
|
||||
"""
|
||||
Gets the value of a GUI item via its mapped field.
|
||||
[C: simulation/sim_ai_settings.py:AISettingsSimulation.run, simulation/sim_base.py:BaseSimulation.get_value, simulation/sim_base.py:BaseSimulation.setup, simulation/sim_base.py:BaseSimulation.wait_for_element, simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/smoke_status_hook.py:test_status_hook, tests/smoke_status_hook.py:wait_for_value, tests/test_auto_switch_sim.py:test_auto_switch_sim, tests/test_deepseek_infra.py:test_gui_provider_list_via_hooks, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_gui2_parity.py:test_gui2_click_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_rag_visual_sim.py:test_rag_full_lifecycle_sim, tests/test_rag_visual_sim.py:test_rag_settings_persistence_sim, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_system_prompt_sim.py:test_system_prompt_sim, tests/test_undo_redo_sim.py:test_undo_redo_context_mutation, tests/test_undo_redo_sim.py:test_undo_redo_discussion_mutation, tests/test_undo_redo_sim.py:test_undo_redo_lifecycle, tests/test_visual_mma.py:test_visual_mma_components, tests/test_workspace_profiles_sim.py:test_workspace_profiles_restoration]
|
||||
"""
|
||||
# Try state endpoint first (new preferred way)
|
||||
state = self.get_gui_state()
|
||||
if item in state:
|
||||
@@ -168,33 +222,54 @@ class ApiHookClient:
|
||||
return None
|
||||
|
||||
def get_text_value(self, item_tag: str) -> str | None:
|
||||
"""Wraps get_value and returns its string representation, or None."""
|
||||
"""
|
||||
Wraps get_value and returns its string representation, or None.
|
||||
[C: tests/test_api_hook_client.py:test_get_text_value]
|
||||
"""
|
||||
val = self.get_value(item_tag)
|
||||
return str(val) if val is not None else None
|
||||
|
||||
def get_indicator_state(self, item_tag: str) -> dict[str, bool]:
|
||||
"""Returns the visibility/active state of a status indicator."""
|
||||
"""
|
||||
Returns the visibility/active state of a status indicator.
|
||||
[C: simulation/live_walkthrough.py:main, tests/test_api_hook_extensions.py:test_get_indicator_state_integration, tests/test_live_workflow.py:test_full_live_workflow]
|
||||
"""
|
||||
val = self.get_value(item_tag)
|
||||
return {"shown": bool(val)}
|
||||
|
||||
def get_gui_diagnostics(self) -> dict[str, Any]:
|
||||
"""Retrieves performance and diagnostic metrics."""
|
||||
"""
|
||||
Retrieves performance and diagnostic metrics.
|
||||
[C: tests/test_api_hook_client.py:test_get_performance_success, tests/test_hooks.py:test_live_hook_server_responses, tests/test_selectable_ui.py:test_selectable_label_stability]
|
||||
"""
|
||||
return self._make_request('GET', '/api/gui/diagnostics') or {}
|
||||
|
||||
def get_performance(self) -> dict[str, Any]:
|
||||
"""Retrieves performance metrics from the dedicated endpoint."""
|
||||
"""
|
||||
Retrieves performance metrics from the dedicated endpoint.
|
||||
[C: tests/test_gui2_performance.py:test_performance_benchmarking, tests/test_gui_performance_requirements.py:test_idle_performance_requirements, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing]
|
||||
"""
|
||||
return self._make_request('GET', '/api/performance') or {}
|
||||
|
||||
def get_mma_status(self) -> dict[str, Any]:
|
||||
"""Retrieves the dedicated MMA engine status."""
|
||||
"""
|
||||
Retrieves the dedicated MMA engine status.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_live_workflow.py:test_full_live_workflow, tests/test_mma_concurrent_tracks_sim.py:_poll_mma_status, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_mma_step_mode_sim.py:_poll_mma_status, tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow, tests/test_visual_orchestration.py:test_mma_epic_lifecycle, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing, tests/test_visual_sim_mma_v2.py:_poll]
|
||||
"""
|
||||
return self._make_request('GET', '/api/gui/mma_status') or {}
|
||||
|
||||
def get_mma_workers(self) -> dict[str, Any]:
|
||||
"""Retrieves status for all active MMA workers."""
|
||||
"""
|
||||
Retrieves status for all active MMA workers.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:_poll_mma_workers]
|
||||
"""
|
||||
return self._make_request('GET', '/api/mma/workers') or {}
|
||||
|
||||
def get_context_state(self) -> dict[str, Any]:
|
||||
"""Retrieves the current file and screenshot context state."""
|
||||
"""
|
||||
Retrieves the current file and screenshot context state.
|
||||
[C: tests/test_gui_context_presets.py:test_gui_context_preset_save_load]
|
||||
"""
|
||||
return self._make_request('GET', '/api/context/state') or {}
|
||||
|
||||
def get_financial_metrics(self) -> dict[str, Any]:
|
||||
@@ -206,13 +281,18 @@ class ApiHookClient:
|
||||
return self._make_request('GET', '/api/system/telemetry') or {}
|
||||
|
||||
def get_node_status(self, node_id: str) -> dict[str, Any]:
|
||||
"""Retrieves status for a specific node in the MMA DAG."""
|
||||
"""
|
||||
Retrieves status for a specific node in the MMA DAG.
|
||||
[C: tests/test_api_hook_client.py:test_get_node_status]
|
||||
"""
|
||||
return self._make_request('GET', f'/api/mma/node/{node_id}') or {}
|
||||
|
||||
def request_confirmation(self, tool_name: str, args: dict) -> bool | None:
|
||||
"""
|
||||
Pushes a manual confirmation request and waits for response.
|
||||
Blocks for up to 60 seconds.
|
||||
|
||||
Pushes a manual confirmation request and waits for response.
|
||||
Blocks for up to 60 seconds.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_sync_hooks.py:test_api_ask_client_error, tests/test_sync_hooks.py:test_api_ask_client_method, tests/test_sync_hooks.py:test_api_ask_client_rejection]
|
||||
"""
|
||||
# Long timeout as this waits for human input (60 seconds)
|
||||
res = self._make_request('POST', '/api/ask',
|
||||
@@ -221,7 +301,10 @@ class ApiHookClient:
|
||||
return res.get('response') if res else None
|
||||
|
||||
def reset_session(self) -> None:
|
||||
"""Resets the current session via button click."""
|
||||
"""
|
||||
Resets the current session via button click.
|
||||
[C: src/app_controller.py:AppController._handle_reset_session, src/app_controller.py:AppController.current_model, src/app_controller.py:AppController.current_provider, src/app_controller.py:AppController.init_state, src/gui_2.py:App._render_provider_panel, src/gui_2.py:App._show_menus, src/multi_agent_conductor.py:run_worker_lifecycle, tests/conftest.py:live_gui, tests/conftest.py:reset_ai_client, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_metrics.py:test_get_gemini_cache_stats_with_mock_client, tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_minimax_provider.py:test_minimax_history_bleed_stats, tests/test_mma_agent_focus_phase1.py:test_append_comms_has_source_tier_key, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_none_when_unset, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_set_when_current_tier_set, tests/test_mma_agent_focus_phase1.py:test_append_comms_source_tier_tier2, tests/test_session_logger_reset.py:test_reset_session, tests/test_token_usage.py:test_token_usage_tracking]
|
||||
"""
|
||||
self.click("btn_reset")
|
||||
|
||||
def trigger_patch(self, patch_text: str, file_paths: list[str]) -> dict[str, Any]:
|
||||
@@ -232,11 +315,17 @@ class ApiHookClient:
|
||||
}) or {}
|
||||
|
||||
def apply_patch(self) -> dict[str, Any]:
|
||||
"""Applies the pending patch."""
|
||||
"""
|
||||
Applies the pending patch.
|
||||
[C: tests/test_patch_modal.py:test_apply_callback]
|
||||
"""
|
||||
return self._make_request('POST', '/api/patch/apply') or {}
|
||||
|
||||
def reject_patch(self) -> dict[str, Any]:
|
||||
"""Rejects the pending patch."""
|
||||
"""
|
||||
Rejects the pending patch.
|
||||
[C: tests/test_patch_modal.py:test_reject_callback, tests/test_patch_modal.py:test_reject_patch]
|
||||
"""
|
||||
return self._make_request('POST', '/api/patch/reject') or {}
|
||||
|
||||
def get_patch_status(self) -> dict[str, Any]:
|
||||
@@ -244,7 +333,10 @@ class ApiHookClient:
|
||||
return self._make_request('GET', '/api/patch/status') or {}
|
||||
|
||||
def spawn_mma_worker(self, data: dict) -> dict:
|
||||
"""Spawns a new MMA worker with the provided configuration."""
|
||||
"""
|
||||
Spawns a new MMA worker with the provided configuration.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation]
|
||||
"""
|
||||
return self._make_request('POST', '/api/mma/workers/spawn', data=data) or {}
|
||||
|
||||
def kill_mma_worker(self, worker_id: str) -> dict:
|
||||
@@ -252,7 +344,10 @@ class ApiHookClient:
|
||||
return self._make_request('POST', '/api/mma/workers/kill', data={"worker_id": worker_id}) or {}
|
||||
|
||||
def pause_mma_pipeline(self) -> dict:
|
||||
"""Pauses the MMA execution pipeline."""
|
||||
"""
|
||||
Pauses the MMA execution pipeline.
|
||||
[C: tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow]
|
||||
"""
|
||||
return self._make_request('POST', '/api/mma/pipeline/pause') or {}
|
||||
|
||||
def resume_mma_pipeline(self) -> dict:
|
||||
@@ -260,14 +355,22 @@ class ApiHookClient:
|
||||
return self._make_request('POST', '/api/mma/pipeline/resume') or {}
|
||||
|
||||
def inject_context(self, data: dict) -> dict:
|
||||
"""Injects custom file context into the application."""
|
||||
"""
|
||||
Injects custom file context into the application.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation]
|
||||
"""
|
||||
return self._make_request('POST', '/api/context/inject', data=data) or {}
|
||||
|
||||
def mutate_mma_dag(self, data: dict) -> dict:
|
||||
"""Mutates the MMA DAG (Directed Acyclic Graph) structure."""
|
||||
"""
|
||||
Mutates the MMA DAG (Directed Acyclic Graph) structure.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation]
|
||||
"""
|
||||
return self._make_request('POST', '/api/mma/dag/mutate', data=data) or {}
|
||||
|
||||
def approve_mma_ticket(self, ticket_id: str) -> dict:
|
||||
"""Manually approves a specific ticket for execution in Step Mode."""
|
||||
"""
|
||||
Manually approves a specific ticket for execution in Step Mode.
|
||||
[C: tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow]
|
||||
"""
|
||||
return self._make_request('POST', '/api/mma/ticket/approve', data={"ticket_id": ticket_id}) or {}
|
||||
|
||||
|
||||
+40
-2
@@ -69,7 +69,10 @@ def _set_app_attr(app: Any, name: str, value: Any) -> None:
|
||||
class HookServerInstance(ThreadingHTTPServer):
|
||||
"""Custom HTTPServer that carries a reference to the main App instance."""
|
||||
def __init__(self, server_address: tuple[str, int], RequestHandlerClass: type, app: Any) -> None:
|
||||
"""Initializes the server instance with an app reference."""
|
||||
"""
|
||||
Initializes the server instance with an app reference.
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
super().__init__(server_address, RequestHandlerClass)
|
||||
self.app = app
|
||||
|
||||
@@ -410,6 +413,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
event = threading.Event()
|
||||
result = {"status": "done"}
|
||||
def apply_patch():
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_apply_callback]
|
||||
"""
|
||||
try:
|
||||
if hasattr(app, "_apply_pending_patch"):
|
||||
app._apply_pending_patch()
|
||||
@@ -436,6 +442,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
event = threading.Event()
|
||||
result = {"status": "done"}
|
||||
def reject_patch():
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_reject_callback, tests/test_patch_modal.py:test_reject_patch]
|
||||
"""
|
||||
try:
|
||||
app._show_patch_modal = False
|
||||
app._pending_patch_text = None
|
||||
@@ -552,6 +561,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(json.dumps({"status": "queued"}).encode("utf-8"))
|
||||
elif self.path == "/api/mma/workers/kill":
|
||||
def kill_worker():
|
||||
"""
|
||||
[C: src/app_controller.py:AppController.kill_worker, src/gui_2.py:App._cb_kill_ticket, tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread]
|
||||
"""
|
||||
try:
|
||||
worker_id = data.get("worker_id")
|
||||
func = _get_app_attr(app, "_kill_worker")
|
||||
@@ -591,6 +603,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(json.dumps({"status": "queued"}).encode("utf-8"))
|
||||
elif self.path == "/api/context/inject":
|
||||
def inject_context():
|
||||
"""
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation]
|
||||
"""
|
||||
files = _get_app_attr(app, "files")
|
||||
if isinstance(files, list):
|
||||
files.extend(data.get("files", []))
|
||||
@@ -650,6 +665,9 @@ class HookHandler(BaseHTTPRequestHandler):
|
||||
|
||||
class HookServer:
|
||||
def __init__(self, app: Any, port: int = 8999) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.app = app
|
||||
self.port = port
|
||||
self.server = None
|
||||
@@ -657,6 +675,9 @@ class HookServer:
|
||||
self.websocket_server: WebSocketServer | None = None
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._cb_accept_tracks, src/app_controller.py:AppController._cb_plan_epic, src/app_controller.py:AppController._cb_start_track, src/app_controller.py:AppController._fetch_models, src/app_controller.py:AppController._handle_approve_ask, src/app_controller.py:AppController._handle_generate_send, src/app_controller.py:AppController._handle_md_only, src/app_controller.py:AppController._handle_reject_ask, src/app_controller.py:AppController._init_ai_and_hooks, src/app_controller.py:AppController._process_event_queue, src/app_controller.py:AppController._prune_old_logs, src/app_controller.py:AppController._rebuild_rag_index, src/app_controller.py:AppController._run_event_loop, src/app_controller.py:AppController._start_track_logic, src/app_controller.py:AppController.cb_prune_logs, src/app_controller.py:AppController.start_services, src/gui_2.py:App._render_discussion_panel, src/mcp_client.py:ExternalMCPManager.add_server, src/multi_agent_conductor.py:WorkerPool.spawn, src/performance_monitor.py:PerformanceMonitor.__init__, tests/test_ai_client_concurrency.py:test_ai_client_tier_isolation, tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread, tests/test_conductor_engine_v2.py:side_effect, tests/test_spawn_interception_v2.py:test_confirm_spawn_pushed_to_queue, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
if self.thread and self.thread.is_alive():
|
||||
return
|
||||
is_gemini_cli = _get_app_attr(self.app, 'current_provider', '') == 'gemini_cli'
|
||||
@@ -682,6 +703,9 @@ class HookServer:
|
||||
logging.info(f"Hook server started on port {self.port}")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController.shutdown, src/mcp_client.py:ExternalMCPManager.stop_all, tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
if self.websocket_server:
|
||||
self.websocket_server.stop()
|
||||
if self.server:
|
||||
@@ -694,6 +718,9 @@ class HookServer:
|
||||
class WebSocketServer:
|
||||
"""WebSocket gateway for real-time event streaming."""
|
||||
def __init__(self, app: Any, port: int = 9000) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.app = app
|
||||
self.port = port
|
||||
self.clients: dict[str, set] = {"events": set(), "telemetry": set()}
|
||||
@@ -726,27 +753,38 @@ class WebSocketServer:
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self._stop_event = asyncio.Event()
|
||||
async def main():
|
||||
"""
|
||||
[C: simulation/live_walkthrough.py:module, simulation/ping_pong.py:module, src/gui_2.py:module, tests/mock_concurrent_mma.py:module, tests/mock_gemini_cli.py:module, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_allow_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_deny_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_unreachable_hook_server, tests/test_cli_tool_bridge.py:module, tests/test_cli_tool_bridge_mapping.py:TestCliToolBridgeMapping.test_mapping_from_api_format, tests/test_cli_tool_bridge_mapping.py:module, tests/test_discussion_takes.py:module, tests/test_external_editor_gui.py:module, tests/test_headless_service.py:TestHeadlessStartup.test_headless_flag_triggers_run, tests/test_headless_service.py:TestHeadlessStartup.test_normal_startup_calls_app_run, tests/test_mma_skeleton.py:module, tests/test_orchestrator_pm.py:module, tests/test_orchestrator_pm_history.py:module, tests/test_post_process.py:module, tests/test_presets.py:module, tests/test_project_serialization.py:module, tests/test_run_worker_lifecycle_abort.py:module, tests/test_symbol_lookup.py:module, tests/test_system_prompt_exposure.py:module, tests/test_theme_nerv_fx.py:module]
|
||||
"""
|
||||
async with serve(self._handler, "127.0.0.1", self.port) as server:
|
||||
self.server = server
|
||||
await self._stop_event.wait()
|
||||
self.loop.run_until_complete(main())
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._cb_accept_tracks, src/app_controller.py:AppController._cb_plan_epic, src/app_controller.py:AppController._cb_start_track, src/app_controller.py:AppController._fetch_models, src/app_controller.py:AppController._handle_approve_ask, src/app_controller.py:AppController._handle_generate_send, src/app_controller.py:AppController._handle_md_only, src/app_controller.py:AppController._handle_reject_ask, src/app_controller.py:AppController._init_ai_and_hooks, src/app_controller.py:AppController._process_event_queue, src/app_controller.py:AppController._prune_old_logs, src/app_controller.py:AppController._rebuild_rag_index, src/app_controller.py:AppController._run_event_loop, src/app_controller.py:AppController._start_track_logic, src/app_controller.py:AppController.cb_prune_logs, src/app_controller.py:AppController.start_services, src/gui_2.py:App._render_discussion_panel, src/mcp_client.py:ExternalMCPManager.add_server, src/multi_agent_conductor.py:WorkerPool.spawn, src/performance_monitor.py:PerformanceMonitor.__init__, tests/test_ai_client_concurrency.py:test_ai_client_tier_isolation, tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread, tests/test_conductor_engine_v2.py:side_effect, tests/test_spawn_interception_v2.py:test_confirm_spawn_pushed_to_queue, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
if self.thread and self.thread.is_alive():
|
||||
return
|
||||
self.thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController.shutdown, src/mcp_client.py:ExternalMCPManager.stop_all, tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
if self.loop and self._stop_event:
|
||||
self.loop.call_soon_threadsafe(self._stop_event.set)
|
||||
if self.thread:
|
||||
self.thread.join(timeout=2.0)
|
||||
|
||||
def broadcast(self, channel: str, payload: dict[str, Any]) -> None:
|
||||
"""
|
||||
[C: src/app_controller.py:AppController._process_pending_gui_tasks, src/events.py:AsyncEventQueue.put, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
if not self.loop or channel not in self.clients:
|
||||
return
|
||||
message = json.dumps({"channel": channel, "payload": payload})
|
||||
for ws in list(self.clients[channel]):
|
||||
asyncio.run_coroutine_threadsafe(ws.send(message), self.loop)
|
||||
|
||||
|
||||
+111
-89
@@ -43,7 +43,7 @@ from src import theme_2 as theme
|
||||
|
||||
def hide_tk_root() -> Tk:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_files_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._render_provider_panel, src/gui_2.py:App._render_screenshots_panel, src/gui_2.py:App._render_theme_panel, src/gui_2.py:App.render_path_field]
|
||||
[C: src/gui_2.py:App._render_files_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._render_provider_panel, src/gui_2.py:App._render_screenshots_panel, src/gui_2.py:App._render_theme_panel, src/gui_2.py:App.render_path_field]
|
||||
"""
|
||||
root = Tk()
|
||||
root.withdraw()
|
||||
@@ -53,15 +53,16 @@ def hide_tk_root() -> Tk:
|
||||
def parse_symbols(text: str) -> list[str]:
|
||||
"""
|
||||
|
||||
Finds all occurrences of '@SymbolName' in text and returns SymbolName.
|
||||
SymbolName can be a function, class, or method (e.g. @MyClass, @my_func, @MyClass.my_method).
|
||||
|
||||
Finds all occurrences of '@SymbolName' in text and returns SymbolName.
|
||||
SymbolName can be a function, class, or method (e.g. @MyClass, @my_func, @MyClass.my_method).
|
||||
[C: tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_basic, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_edge_cases, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_methods, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_mixed, tests/test_symbol_lookup.py:TestSymbolLookup.test_parse_symbols_no_symbols]
|
||||
"""
|
||||
return re.findall(r"@([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)", text)
|
||||
|
||||
def get_symbol_definition(symbol: str, files: list[str]) -> tuple[str, str, int] | None:
|
||||
"""
|
||||
[C: tests/test_symbol_lookup.py:TestSymbolLookup.test_get_symbol_definition_found, tests/test_symbol_lookup.py:TestSymbolLookup.test_get_symbol_definition_not_found]
|
||||
[C: tests/test_symbol_lookup.py:TestSymbolLookup.test_get_symbol_definition_found, tests/test_symbol_lookup.py:TestSymbolLookup.test_get_symbol_definition_not_found]
|
||||
"""
|
||||
for file_path in files:
|
||||
result = mcp_client.py_get_symbol_info(file_path, symbol)
|
||||
@@ -83,7 +84,7 @@ class ConfirmRequest(BaseModel):
|
||||
class ConfirmDialog:
|
||||
def __init__(self, script: str, base_dir: str) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self._uid = str(uuid.uuid4())
|
||||
self._script = str(script) if script is not None else ""
|
||||
@@ -94,7 +95,7 @@ class ConfirmDialog:
|
||||
|
||||
def wait(self) -> tuple[bool, str]:
|
||||
"""
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
start_time = time.time()
|
||||
with self._condition:
|
||||
@@ -107,7 +108,7 @@ class ConfirmDialog:
|
||||
class MMAApprovalDialog:
|
||||
def __init__(self, ticket_id: str, payload: str) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self._payload = payload
|
||||
self._condition = threading.Condition()
|
||||
@@ -116,7 +117,7 @@ class MMAApprovalDialog:
|
||||
|
||||
def wait(self) -> tuple[bool, str]:
|
||||
"""
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
start_time = time.time()
|
||||
with self._condition:
|
||||
@@ -129,7 +130,7 @@ class MMAApprovalDialog:
|
||||
class MMASpawnApprovalDialog:
|
||||
def __init__(self, ticket_id: str, role: str, prompt: str, context_md: str) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self._prompt = prompt
|
||||
self._context_md = context_md
|
||||
@@ -140,7 +141,7 @@ class MMASpawnApprovalDialog:
|
||||
|
||||
def wait(self) -> dict[str, Any]:
|
||||
"""
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
start_time = time.time()
|
||||
with self._condition:
|
||||
@@ -157,15 +158,14 @@ class MMASpawnApprovalDialog:
|
||||
|
||||
class AppController:
|
||||
"""
|
||||
|
||||
The headless controller for the Manual Slop application.
|
||||
Owns the application state and manages background services.
|
||||
The headless controller for the Manual Slop application.
|
||||
Owns the application state and manages background services.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Initialize locks first to avoid initialization order issues
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self._send_thread_lock: threading.Lock = threading.Lock()
|
||||
self._disc_entries_lock: threading.Lock = threading.Lock()
|
||||
@@ -517,7 +517,8 @@ class AppController:
|
||||
|
||||
def _update_inject_preview(self) -> None:
|
||||
"""
|
||||
Updates the preview content based on the selected file and injection mode.
|
||||
|
||||
Updates the preview content based on the selected file and injection mode.
|
||||
[C: src/gui_2.py:App._gui_func, tests/test_skeleton_injection.py:test_update_inject_preview_full, tests/test_skeleton_injection.py:test_update_inject_preview_skeleton, tests/test_skeleton_injection.py:test_update_inject_preview_truncation]
|
||||
"""
|
||||
if not self._inject_file_path:
|
||||
@@ -754,7 +755,7 @@ class AppController:
|
||||
def _process_pending_gui_tasks(self) -> None:
|
||||
# Periodic telemetry broadcast
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, tests/test_api_hook_extensions.py:test_app_processes_new_actions, tests/test_gui_updates.py:test_gui_updates_on_event, tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_mma_orchestration_gui.py:test_handle_ai_response_fallback, tests/test_mma_orchestration_gui.py:test_handle_ai_response_with_stream_id, tests/test_mma_orchestration_gui.py:test_process_pending_gui_tasks_mma_spawn_approval, tests/test_mma_orchestration_gui.py:test_process_pending_gui_tasks_show_track_proposal, tests/test_process_pending_gui_tasks.py:test_gcli_path_updates_adapter, tests/test_process_pending_gui_tasks.py:test_redundant_calls_in_process_pending_gui_tasks]
|
||||
[C: src/gui_2.py:App._gui_func, tests/test_api_hook_extensions.py:test_app_processes_new_actions, tests/test_gui_updates.py:test_gui_updates_on_event, tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_mma_orchestration_gui.py:test_handle_ai_response_fallback, tests/test_mma_orchestration_gui.py:test_handle_ai_response_with_stream_id, tests/test_mma_orchestration_gui.py:test_process_pending_gui_tasks_mma_spawn_approval, tests/test_mma_orchestration_gui.py:test_process_pending_gui_tasks_show_track_proposal, tests/test_process_pending_gui_tasks.py:test_gcli_path_updates_adapter, tests/test_process_pending_gui_tasks.py:test_redundant_calls_in_process_pending_gui_tasks]
|
||||
"""
|
||||
now = time.time()
|
||||
if hasattr(self, 'event_queue') and hasattr(self.event_queue, 'websocket_server') and self.event_queue.websocket_server:
|
||||
@@ -955,7 +956,7 @@ class AppController:
|
||||
def __init__(self, t): self.t = t
|
||||
def wait(self):
|
||||
"""
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
[C: src/mcp_client.py:StdioMCPServer.stop, src/multi_agent_conductor.py:confirm_execution, src/multi_agent_conductor.py:confirm_spawn, tests/conftest.py:live_gui, tests/test_ai_client_concurrency.py:run_t1, tests/test_ai_client_concurrency.py:run_t2, tests/test_conductor_engine_abort.py:worker, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
return {'approved': True, 'abort': False, 'prompt': self.t.get("prompt"), 'context_md': self.t.get("context_md")}
|
||||
task["dialog_container"][0] = AutoSpawnDialog(task)
|
||||
@@ -1027,7 +1028,8 @@ class AppController:
|
||||
|
||||
def _process_pending_history_adds(self) -> None:
|
||||
"""
|
||||
Synchronizes pending history entries to the active discussion and project state.
|
||||
|
||||
Synchronizes pending history entries to the active discussion and project state.
|
||||
[C: src/gui_2.py:App._gui_func]
|
||||
"""
|
||||
with self._pending_history_adds_lock:
|
||||
@@ -1055,7 +1057,8 @@ class AppController:
|
||||
|
||||
def _process_pending_tool_calls(self) -> bool:
|
||||
"""
|
||||
Drains pending tool calls into the tool log. Returns True if any were processed.
|
||||
|
||||
Drains pending tool calls into the tool log. Returns True if any were processed.
|
||||
[C: src/gui_2.py:App._gui_func]
|
||||
"""
|
||||
with self._pending_tool_calls_lock:
|
||||
@@ -1102,7 +1105,8 @@ class AppController:
|
||||
|
||||
def init_state(self):
|
||||
"""
|
||||
Initializes the application state from configurations.
|
||||
|
||||
Initializes the application state from configurations.
|
||||
[C: src/gui_2.py:App.__init__, src/gui_2.py:App._render_paths_panel, src/gui_2.py:App._save_paths, tests/test_app_controller_mcp.py:test_app_controller_mcp_loading, tests/test_app_controller_mcp.py:test_app_controller_mcp_project_override, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_app_controller_init_state_loads_prompts]
|
||||
"""
|
||||
self.active_tickets = []
|
||||
@@ -1247,7 +1251,7 @@ class AppController:
|
||||
|
||||
async def refresh_external_mcps(self):
|
||||
"""
|
||||
[C: tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
[C: tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
"""
|
||||
await mcp_client.get_external_mcp_manager().stop_all()
|
||||
# Start servers with auto_start=True
|
||||
@@ -1257,7 +1261,7 @@ class AppController:
|
||||
|
||||
def cb_load_prior_log(self, path: Optional[str] = None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_log_management]
|
||||
[C: src/gui_2.py:App._render_log_management]
|
||||
"""
|
||||
root = hide_tk_root()
|
||||
if path is None:
|
||||
@@ -1448,7 +1452,7 @@ class AppController:
|
||||
|
||||
def cb_exit_prior_session(self):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_comms_history_panel, src/gui_2.py:App._render_discussion_panel]
|
||||
[C: src/gui_2.py:App._render_comms_history_panel, src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
self.is_viewing_prior_session = False
|
||||
if self._current_session_usage:
|
||||
@@ -1533,7 +1537,7 @@ class AppController:
|
||||
|
||||
def _fetch_models(self, provider: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App.run]
|
||||
[C: src/gui_2.py:App.run]
|
||||
"""
|
||||
self.ai_status = "fetching models..."
|
||||
|
||||
@@ -1559,7 +1563,8 @@ class AppController:
|
||||
|
||||
def start_services(self, app: Any = None):
|
||||
"""
|
||||
Starts background threads.
|
||||
|
||||
Starts background threads.
|
||||
[C: src/gui_2.py:App.__init__]
|
||||
"""
|
||||
self._prune_old_logs()
|
||||
@@ -1569,7 +1574,8 @@ class AppController:
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
Stops background threads and cleans up resources.
|
||||
|
||||
Stops background threads and cleans up resources.
|
||||
[C: src/gui_2.py:App.run, src/gui_2.py:App.shutdown, tests/conftest.py:app_instance, tests/conftest.py:mock_app]
|
||||
"""
|
||||
from src import ai_client
|
||||
@@ -1671,7 +1677,8 @@ class AppController:
|
||||
|
||||
def _handle_request_event(self, event: events.UserRequestEvent) -> None:
|
||||
"""
|
||||
Processes a UserRequestEvent by calling the AI client.
|
||||
|
||||
Processes a UserRequestEvent by calling the AI client.
|
||||
[C: tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_rag_integration.py:test_rag_integration]
|
||||
"""
|
||||
self.ai_status = 'sending...'
|
||||
@@ -1732,7 +1739,7 @@ class AppController:
|
||||
|
||||
def _on_comms_entry(self, entry: Dict[str, Any]) -> None:
|
||||
"""
|
||||
[C: tests/test_app_controller_offloading.py:test_on_comms_entry_tool_result_offloading]
|
||||
[C: tests/test_app_controller_offloading.py:test_on_comms_entry_tool_result_offloading]
|
||||
"""
|
||||
optimized_entry = self._offload_entry_payload(entry)
|
||||
session_logger.log_comms(optimized_entry)
|
||||
@@ -1829,7 +1836,7 @@ class AppController:
|
||||
|
||||
def _on_tool_log(self, script: str, result: str) -> None:
|
||||
"""
|
||||
[C: tests/test_app_controller_offloading.py:test_on_tool_log_offloading]
|
||||
[C: tests/test_app_controller_offloading.py:test_on_tool_log_offloading]
|
||||
"""
|
||||
session_logger.log_tool_call(script, result, None)
|
||||
session_logger.log_tool_output(result)
|
||||
@@ -1839,7 +1846,7 @@ class AppController:
|
||||
|
||||
def _on_api_event(self, event_name: str = "generic_event", **kwargs: Any) -> None:
|
||||
"""
|
||||
[C: tests/test_gui_updates.py:test_gui_updates_on_event]
|
||||
[C: tests/test_gui_updates.py:test_gui_updates_on_event]
|
||||
"""
|
||||
payload = kwargs.get("payload", {})
|
||||
# Push to background event queue, NOT GUI queue
|
||||
@@ -1857,7 +1864,7 @@ class AppController:
|
||||
|
||||
def _confirm_and_run(self, script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> Optional[str]:
|
||||
"""
|
||||
[C: tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_mutating_tool_triggers_callback, tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_rejection_prevents_dispatch]
|
||||
[C: tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_mutating_tool_triggers_callback, tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_rejection_prevents_dispatch]
|
||||
"""
|
||||
if self.test_hooks_enabled and not getattr(self, "ui_manual_approve", False):
|
||||
self.ai_status = "running powershell..."
|
||||
@@ -1897,7 +1904,7 @@ class AppController:
|
||||
|
||||
def _append_tool_log(self, script: str, result: str, source_tier: str | None = None, elapsed_ms: float = 0.0) -> None:
|
||||
"""
|
||||
[C: tests/test_mma_agent_focus_phase1.py:test_append_tool_log_dict_has_source_tier, tests/test_mma_agent_focus_phase1.py:test_append_tool_log_dict_keys, tests/test_mma_agent_focus_phase1.py:test_append_tool_log_stores_dict]
|
||||
[C: tests/test_mma_agent_focus_phase1.py:test_append_tool_log_dict_has_source_tier, tests/test_mma_agent_focus_phase1.py:test_append_tool_log_dict_keys, tests/test_mma_agent_focus_phase1.py:test_append_tool_log_stores_dict]
|
||||
"""
|
||||
self._tool_log.append({"script": script, "result": result, "ts": time.time(), "source_tier": source_tier})
|
||||
tool_name = self._extract_tool_name(script)
|
||||
@@ -1995,7 +2002,8 @@ class AppController:
|
||||
|
||||
def create_api(self) -> FastAPI:
|
||||
"""
|
||||
Creates and configures the FastAPI application for headless mode.
|
||||
|
||||
Creates and configures the FastAPI application for headless mode.
|
||||
[C: src/gui_2.py:App.run, tests/test_headless_service.py:TestHeadlessAPI.setUp]
|
||||
"""
|
||||
api = FastAPI(title="Manual Slop Headless API")
|
||||
@@ -2022,7 +2030,8 @@ class AppController:
|
||||
@api.get("/api/gui/state", dependencies=[Depends(get_api_key)])
|
||||
def get_gui_state() -> dict[str, Any]:
|
||||
"""
|
||||
Returns the current GUI state for specific fields.
|
||||
|
||||
Returns the current GUI state for specific fields.
|
||||
[C: tests/test_ai_settings_layout.py:test_change_provider_via_hook, tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_conductor_api_hook_integration.py:simulate_conductor_phase_completion, tests/test_external_editor_gui.py:test_button_click_is_received, tests/test_external_editor_gui.py:test_patch_modal_shows_with_configured_editor, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_text_viewer.py:test_text_viewer_state_update, tests/test_hooks.py:test_live_hook_server_responses, tests/test_live_gui_integration_v2.py:test_api_gui_state_live, tests/test_live_workflow.py:test_full_live_workflow, tests/test_live_workflow.py:wait_for_value, tests/test_patch_modal_gui.py:test_patch_apply_modal_workflow, tests/test_patch_modal_gui.py:test_patch_modal_appears_on_trigger, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_saved_presets_sim.py:test_preset_switching, tests/test_task_dag_popout_sim.py:test_task_dag_popout, tests/test_tool_management_layout.py:test_tool_management_gettable_fields, tests/test_tool_management_layout.py:test_tool_management_state_updates, tests/test_tool_presets_sim.py:test_tool_preset_switching, tests/test_usage_analytics_popout_sim.py:test_usage_analytics_popout]
|
||||
"""
|
||||
gettable = getattr(self, "_gettable_fields", {})
|
||||
@@ -2039,7 +2048,8 @@ class AppController:
|
||||
@api.get("/api/gui/mma_status", dependencies=[Depends(get_api_key)])
|
||||
def get_mma_status() -> dict[str, Any]:
|
||||
"""
|
||||
Dedicated endpoint for MMA-related status.
|
||||
|
||||
Dedicated endpoint for MMA-related status.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation, tests/test_live_workflow.py:test_full_live_workflow, tests/test_mma_concurrent_tracks_sim.py:_poll_mma_status, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_mma_step_mode_sim.py:_poll_mma_status, tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow, tests/test_visual_orchestration.py:test_mma_epic_lifecycle, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing, tests/test_visual_sim_mma_v2.py:_poll]
|
||||
"""
|
||||
return {
|
||||
@@ -2057,7 +2067,8 @@ class AppController:
|
||||
@api.post("/api/gui", dependencies=[Depends(get_api_key)])
|
||||
def post_gui(req: dict) -> dict[str, str]:
|
||||
"""
|
||||
Pushes a GUI task to the event queue.
|
||||
|
||||
Pushes a GUI task to the event queue.
|
||||
[C: tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_api_hook_client.py:test_post_gui_success, tests/test_gui2_parity.py:test_gui2_custom_callback_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_visual_mma.py:test_visual_mma_components]
|
||||
"""
|
||||
self.event_queue.put("gui_task", req)
|
||||
@@ -2085,7 +2096,8 @@ class AppController:
|
||||
@api.get("/api/performance", dependencies=[Depends(get_api_key)])
|
||||
def get_performance() -> dict[str, Any]:
|
||||
"""
|
||||
Returns performance monitor metrics.
|
||||
|
||||
Returns performance monitor metrics.
|
||||
[C: tests/test_gui2_performance.py:test_performance_benchmarking, tests/test_gui_performance_requirements.py:test_idle_performance_requirements, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing]
|
||||
"""
|
||||
return {"performance": self.perf_monitor.get_metrics()}
|
||||
@@ -2207,7 +2219,8 @@ class AppController:
|
||||
@api.get("/api/v1/sessions/{session_id}", dependencies=[Depends(get_api_key)])
|
||||
def get_session(session_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Returns the content of the comms.log for a specific session.
|
||||
|
||||
Returns the content of the comms.log for a specific session.
|
||||
[C: simulation/ping_pong.py:main, simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/sim_tools.py:ToolsSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/test_api_hook_client.py:test_get_session_success, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_live_workflow.py:test_full_live_workflow, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim]
|
||||
"""
|
||||
log_path = paths.get_logs_dir() / session_id / "comms.log"
|
||||
@@ -2265,14 +2278,14 @@ class AppController:
|
||||
|
||||
def _cb_reset_base_prompt(self, user_data=None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
"""
|
||||
self.ui_base_system_prompt = ai_client._SYSTEM_PROMPT
|
||||
self.ui_use_default_base_prompt = False
|
||||
|
||||
def _cb_clear_summary_cache(self, user_data=None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_files_panel]
|
||||
[C: src/gui_2.py:App._render_files_panel]
|
||||
"""
|
||||
from src import summarize
|
||||
summarize._summary_cache.clear()
|
||||
@@ -2280,7 +2293,7 @@ class AppController:
|
||||
|
||||
def _cb_show_base_prompt_diff(self, user_data=None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
"""
|
||||
self._show_base_prompt_diff_modal = True
|
||||
|
||||
@@ -2292,7 +2305,7 @@ class AppController:
|
||||
|
||||
def _switch_project(self, path: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_projects_panel]
|
||||
[C: src/gui_2.py:App._render_projects_panel]
|
||||
"""
|
||||
if not Path(path).exists():
|
||||
self.ai_status = f"project file not found: {path}"
|
||||
@@ -2315,7 +2328,7 @@ class AppController:
|
||||
def _refresh_from_project(self) -> None:
|
||||
# Deserialize FileItems in files.paths
|
||||
"""
|
||||
[C: tests/test_mma_dashboard_refresh.py:test_mma_dashboard_initialization_refresh, tests/test_mma_dashboard_refresh.py:test_mma_dashboard_refresh]
|
||||
[C: tests/test_mma_dashboard_refresh.py:test_mma_dashboard_initialization_refresh, tests/test_mma_dashboard_refresh.py:test_mma_dashboard_refresh]
|
||||
"""
|
||||
raw_paths = self.project.get("files", {}).get("paths", [])
|
||||
self.files = []
|
||||
@@ -2397,7 +2410,7 @@ class AppController:
|
||||
|
||||
def _cb_save_workspace_profile(self, name: str, scope: str = 'project') -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_save_workspace_profile_modal]
|
||||
[C: src/gui_2.py:App._render_save_workspace_profile_modal]
|
||||
"""
|
||||
if not hasattr(self, '_app') or not self._app:
|
||||
return
|
||||
@@ -2408,7 +2421,7 @@ class AppController:
|
||||
|
||||
def _cb_delete_workspace_profile(self, name: str, scope: str = 'project') -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._show_menus]
|
||||
[C: src/gui_2.py:App._show_menus]
|
||||
"""
|
||||
self.workspace_manager.delete_profile(name, scope=scope)
|
||||
self.workspace_profiles = self.workspace_manager.load_all_profiles()
|
||||
@@ -2417,7 +2430,7 @@ class AppController:
|
||||
|
||||
def _cb_load_workspace_profile(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._show_menus]
|
||||
[C: src/gui_2.py:App._show_menus]
|
||||
"""
|
||||
if name in self.workspace_profiles:
|
||||
profile = self.workspace_profiles[name]
|
||||
@@ -2426,7 +2439,7 @@ class AppController:
|
||||
|
||||
def _apply_preset(self, name: str, scope: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
[C: src/gui_2.py:App._render_system_prompts_panel]
|
||||
"""
|
||||
print(f"[DEBUG] _apply_preset: name={name}, scope={scope}")
|
||||
if name == "None":
|
||||
@@ -2448,7 +2461,7 @@ class AppController:
|
||||
|
||||
def _cb_save_preset(self, name, content, scope):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_preset_manager_content]
|
||||
[C: src/gui_2.py:App._render_preset_manager_content]
|
||||
"""
|
||||
print(f"[DEBUG] _cb_save_preset: name={name}, scope={scope}")
|
||||
if not name or not name.strip():
|
||||
@@ -2463,14 +2476,14 @@ class AppController:
|
||||
|
||||
def _cb_delete_preset(self, name, scope):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_preset_manager_content]
|
||||
[C: src/gui_2.py:App._render_preset_manager_content]
|
||||
"""
|
||||
self.preset_manager.delete_preset(name, scope)
|
||||
self.presets = self.preset_manager.load_all()
|
||||
|
||||
def _cb_save_tool_preset(self, name, categories, scope):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
"""
|
||||
preset = models.ToolPreset(name=name, categories=categories)
|
||||
self.tool_preset_manager.save_preset(preset, scope)
|
||||
@@ -2478,14 +2491,14 @@ class AppController:
|
||||
|
||||
def _cb_delete_tool_preset(self, name, scope):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
"""
|
||||
self.tool_preset_manager.delete_preset(name, scope)
|
||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||
|
||||
def _cb_save_bias_profile(self, profile: models.BiasProfile, scope: str = "project"):
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
[C: src/gui_2.py:App._render_tool_preset_manager_content]
|
||||
"""
|
||||
self.tool_preset_manager.save_bias_profile(profile, scope)
|
||||
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
||||
@@ -2496,14 +2509,14 @@ class AppController:
|
||||
|
||||
def _cb_save_persona(self, persona: models.Persona, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_persona_editor_window]
|
||||
[C: src/gui_2.py:App._render_persona_editor_window]
|
||||
"""
|
||||
self.persona_manager.save_persona(persona, scope)
|
||||
self.personas = self.persona_manager.load_all()
|
||||
|
||||
def _cb_delete_persona(self, name: str, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_persona_editor_window]
|
||||
[C: src/gui_2.py:App._render_persona_editor_window]
|
||||
"""
|
||||
self.persona_manager.delete_persona(name, scope)
|
||||
self.personas = self.persona_manager.load_all()
|
||||
@@ -2511,7 +2524,7 @@ class AppController:
|
||||
|
||||
def _cb_load_track(self, track_id: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_mma_dashboard]
|
||||
[C: src/gui_2.py:App._render_mma_dashboard]
|
||||
"""
|
||||
state = project_manager.load_track_state(track_id, self.active_project_root)
|
||||
if state:
|
||||
@@ -2545,7 +2558,7 @@ class AppController:
|
||||
|
||||
def _save_active_project(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App.delete_context_preset, src/gui_2.py:App.save_context_preset]
|
||||
[C: src/gui_2.py:App.delete_context_preset, src/gui_2.py:App.save_context_preset]
|
||||
"""
|
||||
if self.active_project_path:
|
||||
try:
|
||||
@@ -2556,7 +2569,7 @@ class AppController:
|
||||
|
||||
def _get_discussion_names(self) -> list[str]:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
discussions = disc_sec.get("discussions", {})
|
||||
@@ -2564,7 +2577,7 @@ class AppController:
|
||||
|
||||
def _switch_discussion(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_takes_panel]
|
||||
[C: src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_takes_panel]
|
||||
"""
|
||||
self._flush_disc_entries_to_project()
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
@@ -2582,7 +2595,7 @@ class AppController:
|
||||
|
||||
def _flush_disc_entries_to_project(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries]
|
||||
if self.active_track and self._track_discussion_active:
|
||||
@@ -2596,7 +2609,7 @@ class AppController:
|
||||
|
||||
def _create_discussion(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_synthesis_panel, src/gui_2.py:App._render_takes_panel]
|
||||
[C: src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_synthesis_panel, src/gui_2.py:App._render_takes_panel]
|
||||
"""
|
||||
disc_sec = self.project.setdefault("discussion", {})
|
||||
discussions = disc_sec.setdefault("discussions", {})
|
||||
@@ -2608,7 +2621,7 @@ class AppController:
|
||||
|
||||
def _branch_discussion(self, index: int) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
self._flush_disc_entries_to_project()
|
||||
# Generate a unique branch name
|
||||
@@ -2625,7 +2638,7 @@ class AppController:
|
||||
self._switch_discussion(new_name)
|
||||
def _rename_discussion(self, old_name: str, new_name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
discussions = disc_sec.get("discussions", {})
|
||||
@@ -2641,7 +2654,7 @@ class AppController:
|
||||
|
||||
def _delete_discussion(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
[C: src/gui_2.py:App._render_discussion_panel]
|
||||
"""
|
||||
disc_sec = self.project.get("discussion", {})
|
||||
discussions = disc_sec.get("discussions", {})
|
||||
@@ -2657,7 +2670,7 @@ class AppController:
|
||||
|
||||
def _handle_mma_respond(self, approved: bool, payload: str | None = None, abort: bool = False, prompt: str | None = None, context_md: str | None = None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._handle_approve_mma_step, src/gui_2.py:App._handle_approve_spawn]
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._handle_approve_mma_step, src/gui_2.py:App._handle_approve_spawn]
|
||||
"""
|
||||
if self._pending_mma_approvals:
|
||||
task = self._pending_mma_approvals.pop(0)
|
||||
@@ -2685,7 +2698,8 @@ class AppController:
|
||||
|
||||
def _handle_approve_ask(self) -> None:
|
||||
"""
|
||||
Responds with approval for a pending /api/ask request.
|
||||
|
||||
Responds with approval for a pending /api/ask request.
|
||||
[C: src/gui_2.py:App.__init__, src/gui_2.py:App._gui_func]
|
||||
"""
|
||||
if not self._ask_request_id: return
|
||||
@@ -2706,7 +2720,8 @@ class AppController:
|
||||
|
||||
def _handle_reject_ask(self) -> None:
|
||||
"""
|
||||
Responds with rejection for a pending /api/ask request.
|
||||
|
||||
Responds with rejection for a pending /api/ask request.
|
||||
[C: src/gui_2.py:App._gui_func]
|
||||
"""
|
||||
if not self._ask_request_id: return
|
||||
@@ -2727,7 +2742,8 @@ class AppController:
|
||||
|
||||
def _handle_reset_session(self) -> None:
|
||||
"""
|
||||
Logic for resetting the AI session and GUI state.
|
||||
|
||||
Logic for resetting the AI session and GUI state.
|
||||
[C: src/gui_2.py:App._render_message_panel]
|
||||
"""
|
||||
ai_client.reset_session()
|
||||
@@ -2758,13 +2774,14 @@ class AppController:
|
||||
|
||||
def _handle_md_only(self) -> None:
|
||||
"""
|
||||
Logic for the 'MD Only' action.
|
||||
|
||||
Logic for the 'MD Only' action.
|
||||
[C: src/gui_2.py:App._render_message_panel]
|
||||
"""
|
||||
|
||||
def worker():
|
||||
"""
|
||||
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
"""
|
||||
try:
|
||||
md, path, *_ = self._do_generate()
|
||||
@@ -2779,13 +2796,14 @@ class AppController:
|
||||
|
||||
def _handle_generate_send(self) -> None:
|
||||
"""
|
||||
Logic for the 'Gen + Send' action.
|
||||
|
||||
Logic for the 'Gen + Send' action.
|
||||
[C: src/gui_2.py:App._render_message_panel, src/gui_2.py:App._render_synthesis_panel, src/gui_2.py:App._render_takes_panel, tests/test_gui_events_v2.py:test_handle_generate_send_pushes_event, tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
"""
|
||||
|
||||
def worker():
|
||||
"""
|
||||
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
"""
|
||||
sys.stderr.write("[DEBUG] _handle_generate_send worker started\n")
|
||||
sys.stderr.flush()
|
||||
@@ -2852,7 +2870,7 @@ class AppController:
|
||||
|
||||
def _refresh_api_metrics(self, payload: dict[str, Any], md_content: str | None = None) -> None:
|
||||
"""
|
||||
[C: tests/test_gui_updates.py:test_telemetry_data_updates_correctly]
|
||||
[C: tests/test_gui_updates.py:test_telemetry_data_updates_correctly]
|
||||
"""
|
||||
if "latency" in payload:
|
||||
self.session_usage["last_latency"] = payload["latency"]
|
||||
@@ -2879,7 +2897,7 @@ class AppController:
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_cache_panel]
|
||||
[C: src/gui_2.py:App._render_cache_panel]
|
||||
"""
|
||||
from src import ai_client
|
||||
ai_client.cleanup()
|
||||
@@ -2887,7 +2905,7 @@ class AppController:
|
||||
|
||||
def get_session_insights(self) -> Dict[str, Any]:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_session_insights_panel]
|
||||
[C: src/gui_2.py:App._render_session_insights_panel]
|
||||
"""
|
||||
from src import cost_tracker
|
||||
total_input = sum(e["input"] for e in self._token_history)
|
||||
@@ -2912,7 +2930,7 @@ class AppController:
|
||||
|
||||
def _flush_to_project(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._show_menus]
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._show_menus]
|
||||
"""
|
||||
proj = self.project
|
||||
proj.setdefault("output", {})["output_dir"] = self.ui_output_dir
|
||||
@@ -2952,7 +2970,7 @@ class AppController:
|
||||
|
||||
def _flush_to_config(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._render_theme_panel, src/gui_2.py:App._show_menus, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_app_controller_flush_saves_prompts]
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_projects_panel, src/gui_2.py:App._render_theme_panel, src/gui_2.py:App._show_menus, tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_app_controller_flush_saves_prompts]
|
||||
"""
|
||||
self.config["ai"] = {
|
||||
"provider": self.current_provider,
|
||||
@@ -2995,7 +3013,8 @@ class AppController:
|
||||
|
||||
def _do_generate(self) -> tuple[str, Path, list[dict[str, Any]], str, str]:
|
||||
"""
|
||||
Returns (full_md, output_path, file_items, stable_md, discussion_text).
|
||||
|
||||
Returns (full_md, output_path, file_items, stable_md, discussion_text).
|
||||
[C: src/gui_2.py:App._show_menus, tests/test_tiered_aggregation.py:test_app_controller_do_generate_uses_persona_strategy]
|
||||
"""
|
||||
self._flush_to_project()
|
||||
@@ -3029,7 +3048,7 @@ class AppController:
|
||||
|
||||
def _cb_plan_epic(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_mma_orchestration_gui.py:test_cb_plan_epic_launches_thread]
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_mma_orchestration_gui.py:test_cb_plan_epic_launches_thread]
|
||||
"""
|
||||
def _bg_task() -> None:
|
||||
sys.stderr.write("[DEBUG] _cb_plan_epic _bg_task started\n")
|
||||
@@ -3078,7 +3097,7 @@ class AppController:
|
||||
|
||||
def _cb_accept_tracks(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_track_proposal_modal]
|
||||
[C: src/gui_2.py:App._render_track_proposal_modal]
|
||||
"""
|
||||
self._show_track_proposal_modal = False
|
||||
|
||||
@@ -3122,7 +3141,7 @@ class AppController:
|
||||
|
||||
def _cb_start_track(self, user_data: Any = None) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_track_proposal_modal]
|
||||
[C: src/gui_2.py:App._render_track_proposal_modal]
|
||||
"""
|
||||
if isinstance(user_data, str):
|
||||
# If track_id is provided directly
|
||||
@@ -3234,7 +3253,7 @@ class AppController:
|
||||
|
||||
def _cb_ticket_retry(self, ticket_id: str) -> None:
|
||||
"""
|
||||
[C: tests/test_mma_ticket_actions.py:test_cb_ticket_retry]
|
||||
[C: tests/test_mma_ticket_actions.py:test_cb_ticket_retry]
|
||||
"""
|
||||
for t in self.active_tickets:
|
||||
if t.get('id') == ticket_id:
|
||||
@@ -3244,7 +3263,7 @@ class AppController:
|
||||
|
||||
def _cb_ticket_skip(self, ticket_id: str) -> None:
|
||||
"""
|
||||
[C: tests/test_mma_ticket_actions.py:test_cb_ticket_skip]
|
||||
[C: tests/test_mma_ticket_actions.py:test_cb_ticket_skip]
|
||||
"""
|
||||
for t in self.active_tickets:
|
||||
if t.get('id') == ticket_id:
|
||||
@@ -3266,7 +3285,8 @@ class AppController:
|
||||
|
||||
def kill_worker(self, worker_id: str) -> None:
|
||||
"""
|
||||
Aborts a running worker.
|
||||
|
||||
Aborts a running worker.
|
||||
[C: src/gui_2.py:App._cb_kill_ticket, tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread]
|
||||
"""
|
||||
engine = self.engines.get(self.active_track.id if self.active_track else None)
|
||||
@@ -3289,7 +3309,8 @@ class AppController:
|
||||
|
||||
def inject_context(self, data: dict) -> None:
|
||||
"""
|
||||
Programmatic context injection.
|
||||
|
||||
Programmatic context injection.
|
||||
[C: tests/test_headless_simulation.py:test_mma_track_lifecycle_simulation]
|
||||
"""
|
||||
file_path = data.get("file_path")
|
||||
@@ -3338,7 +3359,7 @@ class AppController:
|
||||
|
||||
def _cb_run_conductor_setup(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_gui_phase3.py:test_conductor_setup_scan]
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_gui_phase3.py:test_conductor_setup_scan]
|
||||
"""
|
||||
base = paths.get_conductor_dir(project_path=self.active_project_root)
|
||||
if not base.exists():
|
||||
@@ -3368,7 +3389,7 @@ class AppController:
|
||||
|
||||
def _cb_create_track(self, name: str, desc: str, track_type: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_gui_phase3.py:test_create_track]
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, tests/test_gui_phase3.py:test_create_track]
|
||||
"""
|
||||
if not name: return
|
||||
date_suffix = datetime.now().strftime("%Y%m%d")
|
||||
@@ -3396,7 +3417,7 @@ class AppController:
|
||||
|
||||
def _push_mma_state_update(self) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._cb_block_ticket, src/gui_2.py:App._cb_unblock_ticket, src/gui_2.py:App._render_mma_dashboard, src/gui_2.py:App._render_task_dag_panel, src/gui_2.py:App._render_ticket_queue, src/gui_2.py:App._reorder_ticket, src/gui_2.py:App.bulk_block, src/gui_2.py:App.bulk_execute, src/gui_2.py:App.bulk_skip, tests/test_gui_phase4.py:test_push_mma_state_update]
|
||||
[C: src/gui_2.py:App._cb_block_ticket, src/gui_2.py:App._cb_unblock_ticket, src/gui_2.py:App._render_mma_dashboard, src/gui_2.py:App._render_task_dag_panel, src/gui_2.py:App._render_ticket_queue, src/gui_2.py:App._reorder_ticket, src/gui_2.py:App.bulk_block, src/gui_2.py:App.bulk_execute, src/gui_2.py:App.bulk_skip, tests/test_gui_phase4.py:test_push_mma_state_update]
|
||||
"""
|
||||
if not self.active_track:
|
||||
return
|
||||
@@ -3420,7 +3441,8 @@ class AppController:
|
||||
|
||||
def _load_active_tickets(self) -> None:
|
||||
"""
|
||||
Populates self.active_tickets based on the current execution mode.
|
||||
|
||||
Populates self.active_tickets based on the current execution mode.
|
||||
[C: tests/test_gui_dag_beads.py:test_load_active_tickets_from_beads]
|
||||
"""
|
||||
if getattr(self, "ui_project_execution_mode", "native") == "beads":
|
||||
@@ -3442,4 +3464,4 @@ class AppController:
|
||||
if self.active_track:
|
||||
self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in self.active_track.tickets]
|
||||
else:
|
||||
self.active_tickets = []
|
||||
self.active_tickets = []
|
||||
+24
-6
@@ -12,22 +12,34 @@ class Bead:
|
||||
|
||||
class BeadsClient:
|
||||
def __init__(self, working_dir: Path):
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.working_dir = Path(working_dir)
|
||||
self.repo_dir = self.working_dir / ".beads_mock"
|
||||
self.beads_file = self.repo_dir / "beads.json"
|
||||
|
||||
def init_repo(self) -> None:
|
||||
"""Initialize the mock repository."""
|
||||
"""
|
||||
Initialize the mock repository.
|
||||
[C: tests/test_aggregate_beads.py:test_build_beads_compaction, tests/test_beads_client.py:test_beads_init_and_query, tests/test_gui_dag_beads.py:test_load_active_tickets_from_beads, tests/test_mcp_client_beads.py:test_bd_mcp_tools]
|
||||
"""
|
||||
self.repo_dir.mkdir(parents=True, exist_ok=True)
|
||||
if not self.beads_file.exists():
|
||||
self.beads_file.write_text("[]", encoding="utf-8")
|
||||
|
||||
def is_initialized(self) -> bool:
|
||||
"""Check if the repository is initialized."""
|
||||
"""
|
||||
Check if the repository is initialized.
|
||||
[C: src/mcp_client.py:dispatch, tests/test_beads_client.py:test_beads_init_and_query]
|
||||
"""
|
||||
return self.beads_file.exists()
|
||||
|
||||
def create_bead(self, title: str, description: str) -> str:
|
||||
"""Create a new bead and return its ID."""
|
||||
"""
|
||||
Create a new bead and return its ID.
|
||||
[C: src/mcp_client.py:dispatch, tests/test_aggregate_beads.py:test_build_beads_compaction, tests/test_beads_client.py:test_beads_init_and_query, tests/test_gui_dag_beads.py:test_load_active_tickets_from_beads]
|
||||
"""
|
||||
beads = self._read_beads()
|
||||
bead_id = f"bead-{len(beads) + 1}"
|
||||
bead = {"id": bead_id, "title": title, "description": description, "status": "active"}
|
||||
@@ -36,7 +48,10 @@ class BeadsClient:
|
||||
return bead_id
|
||||
|
||||
def update_bead(self, bead_id: str, status: str) -> bool:
|
||||
"""Update the status of an existing bead."""
|
||||
"""
|
||||
Update the status of an existing bead.
|
||||
[C: src/mcp_client.py:dispatch, tests/test_aggregate_beads.py:test_build_beads_compaction, tests/test_beads_client.py:test_beads_init_and_query]
|
||||
"""
|
||||
beads = self._read_beads()
|
||||
for bead in beads:
|
||||
if bead["id"] == bead_id:
|
||||
@@ -46,7 +61,10 @@ class BeadsClient:
|
||||
return False
|
||||
|
||||
def list_beads(self) -> List[Bead]:
|
||||
"""List all beads."""
|
||||
"""
|
||||
List all beads.
|
||||
[C: src/gui_2.py:App._render_beads_tab, src/mcp_client.py:dispatch, tests/test_beads_client.py:test_beads_init_and_query]
|
||||
"""
|
||||
return [Bead(**b) for b in self._read_beads()]
|
||||
|
||||
def _read_beads(self) -> List[dict]:
|
||||
@@ -55,4 +73,4 @@ class BeadsClient:
|
||||
return json.loads(self.beads_file.read_text(encoding="utf-8"))
|
||||
|
||||
def _write_beads(self, beads: List[dict]) -> None:
|
||||
self.beads_file.write_text(json.dumps(beads, indent=1), encoding="utf-8")
|
||||
self.beads_file.write_text(json.dumps(beads, indent=1), encoding="utf-8")
|
||||
+9
-1
@@ -7,11 +7,17 @@ from imgui_bundle import imgui, nanovg as nvg, hello_imgui
|
||||
|
||||
class BackgroundShader:
|
||||
def __init__(self):
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.enabled = False
|
||||
self.start_time = time.time()
|
||||
self.ctx: Optional[nvg.Context] = None
|
||||
|
||||
def render(self, width: float, height: float):
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_discussion_panel, src/gui_2.py:App._render_heavy_text, src/gui_2.py:App._render_markdown_test, src/gui_2.py:App._render_response_panel, src/gui_2.py:App._render_snapshot_tab, src/markdown_helper.py:MarkdownRenderer._render_code_block, src/markdown_helper.py:MarkdownRenderer.render, src/markdown_helper.py:render, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_active, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_inactive, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_render, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_disabled, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_render]
|
||||
"""
|
||||
if not self.enabled or width <= 0 or height <= 0:
|
||||
return
|
||||
|
||||
@@ -59,8 +65,10 @@ class BackgroundShader:
|
||||
_bg: Optional[BackgroundShader] = None
|
||||
|
||||
def get_bg():
|
||||
"""
|
||||
[C: src/gui_2.py:App._gui_func, src/gui_2.py:App._render_theme_panel]
|
||||
"""
|
||||
global _bg
|
||||
if _bg is None:
|
||||
_bg = BackgroundShader()
|
||||
return _bg
|
||||
|
||||
|
||||
@@ -41,9 +41,11 @@ from typing import Any
|
||||
|
||||
def generate_tickets(track_brief: str, module_skeletons: str) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Tier 2 (Tech Lead) call.
|
||||
Breaks down a Track Brief and module skeletons into discrete Tier 3 Tickets.
|
||||
"""
|
||||
|
||||
Tier 2 (Tech Lead) call.
|
||||
Breaks down a Track Brief and module skeletons into discrete Tier 3 Tickets.
|
||||
[C: src/native_orchestrator.py:NativeOrchestrator.generate_tickets, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_retry_failure, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_retry_success, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_success, tests/test_orchestration_logic.py:test_generate_tickets]
|
||||
"""
|
||||
# 1. Set Tier 2 Model (Tech Lead - Flash)
|
||||
# 2. Construct Prompt
|
||||
system_prompt = mma_prompts.PROMPTS.get("tier2_sprint_planning")
|
||||
@@ -95,9 +97,11 @@ from src.models import Ticket
|
||||
|
||||
def topological_sort(tickets: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Sorts a list of tickets based on their 'depends_on' field.
|
||||
Raises ValueError if a circular dependency or missing internal dependency is detected.
|
||||
"""
|
||||
|
||||
Sorts a list of tickets based on their 'depends_on' field.
|
||||
Raises ValueError if a circular dependency or missing internal dependency is detected.
|
||||
[C: tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_complex, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_cycle, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_empty, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_linear, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_missing_dependency, tests/test_conductor_tech_lead.py:test_topological_sort_vlog, tests/test_dag_engine.py:test_topological_sort, tests/test_dag_engine.py:test_topological_sort_cycle, tests/test_orchestration_logic.py:test_topological_sort, tests/test_orchestration_logic.py:test_topological_sort_circular, tests/test_perf_dag.py:test_dag_edge_cases, tests/test_perf_dag.py:test_dag_performance]
|
||||
"""
|
||||
# 1. Convert to Ticket objects for TrackDAG
|
||||
ticket_objs = []
|
||||
for t_data in tickets:
|
||||
@@ -118,4 +122,3 @@ if __name__ == "__main__":
|
||||
test_skeletons = "class NewFeature: pass"
|
||||
tickets = generate_tickets(test_brief, test_skeletons)
|
||||
print(json.dumps(tickets, indent=2))
|
||||
|
||||
|
||||
+4
-3
@@ -46,8 +46,10 @@ MODEL_PRICING = [
|
||||
|
||||
def estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
|
||||
"""
|
||||
Estimate the cost of a model call based on input and output tokens.
|
||||
Returns the total cost in USD.
|
||||
|
||||
Estimate the cost of a model call based on input and output tokens.
|
||||
Returns the total cost in USD.
|
||||
[C: src/gui_2.py:App._render_mma_dashboard, src/gui_2.py:App._render_token_budget_panel, tests/test_cost_tracker.py:test_estimate_cost]
|
||||
"""
|
||||
if not model:
|
||||
return 0.0
|
||||
@@ -59,4 +61,3 @@ def estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
|
||||
return input_cost + output_cost
|
||||
|
||||
return 0.0
|
||||
|
||||
|
||||
+68
-48
@@ -32,24 +32,29 @@ from src.performance_monitor import get_monitor
|
||||
|
||||
class TrackDAG:
|
||||
"""
|
||||
Manages a Directed Acyclic Graph of implementation tickets.
|
||||
Provides methods for dependency resolution, cycle detection, and topological sorting.
|
||||
"""
|
||||
|
||||
Manages a Directed Acyclic Graph of implementation tickets.
|
||||
Provides methods for dependency resolution, cycle detection, and topological sorting.
|
||||
"""
|
||||
|
||||
def __init__(self, tickets: List[Ticket]) -> None:
|
||||
"""
|
||||
Initializes the TrackDAG with a list of Ticket objects.
|
||||
Args:
|
||||
tickets: A list of Ticket instances defining the graph nodes and edges.
|
||||
"""
|
||||
|
||||
Initializes the TrackDAG with a list of Ticket objects.
|
||||
Args:
|
||||
tickets: A list of Ticket instances defining the graph nodes and edges.
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.tickets = tickets
|
||||
self.ticket_map = {t.id: t for t in tickets}
|
||||
|
||||
def cascade_blocks(self) -> None:
|
||||
"""
|
||||
Transitively marks `todo` tickets as `blocked` if any dependency is `blocked`.
|
||||
Propagates 'blocked' status from initially blocked nodes to their dependents.
|
||||
"""
|
||||
|
||||
Transitively marks `todo` tickets as `blocked` if any dependency is `blocked`.
|
||||
Propagates 'blocked' status from initially blocked nodes to their dependents.
|
||||
[C: tests/test_perf_dag.py:test_dag_performance]
|
||||
"""
|
||||
with get_monitor().scope("dag_cascade_blocks"):
|
||||
# Build adjacency list of dependents using object references to avoid lookups
|
||||
dependents = {t.id: [] for t in self.tickets}
|
||||
@@ -82,10 +87,12 @@ class TrackDAG:
|
||||
|
||||
def get_ready_tasks(self) -> List[Ticket]:
|
||||
"""
|
||||
Returns a list of tickets that are in 'todo' status and whose dependencies are all 'completed'.
|
||||
Returns:
|
||||
A list of Ticket objects ready for execution.
|
||||
"""
|
||||
|
||||
Returns a list of tickets that are in 'todo' status and whose dependencies are all 'completed'.
|
||||
Returns:
|
||||
A list of Ticket objects ready for execution.
|
||||
[C: src/models.py:Track.get_executable_tickets, tests/test_dag_engine.py:test_get_ready_tasks_branching, tests/test_dag_engine.py:test_get_ready_tasks_linear, tests/test_dag_engine.py:test_get_ready_tasks_multiple_deps, tests/test_orchestration_logic.py:test_track_executable_tickets]
|
||||
"""
|
||||
ready = []
|
||||
for ticket in self.tickets:
|
||||
if ticket.status == 'todo' and self.is_ticket_ready(ticket):
|
||||
@@ -94,10 +101,12 @@ class TrackDAG:
|
||||
|
||||
def has_cycle(self) -> bool:
|
||||
"""
|
||||
Performs an iterative Depth-First Search to detect cycles in the dependency graph.
|
||||
Returns:
|
||||
True if a cycle is detected, False otherwise.
|
||||
"""
|
||||
|
||||
Performs an iterative Depth-First Search to detect cycles in the dependency graph.
|
||||
Returns:
|
||||
True if a cycle is detected, False otherwise.
|
||||
[C: src/gui_2.py:App._render_task_dag_panel, tests/test_dag_engine.py:test_has_cycle_complex_no_cycle, tests/test_dag_engine.py:test_has_cycle_direct_cycle, tests/test_dag_engine.py:test_has_cycle_indirect_cycle, tests/test_dag_engine.py:test_has_cycle_no_cycle, tests/test_perf_dag.py:test_dag_edge_cases, tests/test_perf_dag.py:test_dag_performance]
|
||||
"""
|
||||
with get_monitor().scope("dag_has_cycle"):
|
||||
visited = set()
|
||||
for start_ticket in self.tickets:
|
||||
@@ -125,13 +134,15 @@ class TrackDAG:
|
||||
|
||||
def topological_sort(self) -> List[str]:
|
||||
"""
|
||||
Returns a list of ticket IDs in topological order (dependencies before dependents).
|
||||
Uses Kahn's algorithm for efficient O(V+E) sorting and cycle detection.
|
||||
Returns:
|
||||
A list of ticket ID strings.
|
||||
Raises:
|
||||
ValueError: If a dependency cycle is detected.
|
||||
"""
|
||||
|
||||
Returns a list of ticket IDs in topological order (dependencies before dependents).
|
||||
Uses Kahn's algorithm for efficient O(V+E) sorting and cycle detection.
|
||||
Returns:
|
||||
A list of ticket ID strings.
|
||||
Raises:
|
||||
ValueError: If a dependency cycle is detected.
|
||||
[C: tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_complex, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_cycle, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_empty, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_linear, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_missing_dependency, tests/test_conductor_tech_lead.py:test_topological_sort_vlog, tests/test_dag_engine.py:test_topological_sort, tests/test_dag_engine.py:test_topological_sort_cycle, tests/test_orchestration_logic.py:test_topological_sort, tests/test_orchestration_logic.py:test_topological_sort_circular, tests/test_perf_dag.py:test_dag_edge_cases, tests/test_perf_dag.py:test_dag_performance]
|
||||
"""
|
||||
with get_monitor().scope("dag_topological_sort"):
|
||||
in_degree = {t.id: len(t.depends_on) for t in self.tickets}
|
||||
dependents = {t.id: [] for t in self.tickets}
|
||||
@@ -159,27 +170,32 @@ class TrackDAG:
|
||||
|
||||
class ExecutionEngine:
|
||||
"""
|
||||
A state machine that governs the progression of tasks within a TrackDAG.
|
||||
Handles automatic queueing and manual task approval.
|
||||
"""
|
||||
|
||||
A state machine that governs the progression of tasks within a TrackDAG.
|
||||
Handles automatic queueing and manual task approval.
|
||||
"""
|
||||
|
||||
def __init__(self, dag: TrackDAG, auto_queue: bool = False) -> None:
|
||||
"""
|
||||
Initializes the ExecutionEngine.
|
||||
Args:
|
||||
dag: The TrackDAG instance to manage.
|
||||
auto_queue: If True, ready tasks will automatically move to 'in_progress'.
|
||||
"""
|
||||
|
||||
Initializes the ExecutionEngine.
|
||||
Args:
|
||||
dag: The TrackDAG instance to manage.
|
||||
auto_queue: If True, ready tasks will automatically move to 'in_progress'.
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.dag = dag
|
||||
self.auto_queue = auto_queue
|
||||
|
||||
def tick(self) -> List[Ticket]:
|
||||
"""
|
||||
Evaluates the DAG and returns a list of tasks that are currently 'ready' for execution.
|
||||
If auto_queue is enabled, tasks without 'step_mode' will be marked as 'in_progress'.
|
||||
Returns:
|
||||
A list of ready Ticket objects.
|
||||
"""
|
||||
|
||||
Evaluates the DAG and returns a list of tasks that are currently 'ready' for execution.
|
||||
If auto_queue is enabled, tasks without 'step_mode' will be marked as 'in_progress'.
|
||||
Returns:
|
||||
A list of ready Ticket objects.
|
||||
[C: src/multi_agent_conductor.py:ConductorEngine.run, tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_cascade_blocks_multi_hop, tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_cascade_blocks_simple, tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_execution_engine_tick_cascades_blocks, tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_in_progress_not_blocked, tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_manual_unblock_restores_todo, tests/test_execution_engine.py:test_execution_engine_auto_queue, tests/test_execution_engine.py:test_execution_engine_basic_flow, tests/test_execution_engine.py:test_execution_engine_step_mode]
|
||||
"""
|
||||
with get_monitor().scope("dag_tick"):
|
||||
self.dag.cascade_blocks()
|
||||
ready = self.dag.get_ready_tasks()
|
||||
@@ -187,21 +203,25 @@ class ExecutionEngine:
|
||||
|
||||
def approve_task(self, task_id: str) -> None:
|
||||
"""
|
||||
Manually transitions a task from 'todo' to 'in_progress' if its dependencies are met.
|
||||
Args:
|
||||
task_id: The ID of the task to approve.
|
||||
"""
|
||||
|
||||
Manually transitions a task from 'todo' to 'in_progress' if its dependencies are met.
|
||||
Args:
|
||||
task_id: The ID of the task to approve.
|
||||
[C: src/multi_agent_conductor.py:ConductorEngine.approve_task, tests/test_execution_engine.py:test_execution_engine_approve_task, tests/test_execution_engine.py:test_execution_engine_step_mode]
|
||||
"""
|
||||
ticket = self.dag.ticket_map.get(task_id)
|
||||
if ticket and ticket.status == "todo" and self.dag.is_ticket_ready(ticket):
|
||||
ticket.status = "in_progress"
|
||||
|
||||
def update_task_status(self, task_id: str, status: str) -> None:
|
||||
"""
|
||||
Force-updates the status of a specific task.
|
||||
Args:
|
||||
task_id: The ID of the task.
|
||||
status: The new status string (e.g., 'todo', 'in_progress', 'completed', 'blocked').
|
||||
"""
|
||||
|
||||
Force-updates the status of a specific task.
|
||||
Args:
|
||||
task_id: The ID of the task.
|
||||
status: The new status string (e.g., 'todo', 'in_progress', 'completed', 'blocked').
|
||||
[C: src/multi_agent_conductor.py:ConductorEngine.update_task_status, tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_manual_unblock_restores_todo, tests/test_execution_engine.py:test_execution_engine_auto_queue, tests/test_execution_engine.py:test_execution_engine_basic_flow, tests/test_execution_engine.py:test_execution_engine_status_persistence, tests/test_execution_engine.py:test_execution_engine_update_nonexistent_task]
|
||||
"""
|
||||
ticket = self.dag.ticket_map.get(task_id)
|
||||
if ticket:
|
||||
ticket.status = status
|
||||
ticket.status = status
|
||||
+25
-1
@@ -36,6 +36,9 @@ def parse_diff_header(line: str) -> tuple[Optional[str], Optional[str], Optional
|
||||
return None, None, None
|
||||
|
||||
def parse_hunk_header(line: str) -> Optional[tuple[int, int, int, int]]:
|
||||
"""
|
||||
[C: tests/test_diff_viewer.py:test_parse_hunk_header]
|
||||
"""
|
||||
if not line.startswith("@@"):
|
||||
return None
|
||||
|
||||
@@ -57,6 +60,9 @@ def parse_hunk_header(line: str) -> Optional[tuple[int, int, int, int]]:
|
||||
return (old_start, old_count, new_start, new_count)
|
||||
|
||||
def parse_diff(diff_text: str) -> List[DiffFile]:
|
||||
"""
|
||||
[C: src/gui_2.py:App.request_patch_from_tier4, tests/test_diff_viewer.py:test_diff_line_classification, tests/test_diff_viewer.py:test_parse_diff_empty, tests/test_diff_viewer.py:test_parse_diff_none, tests/test_diff_viewer.py:test_parse_diff_with_context, tests/test_diff_viewer.py:test_parse_multiple_files, tests/test_diff_viewer.py:test_parse_simple_diff, tests/test_diff_viewer.py:test_render_diff_text_immediate]
|
||||
"""
|
||||
if not diff_text or not diff_text.strip():
|
||||
return []
|
||||
|
||||
@@ -132,6 +138,9 @@ def format_diff_for_display(diff_files: List[DiffFile]) -> str:
|
||||
return "\n".join(output)
|
||||
|
||||
def get_line_color(line: str) -> Optional[str]:
|
||||
"""
|
||||
[C: tests/test_diff_viewer.py:test_get_line_color]
|
||||
"""
|
||||
if line.startswith("+"):
|
||||
return "green"
|
||||
elif line.startswith("-"):
|
||||
@@ -141,6 +150,9 @@ def get_line_color(line: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
def render_diff_text_immediate(diff_files: List[DiffFile]) -> List[tuple[str, Optional[str]]]:
|
||||
"""
|
||||
[C: tests/test_diff_viewer.py:test_render_diff_text_immediate]
|
||||
"""
|
||||
output: List[tuple[str, Optional[str]]] = []
|
||||
for df in diff_files:
|
||||
output.append((f"File: {df.old_path}", "white"))
|
||||
@@ -152,6 +164,9 @@ def render_diff_text_immediate(diff_files: List[DiffFile]) -> List[tuple[str, Op
|
||||
return output
|
||||
|
||||
def create_backup(file_path: str) -> Optional[str]:
|
||||
"""
|
||||
[C: tests/test_diff_viewer.py:test_create_backup, tests/test_diff_viewer.py:test_create_backup_nonexistent]
|
||||
"""
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
return None
|
||||
@@ -160,6 +175,9 @@ def create_backup(file_path: str) -> Optional[str]:
|
||||
return str(backup_path)
|
||||
|
||||
def apply_patch_to_file(patch_text: str, base_dir: str = ".") -> Tuple[bool, str]:
|
||||
"""
|
||||
[C: src/gui_2.py:App._apply_pending_patch, tests/test_diff_viewer.py:test_apply_patch_simple, tests/test_diff_viewer.py:test_apply_patch_with_context]
|
||||
"""
|
||||
import difflib
|
||||
|
||||
diff_files = parse_diff(patch_text)
|
||||
@@ -207,6 +225,9 @@ def apply_patch_to_file(patch_text: str, base_dir: str = ".") -> Tuple[bool, str
|
||||
return True, "\n".join(results)
|
||||
|
||||
def restore_from_backup(file_path: str) -> bool:
|
||||
"""
|
||||
[C: tests/test_diff_viewer.py:test_restore_from_backup]
|
||||
"""
|
||||
backup_path = Path(str(file_path) + ".backup")
|
||||
if not backup_path.exists():
|
||||
return False
|
||||
@@ -215,6 +236,9 @@ def restore_from_backup(file_path: str) -> bool:
|
||||
return True
|
||||
|
||||
def cleanup_backup(file_path: str) -> None:
|
||||
"""
|
||||
[C: tests/test_diff_viewer.py:test_cleanup_backup]
|
||||
"""
|
||||
backup_path = Path(str(file_path) + ".backup")
|
||||
if backup_path.exists():
|
||||
backup_path.unlink()
|
||||
backup_path.unlink()
|
||||
+59
-28
File diff suppressed because one or more lines are too long
@@ -12,9 +12,15 @@ from src.models import ExternalEditorConfig, TextEditorConfig
|
||||
|
||||
class ExternalEditorLauncher:
|
||||
def __init__(self, config: ExternalEditorConfig):
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
def get_editor(self, editor_name: Optional[str] = None) -> Optional[TextEditorConfig]:
|
||||
"""
|
||||
[C: tests/test_external_editor.py:TestExternalEditorLauncher.test_get_editor_by_name, tests/test_external_editor.py:TestExternalEditorLauncher.test_get_editor_returns_default, tests/test_external_editor.py:TestExternalEditorLauncher.test_get_editor_unknown_name]
|
||||
"""
|
||||
if editor_name:
|
||||
return self.config.editors.get(editor_name)
|
||||
return self.config.get_default()
|
||||
@@ -22,12 +28,18 @@ class ExternalEditorLauncher:
|
||||
def build_diff_command(
|
||||
self, editor: TextEditorConfig, original_path: str, modified_path: str
|
||||
) -> List[str]:
|
||||
"""
|
||||
[C: tests/test_external_editor.py:TestExternalEditorLauncher.test_build_diff_command, tests/test_external_editor_gui.py:test_verify_command_format, tests/test_external_editor_gui.py:test_verify_vscode_command_format]
|
||||
"""
|
||||
cmd = [editor.path] + editor.diff_args + [original_path, modified_path]
|
||||
return cmd
|
||||
|
||||
def launch_diff(
|
||||
self, editor_name: Optional[str], original_path: str, modified_path: str
|
||||
) -> Optional[subprocess.Popen]:
|
||||
"""
|
||||
[C: src/gui_2.py:App._open_patch_in_external_editor, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_file_not_found, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_missing_editor, tests/test_external_editor.py:TestExternalEditorLauncher.test_launch_diff_success]
|
||||
"""
|
||||
editor = self.get_editor(editor_name)
|
||||
if not editor:
|
||||
return None
|
||||
@@ -104,6 +116,9 @@ def auto_detect_vscode() -> Optional[TextEditorConfig]:
|
||||
|
||||
|
||||
def get_default_launcher() -> ExternalEditorLauncher:
|
||||
"""
|
||||
[C: src/gui_2.py:App._open_patch_in_external_editor, src/gui_2.py:App._render_external_editor_panel]
|
||||
"""
|
||||
from src import models
|
||||
config = models.load_config()
|
||||
editors_config = config.get("tools", {}).get("text_editors", {})
|
||||
@@ -137,6 +152,9 @@ def resolve_project_editor_override(project_path: Optional[str]) -> Optional[str
|
||||
|
||||
|
||||
def create_temp_modified_file(content: str) -> str:
|
||||
"""
|
||||
[C: src/gui_2.py:App._open_patch_in_external_editor, tests/test_external_editor.py:TestHelperFunctions.test_create_temp_modified_file]
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix="_modified", delete=False, encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return f.name
|
||||
+59
-21
@@ -46,11 +46,15 @@ _ast_cache: Dict[str, Tuple[float, tree_sitter.Tree]] = {}
|
||||
|
||||
class ASTParser:
|
||||
"""
|
||||
Parser for extracting AST-based views of source code.
|
||||
Currently supports Python.
|
||||
"""
|
||||
|
||||
Parser for extracting AST-based views of source code.
|
||||
Currently supports Python.
|
||||
"""
|
||||
|
||||
def __init__(self, language: str) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
if language not in ("python", "cpp", "c"):
|
||||
raise ValueError(f"Language '{language}' not supported yet.")
|
||||
self.language_name = language
|
||||
@@ -64,7 +68,10 @@ class ASTParser:
|
||||
self.parser = tree_sitter.Parser(self.language)
|
||||
|
||||
def parse(self, code: str) -> tree_sitter.Tree:
|
||||
"""Parse the given code and return the tree-sitter Tree."""
|
||||
"""
|
||||
Parse the given code and return the tree-sitter Tree.
|
||||
[C: src/mcp_client.py:_search_file, src/mcp_client.py:derive_code_path, src/mcp_client.py:py_check_syntax, src/mcp_client.py:py_get_class_summary, src/mcp_client.py:py_get_definition, src/mcp_client.py:py_get_docstring, src/mcp_client.py:py_get_imports, src/mcp_client.py:py_get_signature, src/mcp_client.py:py_get_symbol_info, src/mcp_client.py:py_get_var_declaration, src/mcp_client.py:py_set_signature, src/mcp_client.py:py_set_var_declaration, src/mcp_client.py:py_update_definition, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/rag_engine.py:RAGEngine._chunk_code, src/summarize.py:_summarise_python, tests/test_ast_parser.py:test_ast_parser_parse, tests/test_tree_sitter_setup.py:test_tree_sitter_python_setup]
|
||||
"""
|
||||
return self.parser.parse(bytes(code, "utf8"))
|
||||
|
||||
def get_cached_tree(self, path: Optional[str], code: str) -> tree_sitter.Tree:
|
||||
@@ -129,8 +136,10 @@ class ASTParser:
|
||||
|
||||
def get_skeleton(self, code: str, path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
|
||||
"""
|
||||
|
||||
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
|
||||
[C: src/mcp_client.py:py_get_skeleton, src/mcp_client.py:ts_c_get_skeleton, src/mcp_client.py:ts_cpp_get_skeleton, src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_ast_parser.py:test_ast_parser_get_skeleton_c, tests/test_ast_parser.py:test_ast_parser_get_skeleton_cpp, tests/test_ast_parser.py:test_ast_parser_get_skeleton_python, tests/test_context_pruner.py:test_ast_caching, tests/test_context_pruner.py:test_performance_large_file]
|
||||
"""
|
||||
code_bytes = code.encode("utf8")
|
||||
tree = self.get_cached_tree(path, code)
|
||||
edits: List[Tuple[int, int, str]] = []
|
||||
@@ -142,6 +151,9 @@ class ASTParser:
|
||||
return False
|
||||
|
||||
def walk(node: tree_sitter.Node) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_search_file, src/mcp_client.py:py_find_usages, src/mcp_client.py:py_get_hierarchy, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/outline_tool.py:CodeOutliner.walk, src/summarize.py:_summarise_python]
|
||||
"""
|
||||
if node.type == "function_definition":
|
||||
body = node.child_by_field_name("body")
|
||||
if body and body.type in ("block", "compound_statement"):
|
||||
@@ -178,10 +190,12 @@ class ASTParser:
|
||||
|
||||
def get_curated_view(self, code: str, path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Returns a curated skeleton of a Python file.
|
||||
Preserves function bodies if they have @core_logic decorator or # [HOT] comment.
|
||||
Otherwise strips bodies but preserves docstrings.
|
||||
"""
|
||||
|
||||
Returns a curated skeleton of a Python file.
|
||||
Preserves function bodies if they have @core_logic decorator or # [HOT] comment.
|
||||
Otherwise strips bodies but preserves docstrings.
|
||||
[C: src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_ast_parser.py:test_ast_parser_get_curated_view]
|
||||
"""
|
||||
code_bytes = code.encode("utf8")
|
||||
tree = self.get_cached_tree(path, code)
|
||||
edits: List[Tuple[int, int, str]] = []
|
||||
@@ -217,6 +231,9 @@ class ASTParser:
|
||||
return False
|
||||
|
||||
def walk(node: tree_sitter.Node) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_search_file, src/mcp_client.py:py_find_usages, src/mcp_client.py:py_get_hierarchy, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/outline_tool.py:CodeOutliner.walk, src/summarize.py:_summarise_python]
|
||||
"""
|
||||
if node.type == "function_definition":
|
||||
body = node.child_by_field_name("body")
|
||||
if body and body.type in ("block", "compound_statement"):
|
||||
@@ -250,9 +267,11 @@ class ASTParser:
|
||||
|
||||
def get_targeted_view(self, code: str, function_names: List[str], path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Returns a targeted view of the code including only the specified functions
|
||||
and their dependencies up to depth 2.
|
||||
"""
|
||||
|
||||
Returns a targeted view of the code including only the specified functions
|
||||
and their dependencies up to depth 2.
|
||||
[C: src/multi_agent_conductor.py:run_worker_lifecycle, tests/test_ast_parser.py:test_ast_parser_get_targeted_view, tests/test_context_pruner.py:test_class_targeted_extraction, tests/test_context_pruner.py:test_targeted_extraction]
|
||||
"""
|
||||
code_bytes = code.encode("utf8")
|
||||
tree = self.get_cached_tree(path, code)
|
||||
all_functions = {}
|
||||
@@ -412,8 +431,10 @@ class ASTParser:
|
||||
|
||||
def get_definition(self, code: str, name: str, path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Returns the full source code for a specific definition by name.
|
||||
Supports 'ClassName::method' or 'method' for C++.
|
||||
|
||||
Returns the full source code for a specific definition by name.
|
||||
Supports 'ClassName::method' or 'method' for C++.
|
||||
[C: src/mcp_client.py:trace, src/mcp_client.py:ts_c_get_definition, src/mcp_client.py:ts_cpp_get_definition, tests/test_ast_parser.py:test_ast_parser_get_definition_c, tests/test_ast_parser.py:test_ast_parser_get_definition_cpp, tests/test_ast_parser.py:test_ast_parser_get_definition_cpp_template]
|
||||
"""
|
||||
code_bytes = code.encode("utf8")
|
||||
tree = self.get_cached_tree(path, code)
|
||||
@@ -421,6 +442,9 @@ class ASTParser:
|
||||
parts = re.split(r'::|\.', name)
|
||||
|
||||
def walk(node: tree_sitter.Node, target_parts: List[str]) -> Optional[tree_sitter.Node]:
|
||||
"""
|
||||
[C: src/mcp_client.py:_search_file, src/mcp_client.py:py_find_usages, src/mcp_client.py:py_get_hierarchy, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/outline_tool.py:CodeOutliner.walk, src/summarize.py:_summarise_python]
|
||||
"""
|
||||
if not target_parts:
|
||||
return None
|
||||
target = target_parts[0]
|
||||
@@ -480,8 +504,10 @@ class ASTParser:
|
||||
|
||||
def get_signature(self, code: str, name: str, path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Returns only the signature part of a function or method.
|
||||
For C/C++, this is the code from the start of the definition until the block start '{'.
|
||||
|
||||
Returns only the signature part of a function or method.
|
||||
For C/C++, this is the code from the start of the definition until the block start '{'.
|
||||
[C: src/mcp_client.py:ts_c_get_signature, src/mcp_client.py:ts_cpp_get_signature, tests/test_ast_parser.py:test_ast_parser_get_signature_c, tests/test_ast_parser.py:test_ast_parser_get_signature_cpp]
|
||||
"""
|
||||
code_bytes = code.encode("utf8")
|
||||
tree = self.get_cached_tree(path, code)
|
||||
@@ -489,6 +515,9 @@ class ASTParser:
|
||||
parts = re.split(r'::|\.', name)
|
||||
|
||||
def walk(node: tree_sitter.Node, target_parts: List[str]) -> Optional[tree_sitter.Node]:
|
||||
"""
|
||||
[C: src/mcp_client.py:_search_file, src/mcp_client.py:py_find_usages, src/mcp_client.py:py_get_hierarchy, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/outline_tool.py:CodeOutliner.walk, src/summarize.py:_summarise_python]
|
||||
"""
|
||||
if not target_parts:
|
||||
return None
|
||||
target = target_parts[0]
|
||||
@@ -559,13 +588,18 @@ class ASTParser:
|
||||
|
||||
def get_code_outline(self, code: str, path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Returns a hierarchical outline of the code (classes, structs, functions, methods).
|
||||
"""
|
||||
|
||||
Returns a hierarchical outline of the code (classes, structs, functions, methods).
|
||||
[C: src/mcp_client.py:ts_c_get_code_outline, src/mcp_client.py:ts_cpp_get_code_outline, tests/test_ast_parser.py:test_ast_parser_get_code_outline_c, tests/test_ast_parser.py:test_ast_parser_get_code_outline_cpp]
|
||||
"""
|
||||
code_bytes = code.encode("utf8")
|
||||
tree = self.get_cached_tree(path, code)
|
||||
output = []
|
||||
|
||||
def walk(node: tree_sitter.Node, indent: int = 0) -> None:
|
||||
"""
|
||||
[C: src/mcp_client.py:_search_file, src/mcp_client.py:py_find_usages, src/mcp_client.py:py_get_hierarchy, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/outline_tool.py:CodeOutliner.walk, src/summarize.py:_summarise_python]
|
||||
"""
|
||||
ntype = node.type
|
||||
label = ""
|
||||
if ntype in ("class_definition", "class_specifier"):
|
||||
@@ -595,7 +629,9 @@ class ASTParser:
|
||||
|
||||
def update_definition(self, code: str, name: str, new_content: str, path: Optional[str] = None) -> str:
|
||||
"""
|
||||
Surgically replace the definition of a class or function by name.
|
||||
|
||||
Surgically replace the definition of a class or function by name.
|
||||
[C: src/mcp_client.py:ts_c_update_definition, src/mcp_client.py:ts_cpp_update_definition, tests/test_ast_parser.py:test_ast_parser_update_definition_cpp]
|
||||
"""
|
||||
code_bytes = code.encode("utf8")
|
||||
tree = self.get_cached_tree(path, code)
|
||||
@@ -603,6 +639,9 @@ class ASTParser:
|
||||
parts = re.split(r'::|\.', name)
|
||||
|
||||
def walk(node: tree_sitter.Node, target_parts: List[str]) -> Optional[tree_sitter.Node]:
|
||||
"""
|
||||
[C: src/mcp_client.py:_search_file, src/mcp_client.py:py_find_usages, src/mcp_client.py:py_get_hierarchy, src/mcp_client.py:trace, src/outline_tool.py:CodeOutliner.outline, src/outline_tool.py:CodeOutliner.walk, src/summarize.py:_summarise_python]
|
||||
"""
|
||||
if not target_parts:
|
||||
return None
|
||||
target = target_parts[0]
|
||||
@@ -677,4 +716,3 @@ def evict(path: Path) -> None:
|
||||
def list_cached() -> List[Dict[str, Any]]:
|
||||
return []
|
||||
|
||||
|
||||
|
||||
+17
-10
@@ -44,10 +44,14 @@ from typing import Optional, Callable, Any
|
||||
|
||||
class GeminiCliAdapter:
|
||||
"""
|
||||
Adapter for the Gemini CLI that parses streaming JSON output.
|
||||
"""
|
||||
|
||||
Adapter for the Gemini CLI that parses streaming JSON output.
|
||||
"""
|
||||
def __init__(self, binary_path: str = "gemini"):
|
||||
"""Initializes the adapter with the path to the gemini CLI executable."""
|
||||
"""
|
||||
Initializes the adapter with the path to the gemini CLI executable.
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.binary_path = binary_path
|
||||
self.session_id: Optional[str] = None
|
||||
self.last_usage: Optional[dict[str, Any]] = None
|
||||
@@ -56,9 +60,11 @@ class GeminiCliAdapter:
|
||||
def send(self, message: str, safety_settings: list[Any] | None = None, system_instruction: str | None = None,
|
||||
model: str | None = None, stream_callback: Optional[Callable[[str], None]] = None) -> dict[str, Any]:
|
||||
"""
|
||||
Sends a message to the Gemini CLI and processes the streaming JSON output.
|
||||
Uses non-blocking line-by-line reading to allow stream_callback.
|
||||
"""
|
||||
|
||||
Sends a message to the Gemini CLI and processes the streaming JSON output.
|
||||
Uses non-blocking line-by-line reading to allow stream_callback.
|
||||
[C: simulation/user_agent.py:UserSimAgent.generate_response, src/multi_agent_conductor.py:run_worker_lifecycle, src/native_orchestrator.py:NativeOrchestrator.execute_ticket, src/orchestrator_pm.py:generate_tracks, tests/test_ai_cache_tracking.py:test_gemini_cache_tracking, tests/test_ai_client_cli.py:test_ai_client_send_gemini_cli, tests/test_api_events.py:test_send_emits_events_proper, tests/test_api_events.py:test_send_emits_tool_events, tests/test_deepseek_provider.py:test_deepseek_completion_logic, tests/test_deepseek_provider.py:test_deepseek_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoner_payload_verification, tests/test_deepseek_provider.py:test_deepseek_reasoning_logic, tests/test_deepseek_provider.py:test_deepseek_streaming, tests/test_deepseek_provider.py:test_deepseek_tool_calling, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_full_flow_integration, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_captures_usage_metadata, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_handles_tool_use_events, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_parses_jsonl_output, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_starts_subprocess_with_correct_args, tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_send_parses_tool_calls_from_streaming_json, tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_send_starts_subprocess_with_model, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_context_bleed_prevention, tests/test_gemini_cli_edge_cases.py:test_gemini_cli_loop_termination, tests/test_gemini_cli_integration.py:test_gemini_cli_full_integration, tests/test_gemini_cli_integration.py:test_gemini_cli_rejection_and_history, tests/test_gemini_cli_parity_regression.py:test_get_history_bleed_stats, tests/test_gemini_cli_parity_regression.py:test_send_invokes_adapter_send, tests/test_gui2_mcp.py:test_mcp_tool_call_is_dispatched, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_token_usage.py:test_token_usage_tracking, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
start_time = time.time()
|
||||
command_parts = [self.binary_path]
|
||||
if model:
|
||||
@@ -185,9 +191,10 @@ class GeminiCliAdapter:
|
||||
|
||||
def count_tokens(self, contents: list[str]) -> int:
|
||||
"""
|
||||
Provides a character-based token estimation for the Gemini CLI.
|
||||
Uses 4 chars/token as a conservative average.
|
||||
"""
|
||||
|
||||
Provides a character-based token estimation for the Gemini CLI.
|
||||
Uses 4 chars/token as a conservative average.
|
||||
[C: tests/test_gemini_cli_adapter_parity.py:TestGeminiCliAdapterParity.test_count_tokens_fallback]
|
||||
"""
|
||||
total_chars = len("\n".join(contents))
|
||||
return total_chars // 4
|
||||
|
||||
|
||||
+60
-4
@@ -103,6 +103,9 @@ class App:
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Initialize controller and delegate state
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.controller = app_controller.AppController()
|
||||
self.controller._app = self
|
||||
from src import history
|
||||
@@ -429,7 +432,10 @@ class App:
|
||||
self._apply_snapshot(entry.state)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Cleanly shuts down the app's background tasks and saves state."""
|
||||
"""
|
||||
Cleanly shuts down the app's background tasks and saves state.
|
||||
[C: tests/conftest.py:app_instance, tests/conftest.py:mock_app]
|
||||
"""
|
||||
try:
|
||||
if hasattr(self, 'runner_params') and self.runner_params.ini_filename:
|
||||
imgui.save_ini_settings_to_disk(self.runner_params.ini_filename)
|
||||
@@ -438,6 +444,9 @@ class App:
|
||||
self.controller.shutdown()
|
||||
|
||||
def save_context_preset(self, name: str) -> None:
|
||||
"""
|
||||
[C: tests/test_context_presets.py:test_save_context_preset]
|
||||
"""
|
||||
sys.stderr.write(f"[DEBUG] save_context_preset called with: {name}\n")
|
||||
sys.stderr.flush()
|
||||
if 'context_presets' not in self.controller.project:
|
||||
@@ -451,6 +460,9 @@ class App:
|
||||
sys.stderr.flush()
|
||||
|
||||
def load_context_preset(self, name: str) -> None:
|
||||
"""
|
||||
[C: tests/test_context_presets.py:test_load_context_preset, tests/test_context_presets.py:test_load_nonexistent_preset]
|
||||
"""
|
||||
presets = self.controller.project.get('context_presets', {})
|
||||
if name in presets:
|
||||
preset = presets[name]
|
||||
@@ -458,6 +470,9 @@ class App:
|
||||
self.screenshots = list(preset.get('screenshots', []))
|
||||
|
||||
def delete_context_preset(self, name: str) -> None:
|
||||
"""
|
||||
[C: tests/test_context_presets.py:test_delete_context_preset, tests/test_context_presets.py:test_delete_nonexistent_preset_no_error]
|
||||
"""
|
||||
if 'context_presets' in self.controller.project:
|
||||
self.controller.project['context_presets'].pop(name, None)
|
||||
self.controller._save_active_project()
|
||||
@@ -597,6 +612,9 @@ class App:
|
||||
imgui.pop_id()
|
||||
|
||||
def _show_menus(self) -> None:
|
||||
"""
|
||||
[C: tests/test_gui_window_controls.py:test_gui_window_controls_minimize_maximize_close]
|
||||
"""
|
||||
if imgui.begin_menu("manual slop"):
|
||||
if imgui.menu_item("Quit", "Ctrl+Q", False)[0]:
|
||||
self.runner_params.app_shall_exit = True
|
||||
@@ -719,6 +737,9 @@ class App:
|
||||
pass
|
||||
|
||||
def _render_shader_live_editor(self) -> None:
|
||||
"""
|
||||
[C: tests/test_shader_live_editor.py:test_shader_live_editor_renders]
|
||||
"""
|
||||
if self.show_windows.get('Shader Editor', False):
|
||||
exp, opened = imgui.begin('Shader Editor', self.show_windows['Shader Editor'])
|
||||
self.show_windows['Shader Editor'] = bool(opened)
|
||||
@@ -2044,6 +2065,9 @@ class App:
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel")
|
||||
|
||||
def _save_paths(self):
|
||||
"""
|
||||
[C: tests/test_gui_paths.py:test_save_paths]
|
||||
"""
|
||||
self.config["paths"] = {
|
||||
"logs_dir": self.ui_logs_dir,
|
||||
"scripts_dir": self.ui_scripts_dir
|
||||
@@ -2295,6 +2319,9 @@ class App:
|
||||
self._patch_error_message = str(e)
|
||||
|
||||
def _render_log_management(self) -> None:
|
||||
"""
|
||||
[C: tests/test_log_management_ui.py:test_render_log_management_logic]
|
||||
"""
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_log_management")
|
||||
exp, opened = imgui.begin("Log Management", self.show_windows["Log Management"])
|
||||
self.show_windows["Log Management"] = bool(opened)
|
||||
@@ -2797,6 +2824,9 @@ def hello():
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_screenshots_panel")
|
||||
|
||||
def _render_discussion_panel(self) -> None:
|
||||
"""
|
||||
[C: tests/test_discussion_takes_gui.py:test_render_discussion_tabs, tests/test_discussion_takes_gui.py:test_switching_discussion_via_tabs, tests/test_gui_discussion_tabs.py:test_discussion_tabs_rendered, tests/test_gui_phase4.py:test_track_discussion_toggle, tests/test_gui_symbol_navigation.py:test_render_discussion_panel_symbol_lookup]
|
||||
"""
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel")
|
||||
# THINKING indicator
|
||||
is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...']
|
||||
@@ -3175,7 +3205,10 @@ def hello():
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel")
|
||||
|
||||
def _render_synthesis_panel(self) -> None:
|
||||
"""Renders a panel for synthesizing multiple discussion takes."""
|
||||
"""
|
||||
Renders a panel for synthesizing multiple discussion takes.
|
||||
[C: tests/test_gui_synthesis.py:test_render_synthesis_panel]
|
||||
"""
|
||||
imgui.text("Select takes to synthesize:")
|
||||
discussions = self.project.get('discussion', {}).get('discussions', {})
|
||||
if not hasattr(self, 'ui_synthesis_selected_takes'):
|
||||
@@ -3917,18 +3950,27 @@ def hello():
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_calls_panel")
|
||||
|
||||
def bulk_execute(self) -> None:
|
||||
"""
|
||||
[C: tests/test_ticket_queue.py:TestBulkOperations.test_bulk_execute]
|
||||
"""
|
||||
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'] = 'in_progress'
|
||||
self._push_mma_state_update()
|
||||
|
||||
def bulk_skip(self) -> None:
|
||||
"""
|
||||
[C: tests/test_ticket_queue.py:TestBulkOperations.test_bulk_skip]
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
[C: tests/test_ticket_queue.py:TestBulkOperations.test_bulk_block]
|
||||
"""
|
||||
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'
|
||||
@@ -3980,6 +4022,9 @@ def hello():
|
||||
self._push_mma_state_update()
|
||||
|
||||
def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None:
|
||||
"""
|
||||
[C: tests/test_ticket_queue.py:TestReorder.test_reorder_ticket_invalid, tests/test_ticket_queue.py:TestReorder.test_reorder_ticket_valid]
|
||||
"""
|
||||
if src_idx == dst_idx: return
|
||||
new_tickets = list(self.active_tickets)
|
||||
ticket = new_tickets.pop(src_idx)
|
||||
@@ -3999,6 +4044,9 @@ def hello():
|
||||
self._push_mma_state_update()
|
||||
|
||||
def _render_ticket_queue(self) -> None:
|
||||
"""
|
||||
[C: tests/test_gui_kill_button.py:test_render_ticket_queue_table_columns]
|
||||
"""
|
||||
imgui.text("Ticket Queue Management")
|
||||
if not self.active_track:
|
||||
imgui.text_disabled("No active track.")
|
||||
@@ -4126,6 +4174,9 @@ def hello():
|
||||
imgui.end_table()
|
||||
|
||||
def _render_mma_dashboard(self) -> None:
|
||||
"""
|
||||
[C: tests/test_gui_progress.py:test_render_mma_dashboard_progress, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_ask_dialog_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_mma_approval_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_spawn_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_no_approval_badge_when_idle]
|
||||
"""
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard")
|
||||
|
||||
# Focus Agent dropdown
|
||||
@@ -4734,6 +4785,9 @@ def hello():
|
||||
imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"Error loading beads: {e}")
|
||||
|
||||
def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None:
|
||||
"""
|
||||
[C: tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier1_renders_stream_content, tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier3_renders_worker_subheaders]
|
||||
"""
|
||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tier_stream_panel")
|
||||
if self.is_viewing_prior_session:
|
||||
imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY")
|
||||
@@ -5135,7 +5189,10 @@ def hello():
|
||||
theme.apply_current()
|
||||
|
||||
def run(self) -> None:
|
||||
"""Initializes the ImGui runner and starts the main application loop."""
|
||||
"""
|
||||
Initializes the ImGui runner and starts the main application loop.
|
||||
[C: simulation/sim_base.py:run_sim, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/project_manager.py:get_git_log, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
"""
|
||||
if "--headless" in sys.argv:
|
||||
print("Headless mode active")
|
||||
self._fetch_models(self.current_provider)
|
||||
@@ -5202,4 +5259,3 @@ def main() -> None:
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
+30
-10
@@ -19,6 +19,9 @@ class UISnapshot:
|
||||
screenshots: list[str]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
[C: src/models.py:ExternalEditorConfig.to_dict, src/models.py:MCPConfiguration.to_dict, src/models.py:RAGConfig.to_dict, src/models.py:ToolPreset.to_dict, src/models.py:Track.to_dict, src/models.py:TrackState.to_dict, src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags]
|
||||
"""
|
||||
return {
|
||||
"ai_input": self.ai_input,
|
||||
"project_system_prompt": self.project_system_prompt,
|
||||
@@ -36,6 +39,9 @@ class UISnapshot:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "UISnapshot":
|
||||
"""
|
||||
[C: src/models.py:ExternalEditorConfig.from_dict, src/models.py:MCPConfiguration.from_dict, src/models.py:RAGConfig.from_dict, src/models.py:ToolPreset.from_dict, src/models.py:Track.from_dict, src/models.py:TrackState.from_dict, src/models.py:load_mcp_config, src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags]
|
||||
"""
|
||||
return cls(
|
||||
ai_input=data.get("ai_input", ""),
|
||||
project_system_prompt=data.get("project_system_prompt", ""),
|
||||
@@ -59,14 +65,19 @@ class HistoryEntry:
|
||||
|
||||
class HistoryManager:
|
||||
def __init__(self, max_capacity: int = 100):
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.max_capacity = max_capacity
|
||||
self._undo_stack: typing.List[HistoryEntry] = []
|
||||
self._redo_stack: typing.List[HistoryEntry] = []
|
||||
|
||||
def push(self, state: typing.Any, description: str) -> None:
|
||||
"""
|
||||
Pushes a new state to the undo stack and clears the redo stack.
|
||||
If the undo stack exceeds max_capacity, the oldest state is removed.
|
||||
|
||||
Pushes a new state to the undo stack and clears the redo stack.
|
||||
If the undo stack exceeds max_capacity, the oldest state is removed.
|
||||
[C: tests/test_history.py:test_jump_to_undo, tests/test_history.py:test_max_capacity, tests/test_history.py:test_push_state, tests/test_history.py:test_redo_cleared_on_push, tests/test_history.py:test_undo_redo]
|
||||
"""
|
||||
entry = HistoryEntry(state=state, description=description)
|
||||
self._undo_stack.append(entry)
|
||||
@@ -76,8 +87,10 @@ class HistoryManager:
|
||||
|
||||
def undo(self, current_state: typing.Any, current_description: str = "Current State") -> typing.Optional[HistoryEntry]:
|
||||
"""
|
||||
Undoes the last action by moving the current_state to the redo stack
|
||||
and returning the top of the undo stack.
|
||||
|
||||
Undoes the last action by moving the current_state to the redo stack
|
||||
and returning the top of the undo stack.
|
||||
[C: tests/test_history.py:test_redo_cleared_on_push, tests/test_history.py:test_undo_redo]
|
||||
"""
|
||||
if not self._undo_stack:
|
||||
return None
|
||||
@@ -88,8 +101,10 @@ class HistoryManager:
|
||||
|
||||
def redo(self, current_state: typing.Any, current_description: str = "Current State") -> typing.Optional[HistoryEntry]:
|
||||
"""
|
||||
Redoes the last undone action by moving the current_state to the undo stack
|
||||
and returning the top of the redo stack.
|
||||
|
||||
Redoes the last undone action by moving the current_state to the undo stack
|
||||
and returning the top of the redo stack.
|
||||
[C: tests/test_history.py:test_undo_redo]
|
||||
"""
|
||||
if not self._redo_stack:
|
||||
return None
|
||||
@@ -107,7 +122,10 @@ class HistoryManager:
|
||||
return len(self._redo_stack) > 0
|
||||
|
||||
def get_history(self) -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
"""Returns a list of descriptions and timestamps for the undo stack."""
|
||||
"""
|
||||
Returns a list of descriptions and timestamps for the undo stack.
|
||||
[C: tests/test_history.py:test_initial_state, tests/test_history.py:test_push_state]
|
||||
"""
|
||||
return [
|
||||
{"description": e.description, "timestamp": e.timestamp}
|
||||
for e in self._undo_stack
|
||||
@@ -115,8 +133,10 @@ class HistoryManager:
|
||||
|
||||
def jump_to_undo(self, index: int, current_state: typing.Any, current_description: str = "Before Jump") -> typing.Optional[HistoryEntry]:
|
||||
"""
|
||||
Jumps to a specific state in the undo stack by moving subsequent states
|
||||
and the current_state to the redo stack.
|
||||
|
||||
Jumps to a specific state in the undo stack by moving subsequent states
|
||||
and the current_state to the redo stack.
|
||||
[C: tests/test_history.py:test_jump_to_undo]
|
||||
"""
|
||||
if index < 0 or index >= len(self._undo_stack):
|
||||
return None
|
||||
@@ -128,4 +148,4 @@ class HistoryManager:
|
||||
while len(self._undo_stack) > index + 1:
|
||||
self._redo_stack.append(self._undo_stack.pop())
|
||||
|
||||
return self._undo_stack.pop()
|
||||
return self._undo_stack.pop()
|
||||
+22
-18
@@ -7,31 +7,36 @@ from src.log_registry import LogRegistry
|
||||
|
||||
class LogPruner:
|
||||
"""
|
||||
Handles the automated deletion of old and insignificant session logs.
|
||||
Ensures that only whitelisted or significant sessions (based on size/content)
|
||||
are preserved long-term.
|
||||
"""
|
||||
|
||||
Handles the automated deletion of old and insignificant session logs.
|
||||
Ensures that only whitelisted or significant sessions (based on size/content)
|
||||
are preserved long-term.
|
||||
"""
|
||||
|
||||
def __init__(self, log_registry: LogRegistry, logs_dir: str) -> None:
|
||||
"""
|
||||
Initializes the LogPruner.
|
||||
|
||||
Args:
|
||||
log_registry: An instance of LogRegistry to check session data.
|
||||
logs_dir: The path to the directory containing session sub-directories.
|
||||
"""
|
||||
|
||||
Initializes the LogPruner.
|
||||
|
||||
Args:
|
||||
log_registry: An instance of LogRegistry to check session data.
|
||||
logs_dir: The path to the directory containing session sub-directories.
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.log_registry = log_registry
|
||||
self.logs_dir = logs_dir
|
||||
|
||||
def prune(self, max_age_days: int = 1, min_size_kb: int = 2) -> None:
|
||||
"""
|
||||
Prunes old and small session directories from the logs directory.
|
||||
|
||||
Deletes session directories that meet the following criteria:
|
||||
1. The session start time is older than max_age_days.
|
||||
2. The session name is NOT in the whitelist provided by the LogRegistry.
|
||||
3. The total size of all files within the session directory is less than min_size_kb.
|
||||
"""
|
||||
|
||||
Prunes old and small session directories from the logs directory.
|
||||
|
||||
Deletes session directories that meet the following criteria:
|
||||
1. The session start time is older than max_age_days.
|
||||
2. The session name is NOT in the whitelist provided by the LogRegistry.
|
||||
3. The total size of all files within the session directory is less than min_size_kb.
|
||||
[C: tests/test_log_pruner.py:test_prune_old_insignificant_logs, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_prune_handles_relative_paths_starting_with_logs, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_prune_removes_empty_sessions_regardless_of_age, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_prune_removes_sessions_without_metadata_regardless_of_age, tests/test_logging_e2e.py:test_logging_e2e]
|
||||
"""
|
||||
now = datetime.now()
|
||||
cutoff_time = now - timedelta(days=max_age_days)
|
||||
# Ensure the base logs directory exists.
|
||||
@@ -115,4 +120,3 @@ class LogPruner:
|
||||
sys.stderr.write(f"[LogPruner] Error removing {resolved_path}: {e}\n")
|
||||
|
||||
self.log_registry.save_registry()
|
||||
|
||||
|
||||
+72
-57
@@ -47,17 +47,20 @@ from typing import Any
|
||||
|
||||
class LogRegistry:
|
||||
"""
|
||||
Manages a persistent registry of session logs using a TOML file.
|
||||
Tracks session paths, start times, whitelisting status, and metadata.
|
||||
"""
|
||||
|
||||
Manages a persistent registry of session logs using a TOML file.
|
||||
Tracks session paths, start times, whitelisting status, and metadata.
|
||||
"""
|
||||
|
||||
def __init__(self, registry_path: str) -> None:
|
||||
"""
|
||||
Initializes the LogRegistry with a path to the registry file.
|
||||
|
||||
Args:
|
||||
registry_path (str): The file path to the TOML registry.
|
||||
"""
|
||||
|
||||
Initializes the LogRegistry with a path to the registry file.
|
||||
|
||||
Args:
|
||||
registry_path (str): The file path to the TOML registry.
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.registry_path = registry_path
|
||||
self.data: dict[str, dict[str, Any]] = {}
|
||||
self.load_registry()
|
||||
@@ -69,9 +72,10 @@ class LogRegistry:
|
||||
|
||||
def load_registry(self) -> None:
|
||||
"""
|
||||
Loads the registry data from the TOML file into memory.
|
||||
Handles date/time conversions from TOML-native formats to strings for consistency.
|
||||
"""
|
||||
|
||||
Loads the registry data from the TOML file into memory.
|
||||
Handles date/time conversions from TOML-native formats to strings for consistency.
|
||||
"""
|
||||
if os.path.exists(self.registry_path):
|
||||
try:
|
||||
with open(self.registry_path, 'rb') as f:
|
||||
@@ -97,9 +101,11 @@ class LogRegistry:
|
||||
|
||||
def save_registry(self) -> None:
|
||||
"""
|
||||
Serializes and saves the current registry data to the TOML file.
|
||||
Converts internal datetime objects to ISO format strings for compatibility.
|
||||
"""
|
||||
|
||||
Serializes and saves the current registry data to the TOML file.
|
||||
Converts internal datetime objects to ISO format strings for compatibility.
|
||||
[C: tests/test_logging_e2e.py:test_logging_e2e]
|
||||
"""
|
||||
try:
|
||||
# Convert datetime objects to ISO format strings for TOML serialization
|
||||
data_to_save: dict[str, Any] = {}
|
||||
@@ -130,13 +136,15 @@ class LogRegistry:
|
||||
|
||||
def register_session(self, session_id: str, path: str, start_time: datetime | str) -> None:
|
||||
"""
|
||||
Registers a new session in the registry.
|
||||
|
||||
Args:
|
||||
session_id (str): Unique identifier for the session.
|
||||
path (str): File path to the session's log directory.
|
||||
start_time (datetime|str): The timestamp when the session started.
|
||||
"""
|
||||
|
||||
Registers a new session in the registry.
|
||||
|
||||
Args:
|
||||
session_id (str): Unique identifier for the session.
|
||||
path (str): File path to the session's log directory.
|
||||
start_time (datetime|str): The timestamp when the session started.
|
||||
[C: src/session_logger.py:open_session, tests/test_auto_whitelist.py:test_auto_whitelist_keywords, tests/test_auto_whitelist.py:test_auto_whitelist_large_size, tests/test_auto_whitelist.py:test_auto_whitelist_message_count, tests/test_auto_whitelist.py:test_no_auto_whitelist_insignificant, tests/test_log_pruner.py:test_prune_old_insignificant_logs, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_get_old_non_whitelisted_sessions_includes_empty_sessions, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_get_old_non_whitelisted_sessions_includes_sessions_without_metadata, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_prune_handles_relative_paths_starting_with_logs, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_prune_removes_empty_sessions_regardless_of_age, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_prune_removes_sessions_without_metadata_regardless_of_age, tests/test_log_registry.py:TestLogRegistry.test_get_old_non_whitelisted_sessions, tests/test_log_registry.py:TestLogRegistry.test_is_session_whitelisted, tests/test_log_registry.py:TestLogRegistry.test_register_session, tests/test_log_registry.py:TestLogRegistry.test_update_session_metadata, tests/test_logging_e2e.py:test_logging_e2e]
|
||||
"""
|
||||
if session_id in self.data:
|
||||
print(f"Warning: Session ID '{session_id}' already exists. Overwriting.")
|
||||
# Store start_time internally as a string to satisfy tests
|
||||
@@ -154,16 +162,18 @@ class LogRegistry:
|
||||
|
||||
def update_session_metadata(self, session_id: str, message_count: int, errors: int, size_kb: int, whitelisted: bool, reason: str) -> None:
|
||||
"""
|
||||
Updates metadata fields for an existing session.
|
||||
|
||||
Args:
|
||||
session_id (str): Unique identifier for the session.
|
||||
message_count (int): Total number of messages in the session.
|
||||
errors (int): Number of errors identified in logs.
|
||||
size_kb (int): Total size of the session logs in kilobytes.
|
||||
whitelisted (bool): Whether the session should be protected from pruning.
|
||||
reason (str): Explanation for the current whitelisting status.
|
||||
"""
|
||||
|
||||
Updates metadata fields for an existing session.
|
||||
|
||||
Args:
|
||||
session_id (str): Unique identifier for the session.
|
||||
message_count (int): Total number of messages in the session.
|
||||
errors (int): Number of errors identified in logs.
|
||||
size_kb (int): Total size of the session logs in kilobytes.
|
||||
whitelisted (bool): Whether the session should be protected from pruning.
|
||||
reason (str): Explanation for the current whitelisting status.
|
||||
[C: tests/test_auto_whitelist.py:test_auto_whitelist_large_size, tests/test_auto_whitelist.py:test_auto_whitelist_message_count, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_get_old_non_whitelisted_sessions_includes_empty_sessions, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_prune_removes_empty_sessions_regardless_of_age, tests/test_log_registry.py:TestLogRegistry.test_get_old_non_whitelisted_sessions, tests/test_log_registry.py:TestLogRegistry.test_is_session_whitelisted, tests/test_log_registry.py:TestLogRegistry.test_update_session_metadata]
|
||||
"""
|
||||
if session_id not in self.data:
|
||||
print(f"Error: Session ID '{session_id}' not found for metadata update.")
|
||||
return
|
||||
@@ -186,14 +196,16 @@ class LogRegistry:
|
||||
|
||||
def is_session_whitelisted(self, session_id: str) -> bool:
|
||||
"""
|
||||
Checks if a specific session is marked as whitelisted.
|
||||
|
||||
Args:
|
||||
session_id (str): Unique identifier for the session.
|
||||
|
||||
Returns:
|
||||
bool: True if whitelisted, False otherwise.
|
||||
"""
|
||||
|
||||
Checks if a specific session is marked as whitelisted.
|
||||
|
||||
Args:
|
||||
session_id (str): Unique identifier for the session.
|
||||
|
||||
Returns:
|
||||
bool: True if whitelisted, False otherwise.
|
||||
[C: tests/test_auto_whitelist.py:test_auto_whitelist_keywords, tests/test_auto_whitelist.py:test_auto_whitelist_large_size, tests/test_auto_whitelist.py:test_auto_whitelist_message_count, tests/test_auto_whitelist.py:test_no_auto_whitelist_insignificant, tests/test_log_registry.py:TestLogRegistry.test_is_session_whitelisted, tests/test_logging_e2e.py:test_logging_e2e]
|
||||
"""
|
||||
session_data = self.data.get(session_id)
|
||||
if session_data is None:
|
||||
return False # Non-existent sessions are not whitelisted
|
||||
@@ -202,13 +214,15 @@ class LogRegistry:
|
||||
|
||||
def update_auto_whitelist_status(self, session_id: str) -> None:
|
||||
"""
|
||||
Analyzes session logs and updates whitelisting status based on heuristics.
|
||||
Sessions are automatically whitelisted if they contain error keywords,
|
||||
have a high message count, or exceed a size threshold.
|
||||
|
||||
Args:
|
||||
session_id (str): Unique identifier for the session to analyze.
|
||||
"""
|
||||
|
||||
Analyzes session logs and updates whitelisting status based on heuristics.
|
||||
Sessions are automatically whitelisted if they contain error keywords,
|
||||
have a high message count, or exceed a size threshold.
|
||||
|
||||
Args:
|
||||
session_id (str): Unique identifier for the session to analyze.
|
||||
[C: src/session_logger.py:close_session]
|
||||
"""
|
||||
if session_id not in self.data:
|
||||
return
|
||||
session_data = self.data[session_id]
|
||||
@@ -260,16 +274,18 @@ class LogRegistry:
|
||||
|
||||
def get_old_non_whitelisted_sessions(self, cutoff_datetime: datetime) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Retrieves a list of sessions that are older than a specific cutoff time
|
||||
and are not marked as whitelisted.
|
||||
Also includes non-whitelisted sessions that are empty (message_count=0 or size_kb=0).
|
||||
|
||||
Args:
|
||||
cutoff_datetime (datetime): The threshold time for identifying old sessions.
|
||||
|
||||
Returns:
|
||||
list: A list of dictionaries containing session details (id, path, start_time).
|
||||
"""
|
||||
|
||||
Retrieves a list of sessions that are older than a specific cutoff time
|
||||
and are not marked as whitelisted.
|
||||
Also includes non-whitelisted sessions that are empty (message_count=0 or size_kb=0).
|
||||
|
||||
Args:
|
||||
cutoff_datetime (datetime): The threshold time for identifying old sessions.
|
||||
|
||||
Returns:
|
||||
list: A list of dictionaries containing session details (id, path, start_time).
|
||||
[C: tests/test_log_pruner.py:test_prune_old_insignificant_logs, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_get_old_non_whitelisted_sessions_includes_empty_sessions, tests/test_log_pruning_heuristic.py:TestLogPruningHeuristic.test_get_old_non_whitelisted_sessions_includes_sessions_without_metadata, tests/test_log_registry.py:TestLogRegistry.test_get_old_non_whitelisted_sessions]
|
||||
"""
|
||||
old_sessions = []
|
||||
for session_id, session_data in self.data.items():
|
||||
# Check if session is older than cutoff and not whitelisted
|
||||
@@ -301,4 +317,3 @@ class LogRegistry:
|
||||
})
|
||||
return old_sessions
|
||||
|
||||
|
||||
|
||||
+14
-5
@@ -9,10 +9,14 @@ from typing import Optional, Dict, Callable
|
||||
|
||||
class MarkdownRenderer:
|
||||
"""
|
||||
Hybrid Markdown renderer that uses imgui_md for text/headers
|
||||
and ImGuiColorTextEdit for syntax-highlighted code blocks.
|
||||
"""
|
||||
|
||||
Hybrid Markdown renderer that uses imgui_md for text/headers
|
||||
and ImGuiColorTextEdit for syntax-highlighted code blocks.
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
[C: src/mcp_client.py:_DDGParser.__init__, src/mcp_client.py:_TextExtractor.__init__]
|
||||
"""
|
||||
self.options = imgui_md.MarkdownOptions()
|
||||
# Base path for fonts (Inter family)
|
||||
self.options.font_options.font_base_path = "fonts/Inter"
|
||||
@@ -62,7 +66,10 @@ class MarkdownRenderer:
|
||||
print(f"Error opening link {url}: {e}")
|
||||
|
||||
def render(self, text: str, context_id: str = "default") -> None:
|
||||
"""Render Markdown text with code block interception."""
|
||||
"""
|
||||
Render Markdown text with code block interception.
|
||||
[C: tests/test_theme_nerv_alert.py:test_alert_pulsing_render_active, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_inactive, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_render, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_disabled, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_render]
|
||||
"""
|
||||
if not text:
|
||||
return
|
||||
|
||||
@@ -159,6 +166,9 @@ def get_renderer() -> MarkdownRenderer:
|
||||
return _renderer
|
||||
|
||||
def render(text: str, context_id: str = "default") -> None:
|
||||
"""
|
||||
[C: tests/test_theme_nerv_alert.py:test_alert_pulsing_render_active, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_inactive, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_render, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_disabled, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_render]
|
||||
"""
|
||||
get_renderer().render(text, context_id)
|
||||
|
||||
def render_unindented(text: str) -> None:
|
||||
@@ -166,4 +176,3 @@ def render_unindented(text: str) -> None:
|
||||
|
||||
def render_code(code: str, lang: str = "", context_id: str = "default", block_idx: int = 0) -> None:
|
||||
get_renderer().render_code(code, lang, context_id, block_idx)
|
||||
|
||||
|
||||
+119
-57
@@ -96,12 +96,14 @@ perf_monitor_callback: Optional[Callable[[], dict[str, Any]]] = None
|
||||
|
||||
def configure(file_items: list[dict[str, Any]], extra_base_dirs: list[str] | None = None) -> None:
|
||||
"""
|
||||
Build the allowlist from aggregate file_items.
|
||||
Called by ai_client before each send so the list reflects the current project.
|
||||
|
||||
file_items : list of dicts from aggregate.build_file_items()
|
||||
extra_base_dirs : additional directory roots to allow traversal of
|
||||
"""
|
||||
|
||||
Build the allowlist from aggregate file_items.
|
||||
Called by ai_client before each send so the list reflects the current project.
|
||||
|
||||
file_items : list of dicts from aggregate.build_file_items()
|
||||
extra_base_dirs : additional directory roots to allow traversal of
|
||||
[C: tests/conftest.py:reset_ai_client, tests/test_arch_boundary_phase1.py:TestArchBoundaryPhase1.test_mcp_client_whitelist_enforcement, tests/test_mcp_client_beads.py:test_bd_mcp_tools]
|
||||
"""
|
||||
global _allowed_paths, _base_dirs, _primary_base_dir
|
||||
_allowed_paths = set()
|
||||
_base_dirs = set()
|
||||
@@ -123,15 +125,17 @@ def configure(file_items: list[dict[str, Any]], extra_base_dirs: list[str] | Non
|
||||
|
||||
def _is_allowed(path: Path) -> bool:
|
||||
"""
|
||||
Return True if `path` is within the allowlist.
|
||||
A path is allowed if:
|
||||
- it is explicitly in _allowed_paths, OR
|
||||
- it is contained within (or equal to) one of the _base_dirs
|
||||
All paths are resolved (follows symlinks) before comparison to prevent
|
||||
symlink-based path traversal.
|
||||
|
||||
CRITICAL: Blacklisted files (history) are NEVER allowed.
|
||||
"""
|
||||
|
||||
Return True if `path` is within the allowlist.
|
||||
A path is allowed if:
|
||||
- it is explicitly in _allowed_paths, OR
|
||||
- it is contained within (or equal to) one of the _base_dirs
|
||||
All paths are resolved (follows symlinks) before comparison to prevent
|
||||
symlink-based path traversal.
|
||||
|
||||
CRITICAL: Blacklisted files (history) are NEVER allowed.
|
||||
[C: tests/test_arch_boundary_phase1.py:TestArchBoundaryPhase1.test_mcp_client_whitelist_enforcement, tests/test_history_management.py:test_mcp_blacklist]
|
||||
"""
|
||||
from src.paths import get_config_path
|
||||
from src.ai_client import get_credentials_path
|
||||
|
||||
@@ -170,9 +174,10 @@ def _is_allowed(path: Path) -> bool:
|
||||
|
||||
def _resolve_and_check(raw_path: str) -> tuple[Path | None, str]:
|
||||
"""
|
||||
Resolve raw_path and verify it passes the allowlist check.
|
||||
Returns (resolved_path, error_string). error_string is empty on success.
|
||||
"""
|
||||
|
||||
Resolve raw_path and verify it passes the allowlist check.
|
||||
Returns (resolved_path, error_string). error_string is empty on success.
|
||||
"""
|
||||
try:
|
||||
p = Path(raw_path)
|
||||
if not p.is_absolute() and _primary_base_dir:
|
||||
@@ -232,9 +237,10 @@ def list_directory(path: str) -> str:
|
||||
|
||||
def search_files(path: str, pattern: str) -> str:
|
||||
"""
|
||||
Search for files matching a glob pattern within path.
|
||||
pattern examples: '*.py', '**/*.toml', 'src/**/*.rs'
|
||||
"""
|
||||
|
||||
Search for files matching a glob pattern within path.
|
||||
pattern examples: '*.py', '**/*.toml', 'src/**/*.rs'
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err or p is None:
|
||||
return err
|
||||
@@ -262,10 +268,11 @@ def search_files(path: str, pattern: str) -> str:
|
||||
|
||||
def get_file_summary(path: str) -> str:
|
||||
"""
|
||||
Return the heuristic summary for a file (same as the initial context block).
|
||||
For .py files: imports, classes, methods, functions, constants.
|
||||
For .toml: table keys. For .md: headings. Others: line count + preview.
|
||||
"""
|
||||
|
||||
Return the heuristic summary for a file (same as the initial context block).
|
||||
For .py files: imports, classes, methods, functions, constants.
|
||||
For .toml: table keys. For .md: headings. Others: line count + preview.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err or p is None:
|
||||
return err
|
||||
@@ -281,8 +288,9 @@ def get_file_summary(path: str) -> str:
|
||||
|
||||
def py_get_skeleton(path: str) -> str:
|
||||
"""
|
||||
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
|
||||
"""
|
||||
|
||||
Returns a skeleton of a Python file (preserving docstrings, stripping function bodies).
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -300,7 +308,10 @@ def py_get_skeleton(path: str) -> str:
|
||||
return f"ERROR generating skeleton for '{path}': {e}"
|
||||
|
||||
def ts_c_get_skeleton(path: str) -> str:
|
||||
"""Returns a skeleton of a C file."""
|
||||
"""
|
||||
Returns a skeleton of a C file.
|
||||
[C: tests/test_ts_c_tools.py:test_ts_c_get_skeleton]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -314,7 +325,10 @@ def ts_c_get_skeleton(path: str) -> str:
|
||||
return f"ERROR generating skeleton for '{path}': {e}"
|
||||
|
||||
def ts_cpp_get_skeleton(path: str) -> str:
|
||||
"""Returns a skeleton of a C++ file."""
|
||||
"""
|
||||
Returns a skeleton of a C++ file.
|
||||
[C: tests/test_ts_cpp_tools.py:test_exhaustive_cpp_samples, tests/test_ts_cpp_tools.py:test_exhaustive_gencpp_samples, tests/test_ts_cpp_tools.py:test_ts_cpp_get_skeleton]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -329,8 +343,9 @@ def ts_cpp_get_skeleton(path: str) -> str:
|
||||
|
||||
def py_get_code_outline(path: str) -> str:
|
||||
"""
|
||||
Returns a hierarchical outline of a code file (classes, functions, methods with line ranges).
|
||||
"""
|
||||
|
||||
Returns a hierarchical outline of a code file (classes, functions, methods with line ranges).
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -346,7 +361,10 @@ def py_get_code_outline(path: str) -> str:
|
||||
return f"ERROR generating outline for '{path}': {e}"
|
||||
|
||||
def ts_c_get_code_outline(path: str) -> str:
|
||||
"""Returns a hierarchical outline of a C file."""
|
||||
"""
|
||||
Returns a hierarchical outline of a C file.
|
||||
[C: tests/test_ts_c_tools.py:test_ts_c_get_code_outline]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -360,7 +378,10 @@ def ts_c_get_code_outline(path: str) -> str:
|
||||
return f"ERROR generating outline for '{path}': {e}"
|
||||
|
||||
def ts_cpp_get_code_outline(path: str) -> str:
|
||||
"""Returns a hierarchical outline of a C++ file."""
|
||||
"""
|
||||
Returns a hierarchical outline of a C++ file.
|
||||
[C: tests/test_ts_cpp_tools.py:test_exhaustive_cpp_samples, tests/test_ts_cpp_tools.py:test_exhaustive_gencpp_samples, tests/test_ts_cpp_tools.py:test_ts_cpp_get_code_outline]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -388,7 +409,10 @@ def ts_c_get_definition(path: str, name: str) -> str:
|
||||
return f"ERROR retrieving definition '{name}' from '{path}': {e}"
|
||||
|
||||
def ts_cpp_get_definition(path: str, name: str) -> str:
|
||||
"""Returns the source code for a specific definition in a C++ file."""
|
||||
"""
|
||||
Returns the source code for a specific definition in a C++ file.
|
||||
[C: tests/test_ts_cpp_tools.py:test_exhaustive_cpp_samples, tests/test_ts_cpp_tools.py:test_exhaustive_gencpp_samples, tests/test_ts_cpp_tools.py:test_ts_cpp_update_definition, tests/test_ts_cpp_tools.py:test_ts_cpp_update_definition_gencpp]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -448,7 +472,10 @@ def ts_c_update_definition(path: str, name: str, new_content: str) -> str:
|
||||
return f"ERROR updating definition '{name}' in '{path}': {e}"
|
||||
|
||||
def ts_cpp_update_definition(path: str, name: str, new_content: str) -> str:
|
||||
"""Surgically replace the definition of a class or function in a C++ file."""
|
||||
"""
|
||||
Surgically replace the definition of a class or function in a C++ file.
|
||||
[C: tests/test_ts_cpp_tools.py:test_ts_cpp_update_definition, tests/test_ts_cpp_tools.py:test_ts_cpp_update_definition_gencpp]
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err: return err
|
||||
assert p is not None
|
||||
@@ -504,8 +531,9 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
|
||||
|
||||
def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = False) -> str:
|
||||
"""
|
||||
Replace exact string match in a file. Preserves indentation and line endings.
|
||||
Drop-in replacement for native edit tool that destroys 1-space indentation.
|
||||
|
||||
Replace exact string match in a file. Preserves indentation and line endings.
|
||||
Drop-in replacement for native edit tool that destroys 1-space indentation.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
@@ -561,9 +589,10 @@ def _get_symbol_node(tree: ast.AST, name: str) -> Optional[ast.AST]:
|
||||
|
||||
def py_get_symbol_info(path: str, name: str) -> tuple[str, int] | str:
|
||||
"""
|
||||
Returns (source_code, line_number) for a specific class, function, or method definition.
|
||||
If not found, returns an error string.
|
||||
"""
|
||||
|
||||
Returns (source_code, line_number) for a specific class, function, or method definition.
|
||||
If not found, returns an error string.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -587,10 +616,11 @@ def py_get_symbol_info(path: str, name: str) -> tuple[str, int] | str:
|
||||
|
||||
def py_get_definition(path: str, name: str) -> str:
|
||||
"""
|
||||
Returns the source code for a specific class, function, or method definition.
|
||||
path: Path to the code file.
|
||||
name: Name of the definition to retrieve (e.g., 'MyClass', 'my_function', 'MyClass.my_method').
|
||||
"""
|
||||
|
||||
Returns the source code for a specific class, function, or method definition.
|
||||
path: Path to the code file.
|
||||
name: Name of the definition to retrieve (e.g., 'MyClass', 'my_function', 'MyClass.my_method').
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -761,10 +791,11 @@ def py_set_var_declaration(path: str, name: str, new_declaration: str) -> str:
|
||||
|
||||
def get_git_diff(path: str, base_rev: str = "HEAD", head_rev: str = "") -> str:
|
||||
"""
|
||||
Returns the git diff for a file or directory.
|
||||
base_rev: The base revision (default: HEAD)
|
||||
head_rev: The head revision (optional)
|
||||
"""
|
||||
|
||||
Returns the git diff for a file or directory.
|
||||
base_rev: The base revision (default: HEAD)
|
||||
head_rev: The head revision (optional)
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
@@ -1114,7 +1145,10 @@ def fetch_url(url: str) -> str:
|
||||
return f"ERROR fetching URL '{url}': {e}"
|
||||
|
||||
def get_ui_performance() -> str:
|
||||
"""Returns current UI performance metrics (FPS, Frame Time, CPU, Input Lag)."""
|
||||
"""
|
||||
Returns current UI performance metrics (FPS, Frame Time, CPU, Input Lag).
|
||||
[C: tests/test_mcp_perf_tool.py:test_mcp_perf_tool_retrieval]
|
||||
"""
|
||||
if perf_monitor_callback is None:
|
||||
return "INFO: UI Performance monitor is not available (headless/CLI mode). This tool is only functional when the Manual Slop GUI is running."
|
||||
try:
|
||||
@@ -1143,6 +1177,9 @@ class StdioMCPServer:
|
||||
return self._id_counter
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
[C: src/multi_agent_conductor.py:WorkerPool.spawn, src/performance_monitor.py:PerformanceMonitor.__init__, tests/test_ai_client_concurrency.py:test_ai_client_tier_isolation, tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread, tests/test_conductor_engine_v2.py:side_effect, tests/test_spawn_interception_v2.py:test_confirm_spawn_pushed_to_queue, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
self.status = 'starting'
|
||||
self.proc = await asyncio.create_subprocess_exec(
|
||||
self.config.command,
|
||||
@@ -1156,6 +1193,9 @@ class StdioMCPServer:
|
||||
self.status = 'running'
|
||||
|
||||
async def stop(self):
|
||||
"""
|
||||
[C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
if self.proc:
|
||||
try:
|
||||
if self.proc.stdin:
|
||||
@@ -1216,7 +1256,10 @@ class ExternalMCPManager:
|
||||
self.servers = {}
|
||||
|
||||
async def add_server(self, config: models.MCPServerConfig):
|
||||
"""Add and start a new MCP server from a configuration object."""
|
||||
"""
|
||||
Add and start a new MCP server from a configuration object.
|
||||
[C: tests/test_external_mcp.py:test_external_mcp_real_process, tests/test_external_mcp.py:test_get_tool_schemas_includes_external]
|
||||
"""
|
||||
if config.url:
|
||||
# RemoteMCPServer placeholder
|
||||
return
|
||||
@@ -1225,13 +1268,19 @@ class ExternalMCPManager:
|
||||
self.servers[config.name] = server
|
||||
|
||||
async def stop_all(self):
|
||||
"""Stop all managed MCP servers and clear the registry."""
|
||||
"""
|
||||
Stop all managed MCP servers and clear the registry.
|
||||
[C: tests/test_external_mcp.py:test_external_mcp_real_process, tests/test_external_mcp.py:test_get_tool_schemas_includes_external, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
"""
|
||||
for server in self.servers.values():
|
||||
await server.stop()
|
||||
self.servers = {}
|
||||
|
||||
def get_all_tools(self) -> dict:
|
||||
"""Retrieve a dictionary of all tools available across all managed servers."""
|
||||
"""
|
||||
Retrieve a dictionary of all tools available across all managed servers.
|
||||
[C: tests/test_external_mcp.py:test_external_mcp_real_process, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
"""
|
||||
all_tools = {}
|
||||
for sname, server in self.servers.items():
|
||||
for tname, tool in server.tools.items():
|
||||
@@ -1243,7 +1292,10 @@ class ExternalMCPManager:
|
||||
return {name: server.status for name, server in self.servers.items()}
|
||||
|
||||
async def async_dispatch(self, tool_name: str, tool_input: dict) -> str:
|
||||
"""Dispatch a tool call to the appropriate external MCP server asynchronously."""
|
||||
"""
|
||||
Dispatch a tool call to the appropriate external MCP server asynchronously.
|
||||
[C: src/rag_engine.py:RAGEngine._async_search_mcp, tests/test_external_mcp.py:test_external_mcp_real_process]
|
||||
"""
|
||||
for server in self.servers.values():
|
||||
if tool_name in server.tools:
|
||||
return await server.call_tool(tool_name, tool_input)
|
||||
@@ -1252,14 +1304,19 @@ class ExternalMCPManager:
|
||||
_external_mcp_manager = ExternalMCPManager()
|
||||
|
||||
def get_external_mcp_manager() -> ExternalMCPManager:
|
||||
"""Retrieve the global ExternalMCPManager instance."""
|
||||
"""
|
||||
Retrieve the global ExternalMCPManager instance.
|
||||
[C: tests/test_external_mcp.py:test_get_tool_schemas_includes_external, tests/test_external_mcp_e2e.py:test_external_mcp_e2e_refresh_and_call]
|
||||
"""
|
||||
global _external_mcp_manager
|
||||
return _external_mcp_manager
|
||||
|
||||
def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
"""
|
||||
Dispatch an MCP tool call by name. Returns the result as a string.
|
||||
"""
|
||||
|
||||
Dispatch an MCP tool call by name. Returns the result as a string.
|
||||
[C: tests/test_gemini_cli_edge_cases.py:test_gemini_cli_parameter_resilience, tests/test_mcp_client_beads.py:test_bd_mcp_tools, tests/test_mcp_ts_integration.py:test_ts_c_get_code_outline_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_signature_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_get_skeleton_dispatch, tests/test_mcp_ts_integration.py:test_ts_c_update_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_code_outline_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_definition_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_signature_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_get_skeleton_dispatch, tests/test_mcp_ts_integration.py:test_ts_cpp_update_definition_dispatch]
|
||||
"""
|
||||
# Handle aliases
|
||||
path = str(tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", ""))))
|
||||
if tool_name == "read_file":
|
||||
@@ -1377,6 +1434,9 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
|
||||
async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
# Check native tools
|
||||
"""
|
||||
[C: src/rag_engine.py:RAGEngine._async_search_mcp, tests/test_external_mcp.py:test_external_mcp_real_process]
|
||||
"""
|
||||
native_names = {t['name'] for t in MCP_TOOL_SPECS}
|
||||
if tool_name in native_names:
|
||||
return await asyncio.to_thread(dispatch, tool_name, tool_input)
|
||||
@@ -1390,6 +1450,9 @@ async def async_dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def get_tool_schemas() -> list[dict[str, Any]]:
|
||||
"""
|
||||
[C: tests/test_arch_boundary_phase2.py:TestArchBoundaryPhase2.test_mcp_client_dispatch_completeness, tests/test_external_mcp.py:test_get_tool_schemas_includes_external, tests/test_mcp_client_beads.py:test_bd_mcp_tools]
|
||||
"""
|
||||
res = list(MCP_TOOL_SPECS)
|
||||
manager = get_external_mcp_manager()
|
||||
for tname, tinfo in manager.get_all_tools().items():
|
||||
@@ -2126,4 +2189,3 @@ TOOL_NAMES: set[str] = {t['name'] for t in MCP_TOOL_SPECS}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+150
-1
File diff suppressed because one or more lines are too long
@@ -47,7 +47,8 @@ from src.dag_engine import TrackDAG, ExecutionEngine
|
||||
|
||||
class WorkerPool:
|
||||
"""
|
||||
Manages a pool of worker threads with a concurrency limit.
|
||||
|
||||
Manages a pool of worker threads with a concurrency limit.
|
||||
"""
|
||||
def __init__(self, max_workers: int = 4):
|
||||
self.max_workers = max_workers
|
||||
@@ -57,8 +58,10 @@ class WorkerPool:
|
||||
|
||||
def spawn(self, ticket_id: str, target: Callable, args: tuple) -> Optional[threading.Thread]:
|
||||
"""
|
||||
Spawns a new worker thread if the pool is not full.
|
||||
Returns the thread object or None if full.
|
||||
|
||||
Spawns a new worker thread if the pool is not full.
|
||||
Returns the thread object or None if full.
|
||||
[C: tests/test_parallel_execution.py:test_worker_pool_completion_cleanup, tests/test_parallel_execution.py:test_worker_pool_limit, tests/test_parallel_execution.py:test_worker_pool_tracking]
|
||||
"""
|
||||
with self._lock:
|
||||
if len(self._active) >= self.max_workers:
|
||||
@@ -79,6 +82,9 @@ class WorkerPool:
|
||||
return t
|
||||
|
||||
def join_all(self, timeout: float = None) -> None:
|
||||
"""
|
||||
[C: tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_parallel_execution.py:test_worker_pool_limit, tests/test_parallel_execution.py:test_worker_pool_tracking]
|
||||
"""
|
||||
with self._lock:
|
||||
threads = list(self._active.values())
|
||||
for t in threads:
|
||||
@@ -87,16 +93,23 @@ class WorkerPool:
|
||||
self._active.clear()
|
||||
|
||||
def get_active_count(self) -> int:
|
||||
"""
|
||||
[C: tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_parallel_execution.py:test_worker_pool_completion_cleanup, tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
with self._lock:
|
||||
return len(self._active)
|
||||
|
||||
def is_full(self) -> bool:
|
||||
"""
|
||||
[C: tests/test_parallel_execution.py:test_worker_pool_limit]
|
||||
"""
|
||||
return self.get_active_count() >= self.max_workers
|
||||
|
||||
class ConductorEngine:
|
||||
"""
|
||||
Orchestrates the execution of tickets within a track.
|
||||
"""
|
||||
|
||||
Orchestrates the execution of tickets within a track.
|
||||
"""
|
||||
|
||||
def __init__(self, track: Track, event_queue: Optional[events.AsyncEventQueue] = None, auto_queue: bool = False) -> None:
|
||||
self.track = track
|
||||
@@ -134,25 +147,40 @@ class ConductorEngine:
|
||||
self.tier_usage[tier]["output"] += output_tokens
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pauses the pipeline execution."""
|
||||
"""
|
||||
Pauses the pipeline execution.
|
||||
[C: tests/test_pipeline_pause.py:test_pause_method, tests/test_pipeline_pause.py:test_resume_method]
|
||||
"""
|
||||
self._pause_event.set()
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resumes the pipeline execution."""
|
||||
"""
|
||||
Resumes the pipeline execution.
|
||||
[C: tests/test_pipeline_pause.py:test_resume_method]
|
||||
"""
|
||||
self._pause_event.clear()
|
||||
|
||||
def approve_task(self, task_id: str) -> None:
|
||||
"""Manually transition todo to in_progress and mark engine dirty."""
|
||||
"""
|
||||
Manually transition todo to in_progress and mark engine dirty.
|
||||
[C: tests/test_execution_engine.py:test_execution_engine_approve_task, tests/test_execution_engine.py:test_execution_engine_step_mode]
|
||||
"""
|
||||
self.engine.approve_task(task_id)
|
||||
self._dirty = True
|
||||
|
||||
def update_task_status(self, task_id: str, status: str) -> None:
|
||||
"""Force-update ticket status and mark engine dirty."""
|
||||
"""
|
||||
Force-update ticket status and mark engine dirty.
|
||||
[C: tests/test_arch_boundary_phase3.py:TestArchBoundaryPhase3.test_manual_unblock_restores_todo, tests/test_execution_engine.py:test_execution_engine_auto_queue, tests/test_execution_engine.py:test_execution_engine_basic_flow, tests/test_execution_engine.py:test_execution_engine_status_persistence, tests/test_execution_engine.py:test_execution_engine_update_nonexistent_task]
|
||||
"""
|
||||
self.engine.update_task_status(task_id, status)
|
||||
self._dirty = True
|
||||
|
||||
def kill_worker(self, ticket_id: str) -> None:
|
||||
"""Sets the abort event for a worker and attempts to join its thread."""
|
||||
"""
|
||||
Sets the abort event for a worker and attempts to join its thread.
|
||||
[C: tests/test_conductor_engine_abort.py:test_kill_worker_sets_abort_and_joins_thread]
|
||||
"""
|
||||
if ticket_id in self._abort_events:
|
||||
print(f"[MMA] Setting abort event for {ticket_id}")
|
||||
self._abort_events[ticket_id].set()
|
||||
@@ -184,9 +212,11 @@ class ConductorEngine:
|
||||
|
||||
def parse_json_tickets(self, json_str: str) -> None:
|
||||
"""
|
||||
Parses a JSON string of ticket definitions (Godot ECS Flat List format)
|
||||
and populates the Track's ticket list.
|
||||
"""
|
||||
|
||||
Parses a JSON string of ticket definitions (Godot ECS Flat List format)
|
||||
and populates the Track's ticket list.
|
||||
[C: tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_orchestration_logic.py:test_conductor_engine_parse_json_tickets]
|
||||
"""
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
if not isinstance(data, list):
|
||||
@@ -213,11 +243,13 @@ class ConductorEngine:
|
||||
|
||||
def run(self, md_content: str = "", max_ticks: Optional[int] = None) -> None:
|
||||
"""
|
||||
Main execution loop using the DAG engine.
|
||||
Args:
|
||||
md_content: The full markdown context (history + files) for AI workers.
|
||||
max_ticks: Optional limit on number of iterations (for testing).
|
||||
"""
|
||||
|
||||
Main execution loop using the DAG engine.
|
||||
Args:
|
||||
md_content: The full markdown context (history + files) for AI workers.
|
||||
max_ticks: Optional limit on number of iterations (for testing).
|
||||
[C: simulation/sim_base.py:run_sim, src/project_manager.py:get_git_commit, src/project_manager.py:get_git_log, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run]
|
||||
"""
|
||||
tick_count = 0
|
||||
while True:
|
||||
if self._pause_event.is_set():
|
||||
@@ -335,8 +367,9 @@ def _queue_put(event_queue: events.AsyncEventQueue, event_name: str, payload) ->
|
||||
|
||||
def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> bool:
|
||||
"""
|
||||
Pushes an approval request to the GUI and waits for response.
|
||||
"""
|
||||
|
||||
Pushes an approval request to the GUI and waits for response.
|
||||
"""
|
||||
dialog_container = [None]
|
||||
task = {
|
||||
"action": "mma_step_approval",
|
||||
@@ -357,9 +390,11 @@ def confirm_execution(payload: str, event_queue: events.AsyncEventQueue, ticket_
|
||||
|
||||
def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.AsyncEventQueue, ticket_id: str) -> Tuple[bool, str, str]:
|
||||
"""
|
||||
Pushes a spawn approval request to the GUI and waits for response.
|
||||
Returns (approved, modified_prompt, modified_context)
|
||||
"""
|
||||
|
||||
Pushes a spawn approval request to the GUI and waits for response.
|
||||
Returns (approved, modified_prompt, modified_context)
|
||||
[C: tests/test_spawn_interception_v2.py:run_confirm]
|
||||
"""
|
||||
dialog_container = [None]
|
||||
task = {
|
||||
"action": "mma_spawn_approval",
|
||||
@@ -396,16 +431,18 @@ def confirm_spawn(role: str, prompt: str, context_md: str, event_queue: events.A
|
||||
|
||||
def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: List[str] | None = None, event_queue: events.AsyncEventQueue | None = None, engine: Optional['ConductorEngine'] = None, md_content: str = "") -> None:
|
||||
"""
|
||||
Simulates the lifecycle of a single agent working on a ticket.
|
||||
Calls the AI client and updates the ticket status based on the response.
|
||||
Args:
|
||||
ticket: The ticket to process.
|
||||
context: The worker context.
|
||||
context_files: List of files to include in the context.
|
||||
event_queue: Queue for pushing state updates and receiving approvals.
|
||||
engine: The conductor engine.
|
||||
md_content: The markdown context (history + files) for AI workers.
|
||||
"""
|
||||
|
||||
Simulates the lifecycle of a single agent working on a ticket.
|
||||
Calls the AI client and updates the ticket status based on the response.
|
||||
Args:
|
||||
ticket: The ticket to process.
|
||||
context: The worker context.
|
||||
context_files: List of files to include in the context.
|
||||
event_queue: Queue for pushing state updates and receiving approvals.
|
||||
engine: The conductor engine.
|
||||
md_content: The markdown context (history + files) for AI workers.
|
||||
[C: tests/test_conductor_engine_v2.py:test_run_worker_lifecycle_calls_ai_client_send, tests/test_conductor_engine_v2.py:test_run_worker_lifecycle_context_injection, tests/test_conductor_engine_v2.py:test_run_worker_lifecycle_handles_blocked_response, tests/test_conductor_engine_v2.py:test_run_worker_lifecycle_pushes_response_via_queue, tests/test_conductor_engine_v2.py:test_run_worker_lifecycle_step_mode_confirmation, tests/test_conductor_engine_v2.py:test_run_worker_lifecycle_step_mode_rejection, tests/test_conductor_engine_v2.py:test_run_worker_lifecycle_token_usage_from_comms_log, tests/test_context_pruner.py:test_token_reduction_logging, tests/test_orchestration_logic.py:test_run_worker_lifecycle_blocked, tests/test_phase6_engine.py:test_worker_streaming_intermediate, tests/test_run_worker_lifecycle_abort.py:TestRunWorkerLifecycleAbort.test_run_worker_lifecycle_returns_early_on_abort, tests/test_spawn_interception_v2.py:test_run_worker_lifecycle_approved, tests/test_spawn_interception_v2.py:test_run_worker_lifecycle_rejected, tests/test_tiered_aggregation.py:test_run_worker_lifecycle_uses_strategy]
|
||||
"""
|
||||
# Enforce Context Amnesia: each ticket starts with a clean slate.
|
||||
ai_client.reset_session()
|
||||
ai_client.set_provider(ai_client.get_provider(), context.model_name)
|
||||
@@ -609,4 +646,3 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files:
|
||||
if event_queue:
|
||||
_queue_put(event_queue, "ticket_completed", {"ticket_id": ticket.id, "timestamp": time.time()})
|
||||
return response
|
||||
|
||||
|
||||
+52
-14
@@ -9,20 +9,29 @@ import re
|
||||
from src import paths
|
||||
|
||||
def read_plan(track_id: str, base_dir: str = ".") -> str:
|
||||
"""Reads the implementation plan (plan.md) for a track."""
|
||||
"""
|
||||
Reads the implementation plan (plan.md) for a track.
|
||||
[C: tests/test_native_orchestrator.py:test_read_plan_nonexistent, tests/test_native_orchestrator.py:test_write_and_read_plan]
|
||||
"""
|
||||
plan_path = paths.get_track_state_dir(track_id, base_dir) / "plan.md"
|
||||
if not plan_path.exists():
|
||||
return ""
|
||||
return plan_path.read_text(encoding="utf-8")
|
||||
|
||||
def write_plan(track_id: str, content: str, base_dir: str = ".") -> None:
|
||||
"""Writes the implementation plan (plan.md) for a track."""
|
||||
"""
|
||||
Writes the implementation plan (plan.md) for a track.
|
||||
[C: tests/test_native_orchestrator.py:test_write_and_read_plan]
|
||||
"""
|
||||
plan_path = paths.get_track_state_dir(track_id, base_dir) / "plan.md"
|
||||
plan_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
plan_path.write_text(content, encoding="utf-8")
|
||||
|
||||
def parse_plan_tasks(content: str) -> list[dict[str, str]]:
|
||||
"""Parses the tasks from a plan.md file."""
|
||||
"""
|
||||
Parses the tasks from a plan.md file.
|
||||
[C: tests/test_native_orchestrator.py:test_parse_plan_tasks]
|
||||
"""
|
||||
tasks = []
|
||||
for line in content.split("\n"):
|
||||
stripped = line.strip()
|
||||
@@ -33,24 +42,36 @@ def parse_plan_tasks(content: str) -> list[dict[str, str]]:
|
||||
return tasks
|
||||
|
||||
def read_metadata(track_id: str, base_dir: str = ".") -> dict:
|
||||
"""Reads the metadata (metadata.json) for a track."""
|
||||
"""
|
||||
Reads the metadata (metadata.json) for a track.
|
||||
[C: tests/test_native_orchestrator.py:test_read_metadata_nonexistent, tests/test_native_orchestrator.py:test_write_and_read_metadata]
|
||||
"""
|
||||
meta_path = paths.get_track_state_dir(track_id, base_dir) / "metadata.json"
|
||||
if not meta_path.exists():
|
||||
return {}
|
||||
return json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
|
||||
def write_metadata(track_id: str, data: dict, base_dir: str = ".") -> None:
|
||||
"""Writes the metadata (metadata.json) for a track."""
|
||||
"""
|
||||
Writes the metadata (metadata.json) for a track.
|
||||
[C: tests/test_native_orchestrator.py:test_write_and_read_metadata]
|
||||
"""
|
||||
meta_path = paths.get_track_state_dir(track_id, base_dir) / "metadata.json"
|
||||
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
meta_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
|
||||
def get_track_dir(track_id: str, base_dir: str = ".") -> Path:
|
||||
"""Returns the state directory for a specific track."""
|
||||
"""
|
||||
Returns the state directory for a specific track.
|
||||
[C: tests/test_native_orchestrator.py:test_get_track_dir]
|
||||
"""
|
||||
return paths.get_track_state_dir(track_id, base_dir)
|
||||
|
||||
def get_archive_dir(base_dir: str = ".") -> Path:
|
||||
"""Returns the central archive directory for completed tracks."""
|
||||
"""
|
||||
Returns the central archive directory for completed tracks.
|
||||
[C: src/orchestrator_pm.py:get_track_history_summary, tests/test_native_orchestrator.py:test_get_archive_dir, tests/test_paths.py:test_conductor_dir_project_relative]
|
||||
"""
|
||||
return paths.get_archive_dir(base_dir)
|
||||
|
||||
class NativeOrchestrator:
|
||||
@@ -59,28 +80,46 @@ class NativeOrchestrator:
|
||||
self._conductor = None
|
||||
|
||||
def load_track(self, track_id: str) -> dict:
|
||||
"""Load track from metadata.json"""
|
||||
"""
|
||||
Load track from metadata.json
|
||||
[C: tests/test_native_orchestrator.py:test_native_orchestrator_class]
|
||||
"""
|
||||
return read_metadata(track_id, str(self.base_dir))
|
||||
|
||||
def save_track(self, track_id: str, data: dict) -> None:
|
||||
"""Persist track metadata"""
|
||||
"""
|
||||
Persist track metadata
|
||||
[C: tests/test_native_orchestrator.py:test_native_orchestrator_class]
|
||||
"""
|
||||
write_metadata(track_id, data, str(self.base_dir))
|
||||
|
||||
def load_plan(self, track_id: str) -> str:
|
||||
"""Load plan.md content"""
|
||||
"""
|
||||
Load plan.md content
|
||||
[C: tests/test_native_orchestrator.py:test_native_orchestrator_class]
|
||||
"""
|
||||
return read_plan(track_id, str(self.base_dir))
|
||||
|
||||
def save_plan(self, track_id: str, content: str) -> None:
|
||||
"""Persist plan.md content"""
|
||||
"""
|
||||
Persist plan.md content
|
||||
[C: tests/test_native_orchestrator.py:test_native_orchestrator_class]
|
||||
"""
|
||||
write_plan(track_id, content, str(self.base_dir))
|
||||
|
||||
def get_track_tasks(self, track_id: str) -> list[dict]:
|
||||
"""Get parsed task list from plan.md"""
|
||||
"""
|
||||
Get parsed task list from plan.md
|
||||
[C: tests/test_native_orchestrator.py:test_native_orchestrator_class]
|
||||
"""
|
||||
content = self.load_plan(track_id)
|
||||
return parse_plan_tasks(content)
|
||||
|
||||
def generate_tickets(self, brief: str, module_skeletons: str = "") -> list[dict]:
|
||||
"""Tier 2: Generate tickets from brief"""
|
||||
"""
|
||||
Tier 2: Generate tickets from brief
|
||||
[C: tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_retry_failure, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_retry_success, tests/test_conductor_tech_lead.py:TestConductorTechLead.test_generate_tickets_success, tests/test_orchestration_logic.py:test_generate_tickets]
|
||||
"""
|
||||
from src import conductor_tech_lead
|
||||
return conductor_tech_lead.generate_tickets(brief, module_skeletons)
|
||||
|
||||
@@ -98,4 +137,3 @@ class NativeOrchestrator:
|
||||
"""Tier 4: Generate patch for error"""
|
||||
from src import ai_client
|
||||
return ai_client.run_tier4_patch_generation(error, file_context)
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ from src import paths
|
||||
|
||||
def get_track_history_summary() -> str:
|
||||
"""
|
||||
Scans conductor/archive/ and conductor/tracks/ to build a summary of past work.
|
||||
"""
|
||||
|
||||
Scans conductor/archive/ and conductor/tracks/ to build a summary of past work.
|
||||
[C: tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_get_track_history_summary, tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_get_track_history_summary_missing_files]
|
||||
"""
|
||||
summary_parts = []
|
||||
archive_path = paths.get_archive_dir()
|
||||
tracks_path = paths.get_tracks_dir()
|
||||
@@ -56,9 +58,11 @@ def get_track_history_summary() -> str:
|
||||
|
||||
def generate_tracks(user_request: str, project_config: dict[str, Any], file_items: list[dict[str, Any]], history_summary: Optional[str] = None) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Tier 1 (Strategic PM) call.
|
||||
Analyzes the project state and user request to generate a list of Tracks.
|
||||
"""
|
||||
|
||||
Tier 1 (Strategic PM) call.
|
||||
Analyzes the project state and user request to generate a list of Tracks.
|
||||
[C: tests/test_orchestration_logic.py:test_generate_tracks, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_malformed_json, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_markdown_wrapped, tests/test_orchestrator_pm.py:TestOrchestratorPM.test_generate_tracks_success, tests/test_orchestrator_pm_history.py:TestOrchestratorPMHistory.test_generate_tracks_with_history]
|
||||
"""
|
||||
# 1. Build Repository Map (Summary View)
|
||||
repo_map = summarize.build_summary_markdown(file_items)
|
||||
# 2. Construct Prompt
|
||||
@@ -125,4 +129,3 @@ if __name__ == "__main__":
|
||||
history = get_track_history_summary()
|
||||
tracks = generate_tracks("Implement a basic unit test for the ai_client.py module.", flat, file_items, history_summary=history)
|
||||
print(json.dumps(tracks, indent=2))
|
||||
|
||||
|
||||
+3
-1
@@ -54,6 +54,9 @@ class CodeOutliner:
|
||||
return None
|
||||
|
||||
def walk(node: ast.AST, indent: int = 0) -> None:
|
||||
"""
|
||||
[C: src/summarize.py:_summarise_python]
|
||||
"""
|
||||
if isinstance(node, ast.ClassDef):
|
||||
start_line = node.lineno
|
||||
end_line = getattr(node, "end_lineno", start_line)
|
||||
@@ -87,4 +90,3 @@ def get_outline(path: Path, code: str) -> str:
|
||||
return outliner.outline(code)
|
||||
else:
|
||||
return f"Outlining not supported for {suffix} files yet."
|
||||
|
||||
|
||||
+34
-1
@@ -16,6 +16,9 @@ class PatchModalManager:
|
||||
self._on_reject_callback: Optional[Callable[[], None]] = None
|
||||
|
||||
def request_patch_approval(self, patch_text: str, file_paths: List[str], generated_by: str = "Tier 4 QA") -> bool:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_close_modal, tests/test_patch_modal.py:test_reject_patch, tests/test_patch_modal.py:test_request_patch_approval, tests/test_patch_modal.py:test_reset]
|
||||
"""
|
||||
from time import time
|
||||
self._pending_patch = PendingPatch(
|
||||
patch_text=patch_text,
|
||||
@@ -27,32 +30,56 @@ class PatchModalManager:
|
||||
return True
|
||||
|
||||
def get_pending_patch(self) -> Optional[PendingPatch]:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_patch_modal_manager_init, tests/test_patch_modal.py:test_reject_patch, tests/test_patch_modal.py:test_request_patch_approval, tests/test_patch_modal.py:test_reset]
|
||||
"""
|
||||
return self._pending_patch
|
||||
|
||||
def is_modal_shown(self) -> bool:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_close_modal, tests/test_patch_modal.py:test_patch_modal_manager_init, tests/test_patch_modal.py:test_reject_patch, tests/test_patch_modal.py:test_request_patch_approval, tests/test_patch_modal.py:test_reset]
|
||||
"""
|
||||
return self._show_modal
|
||||
|
||||
def set_apply_callback(self, callback: Callable[[str], bool]) -> None:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_apply_callback, tests/test_patch_modal.py:test_reset]
|
||||
"""
|
||||
self._on_apply_callback = callback
|
||||
|
||||
def set_reject_callback(self, callback: Callable[[], None]) -> None:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_reject_callback, tests/test_patch_modal.py:test_reset]
|
||||
"""
|
||||
self._on_reject_callback = callback
|
||||
|
||||
def apply_patch(self, patch_text: str) -> bool:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_apply_callback]
|
||||
"""
|
||||
if self._on_apply_callback:
|
||||
return self._on_apply_callback(patch_text)
|
||||
return False
|
||||
|
||||
def reject_patch(self) -> None:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_reject_callback, tests/test_patch_modal.py:test_reject_patch]
|
||||
"""
|
||||
self._pending_patch = None
|
||||
self._show_modal = False
|
||||
if self._on_reject_callback:
|
||||
self._on_reject_callback()
|
||||
|
||||
def close_modal(self) -> None:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_close_modal]
|
||||
"""
|
||||
self._show_modal = False
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_reset]
|
||||
"""
|
||||
self._pending_patch = None
|
||||
self._show_modal = False
|
||||
self._on_apply_callback = None
|
||||
@@ -61,13 +88,19 @@ class PatchModalManager:
|
||||
_patch_modal_manager: Optional[PatchModalManager] = None
|
||||
|
||||
def get_patch_modal_manager() -> PatchModalManager:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_get_patch_modal_manager_singleton]
|
||||
"""
|
||||
global _patch_modal_manager
|
||||
if _patch_modal_manager is None:
|
||||
_patch_modal_manager = PatchModalManager()
|
||||
return _patch_modal_manager
|
||||
|
||||
def reset_patch_modal_manager() -> None:
|
||||
"""
|
||||
[C: tests/test_patch_modal.py:test_get_patch_modal_manager_singleton]
|
||||
"""
|
||||
global _patch_modal_manager
|
||||
if _patch_modal_manager:
|
||||
_patch_modal_manager.reset()
|
||||
_patch_modal_manager = None
|
||||
_patch_modal_manager = None
|
||||
+47
-2
@@ -48,35 +48,62 @@ from typing import Optional, Any
|
||||
_RESOLVED: dict[str, Path] = {}
|
||||
|
||||
def get_config_path() -> Path:
|
||||
"""
|
||||
[C: tests/test_paths.py:test_default_paths]
|
||||
"""
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
return Path(os.environ.get("SLOP_CONFIG", root_dir / "config.toml"))
|
||||
|
||||
def get_global_presets_path() -> Path:
|
||||
"""
|
||||
[C: src/presets.py:PresetManager.__init__, src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope]
|
||||
"""
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
return Path(os.environ.get("SLOP_GLOBAL_PRESETS", root_dir / "presets.toml"))
|
||||
|
||||
def get_project_presets_path(project_root: Path) -> Path:
|
||||
"""
|
||||
[C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope, src/presets.py:PresetManager.project_path]
|
||||
"""
|
||||
return project_root / "project_presets.toml"
|
||||
|
||||
def get_global_tool_presets_path() -> Path:
|
||||
"""
|
||||
[C: src/tool_presets.py:ToolPresetManager._get_path, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets]
|
||||
"""
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
return Path(os.environ.get("SLOP_GLOBAL_TOOL_PRESETS", root_dir / "tool_presets.toml"))
|
||||
|
||||
def get_project_tool_presets_path(project_root: Path) -> Path:
|
||||
"""
|
||||
[C: src/tool_presets.py:ToolPresetManager._get_path, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets]
|
||||
"""
|
||||
return project_root / "project_tool_presets.toml"
|
||||
|
||||
def get_global_personas_path() -> Path:
|
||||
"""
|
||||
[C: src/personas.py:PersonaManager._get_path, src/personas.py:PersonaManager.get_persona_scope, src/personas.py:PersonaManager.load_all]
|
||||
"""
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
return Path(os.environ.get("SLOP_GLOBAL_PERSONAS", root_dir / "personas.toml"))
|
||||
|
||||
def get_project_personas_path(project_root: Path) -> Path:
|
||||
"""
|
||||
[C: src/personas.py:PersonaManager._get_path, src/personas.py:PersonaManager.get_persona_scope, src/personas.py:PersonaManager.load_all]
|
||||
"""
|
||||
return project_root / "project_personas.toml"
|
||||
|
||||
def get_global_workspace_profiles_path() -> Path:
|
||||
"""
|
||||
[C: src/workspace_manager.py:WorkspaceManager._get_path, src/workspace_manager.py:WorkspaceManager.load_all_profiles]
|
||||
"""
|
||||
root_dir = Path(__file__).resolve().parent.parent
|
||||
return Path(os.environ.get("SLOP_GLOBAL_WORKSPACE_PROFILES", root_dir / "workspace_profiles.toml"))
|
||||
|
||||
def get_project_workspace_profiles_path(project_root: Path) -> Path:
|
||||
"""
|
||||
[C: src/workspace_manager.py:WorkspaceManager._get_path, src/workspace_manager.py:WorkspaceManager.load_all_profiles]
|
||||
"""
|
||||
return project_root / ".ai" / "workspace_profiles.toml"
|
||||
|
||||
def _resolve_path(env_var: str, config_key: str, default: str) -> Path:
|
||||
@@ -115,6 +142,9 @@ def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]:
|
||||
return None
|
||||
|
||||
def get_conductor_dir(project_path: Optional[str] = None) -> Path:
|
||||
"""
|
||||
[C: tests/test_paths.py:test_conductor_dir_project_relative, tests/test_project_paths.py:test_get_conductor_dir_default, tests/test_project_paths.py:test_get_conductor_dir_project_specific_with_toml]
|
||||
"""
|
||||
if not project_path:
|
||||
# Fallback for legacy/tests, but we should avoid this
|
||||
return Path('conductor').resolve()
|
||||
@@ -126,19 +156,31 @@ def get_conductor_dir(project_path: Optional[str] = None) -> Path:
|
||||
return (project_root / "conductor").resolve()
|
||||
|
||||
def get_logs_dir() -> Path:
|
||||
"""
|
||||
[C: src/session_logger.py:close_session, src/session_logger.py:open_session, tests/test_paths.py:test_config_overrides, tests/test_paths.py:test_default_paths, tests/test_paths.py:test_env_var_overrides, tests/test_paths.py:test_precedence]
|
||||
"""
|
||||
if "logs_dir" not in _RESOLVED:
|
||||
_RESOLVED["logs_dir"] = _resolve_path("SLOP_LOGS_DIR", "logs_dir", "logs/sessions")
|
||||
return _RESOLVED["logs_dir"]
|
||||
|
||||
def get_scripts_dir() -> Path:
|
||||
"""
|
||||
[C: src/session_logger.py:log_tool_call, src/session_logger.py:open_session, tests/test_paths.py:test_config_overrides, tests/test_paths.py:test_default_paths]
|
||||
"""
|
||||
if "scripts_dir" not in _RESOLVED:
|
||||
_RESOLVED["scripts_dir"] = _resolve_path("SLOP_SCRIPTS_DIR", "scripts_dir", "scripts/generated")
|
||||
return _RESOLVED["scripts_dir"]
|
||||
|
||||
def get_tracks_dir(project_path: Optional[str] = None) -> Path:
|
||||
"""
|
||||
[C: src/project_manager.py:get_all_tracks, tests/test_paths.py:test_conductor_dir_project_relative]
|
||||
"""
|
||||
return get_conductor_dir(project_path) / "tracks"
|
||||
|
||||
def get_track_state_dir(track_id: str, project_path: Optional[str] = None) -> Path:
|
||||
"""
|
||||
[C: src/project_manager.py:load_track_state, src/project_manager.py:save_track_state, tests/test_paths.py:test_conductor_dir_project_relative]
|
||||
"""
|
||||
return get_tracks_dir(project_path) / track_id
|
||||
|
||||
def get_archive_dir(project_path: Optional[str] = None) -> Path:
|
||||
@@ -167,5 +209,8 @@ def get_full_path_info() -> dict[str, dict[str, Any]]:
|
||||
}
|
||||
|
||||
def reset_resolved() -> None:
|
||||
"""For testing only - clear cached resolutions."""
|
||||
_RESOLVED.clear()
|
||||
"""
|
||||
For testing only - clear cached resolutions.
|
||||
[C: tests/conftest.py:reset_paths, tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_gui_phase3.py:test_conductor_setup_scan, tests/test_paths.py:reset_paths, tests/test_project_paths.py:test_get_all_tracks_project_specific, tests/test_project_paths.py:test_get_conductor_dir_default, tests/test_project_paths.py:test_get_conductor_dir_project_specific_with_toml]
|
||||
"""
|
||||
_RESOLVED.clear()
|
||||
@@ -75,6 +75,9 @@ class PerformanceScope:
|
||||
|
||||
|
||||
def get_monitor() -> PerformanceMonitor:
|
||||
"""
|
||||
[C: tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_perf_dag.py:test_dag_performance]
|
||||
"""
|
||||
global _instance
|
||||
if _instance is None:
|
||||
_instance = PerformanceMonitor()
|
||||
@@ -82,9 +85,10 @@ def get_monitor() -> PerformanceMonitor:
|
||||
|
||||
class PerformanceMonitor:
|
||||
"""
|
||||
Tracks application performance metrics like FPS, frame time, and CPU usage.
|
||||
Supports thread-safe tracking for individual components with efficient moving averages.
|
||||
"""
|
||||
|
||||
Tracks application performance metrics like FPS, frame time, and CPU usage.
|
||||
Supports thread-safe tracking for individual components with efficient moving averages.
|
||||
"""
|
||||
def __init__(self, history_size: int = 300) -> None:
|
||||
self.enabled: bool = False
|
||||
self.history_size = history_size
|
||||
@@ -154,6 +158,9 @@ class PerformanceMonitor:
|
||||
return self._history_sums[key] / len(h)
|
||||
|
||||
def start_frame(self) -> None:
|
||||
"""
|
||||
[C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing]
|
||||
"""
|
||||
now = time.perf_counter()
|
||||
with self._lock:
|
||||
if self._last_frame_start_time > 0:
|
||||
@@ -165,6 +172,9 @@ class PerformanceMonitor:
|
||||
self._frame_count += 1
|
||||
|
||||
def end_frame(self) -> None:
|
||||
"""
|
||||
[C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing]
|
||||
"""
|
||||
if self._start_time is None:
|
||||
return
|
||||
now = time.perf_counter()
|
||||
@@ -193,12 +203,18 @@ class PerformanceMonitor:
|
||||
self._fps_timer = 0.0
|
||||
|
||||
def start_component(self, name: str) -> None:
|
||||
"""
|
||||
[C: tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics]
|
||||
"""
|
||||
if not self.enabled: return
|
||||
now = time.perf_counter()
|
||||
with self._lock:
|
||||
self._component_starts[name] = now
|
||||
|
||||
def end_component(self, name: str) -> None:
|
||||
"""
|
||||
[C: tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics]
|
||||
"""
|
||||
if not self.enabled: return
|
||||
now = time.perf_counter()
|
||||
with self._lock:
|
||||
@@ -215,7 +231,10 @@ class PerformanceMonitor:
|
||||
self._add_to_history(f'comp_{name}', elapsed)
|
||||
|
||||
def get_metrics(self) -> dict[str, float]:
|
||||
"""Returns current metrics and their moving averages. Thread-safe."""
|
||||
"""
|
||||
Returns current metrics and their moving averages. Thread-safe.
|
||||
[C: tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_perf_dag.py:test_dag_performance, tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager]
|
||||
"""
|
||||
with self._lock:
|
||||
fps = self._fps
|
||||
last_ft = self._last_frame_time
|
||||
@@ -246,7 +265,10 @@ class PerformanceMonitor:
|
||||
return metrics
|
||||
|
||||
def get_history(self, key: str) -> List[float]:
|
||||
"""Returns a snapshot of the full history buffer for a specific metric key."""
|
||||
"""
|
||||
Returns a snapshot of the full history buffer for a specific metric key.
|
||||
[C: tests/test_history.py:test_initial_state, tests/test_history.py:test_push_state]
|
||||
"""
|
||||
with self._lock:
|
||||
if key in self._history:
|
||||
return list(self._history[key])
|
||||
@@ -255,11 +277,16 @@ class PerformanceMonitor:
|
||||
return []
|
||||
|
||||
def scope(self, name: str) -> PerformanceScope:
|
||||
"""Returns a context manager for timing a component."""
|
||||
"""
|
||||
Returns a context manager for timing a component.
|
||||
[C: tests/test_perf_aggregate.py:test_build_tier3_context_scaling, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager]
|
||||
"""
|
||||
return PerformanceScope(self, name)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
[C: tests/test_performance_monitor.py:test_perf_monitor_basic_timing, tests/test_performance_monitor.py:test_perf_monitor_component_timing, tests/test_performance_monitor.py:test_perf_monitor_extended_metrics, tests/test_performance_monitor.py:test_perf_monitor_scope_context_manager, tests/test_websocket_server.py:test_websocket_subscription_and_broadcast]
|
||||
"""
|
||||
self._stop_event.set()
|
||||
if self._cpu_thread.is_alive():
|
||||
self._cpu_thread.join(timeout=2.0)
|
||||
|
||||
|
||||
+19
-2
@@ -12,6 +12,9 @@ class PersonaManager:
|
||||
self.project_root = project_root
|
||||
|
||||
def _get_path(self, scope: str) -> Path:
|
||||
"""
|
||||
[C: src/tool_presets.py:ToolPresetManager.delete_bias_profile, src/tool_presets.py:ToolPresetManager.delete_preset, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile]
|
||||
"""
|
||||
if scope == "global":
|
||||
return paths.get_global_personas_path()
|
||||
elif scope == "project":
|
||||
@@ -22,7 +25,10 @@ class PersonaManager:
|
||||
raise ValueError("Invalid scope, must be 'global' or 'project'")
|
||||
|
||||
def load_all(self) -> Dict[str, Persona]:
|
||||
"""Merges global and project personas into a single dictionary."""
|
||||
"""
|
||||
Merges global and project personas into a single dictionary.
|
||||
[C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||
"""
|
||||
personas = {}
|
||||
|
||||
global_path = paths.get_global_personas_path()
|
||||
@@ -39,6 +45,9 @@ class PersonaManager:
|
||||
return personas
|
||||
|
||||
def save_persona(self, persona: Persona, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: tests/test_persona_manager.py:test_save_persona]
|
||||
"""
|
||||
path = self._get_path(scope)
|
||||
data = self._load_file(path)
|
||||
if "personas" not in data:
|
||||
@@ -63,6 +72,9 @@ class PersonaManager:
|
||||
return "project"
|
||||
|
||||
def delete_persona(self, name: str, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: tests/test_persona_manager.py:test_delete_persona]
|
||||
"""
|
||||
path = self._get_path(scope)
|
||||
data = self._load_file(path)
|
||||
if "personas" in data and name in data["personas"]:
|
||||
@@ -70,6 +82,9 @@ class PersonaManager:
|
||||
self._save_file(path, data)
|
||||
|
||||
def _load_file(self, path: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
[C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope, src/presets.py:PresetManager.load_all, src/presets.py:PresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.load_all_profiles, src/workspace_manager.py:WorkspaceManager.save_profile]
|
||||
"""
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
@@ -79,7 +94,9 @@ class PersonaManager:
|
||||
return {}
|
||||
|
||||
def _save_file(self, path: Path, data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
[C: src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile]
|
||||
"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
|
||||
|
||||
+18
-3
@@ -18,7 +18,10 @@ class PresetManager:
|
||||
return get_project_presets_path(self.project_root) if self.project_root else None
|
||||
|
||||
def load_all(self) -> Dict[str, Preset]:
|
||||
"""Merges global and project presets into a single dictionary."""
|
||||
"""
|
||||
Merges global and project presets into a single dictionary.
|
||||
[C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||
"""
|
||||
presets: Dict[str, Preset] = {}
|
||||
|
||||
# Load global presets
|
||||
@@ -41,7 +44,10 @@ class PresetManager:
|
||||
return presets
|
||||
|
||||
def save_preset(self, preset: Preset, scope: str = "project") -> None:
|
||||
"""Saves a preset to either the global or project-specific TOML file."""
|
||||
"""
|
||||
Saves a preset to either the global or project-specific TOML file.
|
||||
[C: tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_preset_manager.py:test_save_preset_project_no_root, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||
"""
|
||||
path = self.global_path if scope == "global" else self.project_path
|
||||
if not path:
|
||||
if scope == "project":
|
||||
@@ -56,6 +62,9 @@ class PresetManager:
|
||||
self._save_file(path, data)
|
||||
|
||||
def delete_preset(self, name: str, scope: str) -> None:
|
||||
"""
|
||||
[C: tests/test_preset_manager.py:test_delete_preset, tests/test_presets.py:TestPresetManager.test_delete_preset]
|
||||
"""
|
||||
if scope == "project" and self.project_root:
|
||||
path = get_project_presets_path(self.project_root)
|
||||
else:
|
||||
@@ -82,6 +91,9 @@ class PresetManager:
|
||||
return "project"
|
||||
|
||||
def _load_file(self, path: Path) -> Dict[str, Any]:
|
||||
"""
|
||||
[C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.load_all_profiles, src/workspace_manager.py:WorkspaceManager.save_profile]
|
||||
"""
|
||||
if not path.exists():
|
||||
return {"presets": {}}
|
||||
try:
|
||||
@@ -97,8 +109,11 @@ class PresetManager:
|
||||
return {"presets": {}}
|
||||
|
||||
def _save_file(self, path: Path, data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
[C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile]
|
||||
"""
|
||||
if path.parent.exists() and path.parent.is_file():
|
||||
raise ValueError(f"Cannot save to {path}: Parent directory {path.parent} is a file.")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(tomli_w.dumps(data).encode("utf-8"))
|
||||
f.write(tomli_w.dumps(data).encode("utf-8"))
|
||||
+85
-36
@@ -29,7 +29,10 @@ def parse_ts(s: str) -> Optional[datetime.datetime]:
|
||||
# ── entry serialisation ──────────────────────────────────────────────────────
|
||||
|
||||
def entry_to_str(entry: dict[str, Any]) -> str:
|
||||
"""Serialise a disc entry dict -> stored string."""
|
||||
"""
|
||||
Serialise a disc entry dict -> stored string.
|
||||
[C: tests/test_thinking_persistence.py:test_entry_to_str_with_thinking]
|
||||
"""
|
||||
ts = entry.get("ts", "")
|
||||
role = entry.get("role", "User")
|
||||
content = entry.get("content", "")
|
||||
@@ -46,7 +49,10 @@ def entry_to_str(entry: dict[str, Any]) -> str:
|
||||
return f"{role}:\n{content}"
|
||||
|
||||
def str_to_entry(raw: str, roles: list[str]) -> dict[str, Any]:
|
||||
"""Parse a stored string back to a disc entry dict."""
|
||||
"""
|
||||
Parse a stored string back to a disc entry dict.
|
||||
[C: tests/test_thinking_persistence.py:test_str_to_entry_with_thinking]
|
||||
"""
|
||||
ts = ""
|
||||
rest = raw
|
||||
if rest.startswith("@"):
|
||||
@@ -93,9 +99,15 @@ def get_git_log(git_dir: str, n: int = 5) -> str:
|
||||
# ── default structures ───────────────────────────────────────────────────────
|
||||
|
||||
def default_discussion() -> dict[str, Any]:
|
||||
"""
|
||||
[C: tests/test_discussion_takes.py:TestDiscussionTakes.test_promote_take_renames_discussion]
|
||||
"""
|
||||
return {"git_commit": "", "last_updated": now_ts(), "history": []}
|
||||
|
||||
def default_project(name: str = "unnamed") -> dict[str, Any]:
|
||||
"""
|
||||
[C: tests/test_deepseek_infra.py:test_default_project_includes_reasoning_role, tests/test_discussion_takes.py:TestDiscussionTakes.setUp, tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_save_separation, tests/test_project_manager_modes.py:test_default_project_execution_mode, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_default_roles_include_context, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip]
|
||||
"""
|
||||
return {
|
||||
"project": {"name": name, "git_dir": "", "system_prompt": "", "main_context": "", "execution_mode": "native"},
|
||||
"output": {"output_dir": "./md_gen"},
|
||||
@@ -148,15 +160,20 @@ def default_project(name: str = "unnamed") -> dict[str, Any]:
|
||||
# ── load / save ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_history_path(project_path: Union[str, Path]) -> Path:
|
||||
"""Return the Path to the sibling history TOML file for a given project."""
|
||||
"""
|
||||
Return the Path to the sibling history TOML file for a given project.
|
||||
[C: tests/test_history_management.py:test_save_separation]
|
||||
"""
|
||||
p = Path(project_path)
|
||||
return p.parent / f"{p.stem}_history.toml"
|
||||
|
||||
def load_project(path: Union[str, Path]) -> dict[str, Any]:
|
||||
"""
|
||||
Load a project TOML file.
|
||||
Automatically migrates legacy 'discussion' keys to a sibling history file.
|
||||
"""
|
||||
|
||||
Load a project TOML file.
|
||||
Automatically migrates legacy 'discussion' keys to a sibling history file.
|
||||
[C: tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_migration_on_load, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip]
|
||||
"""
|
||||
with open(path, "rb") as f:
|
||||
proj = tomllib.load(f)
|
||||
# Deserialise FileItems in files.paths
|
||||
@@ -176,7 +193,10 @@ def load_project(path: Union[str, Path]) -> dict[str, Any]:
|
||||
return proj
|
||||
|
||||
def load_history(project_path: Union[str, Path]) -> dict[str, Any]:
|
||||
"""Load the segregated discussion history from its dedicated TOML file."""
|
||||
"""
|
||||
Load the segregated discussion history from its dedicated TOML file.
|
||||
[C: tests/test_thinking_persistence.py:test_save_and_load_history_with_thinking_segments]
|
||||
"""
|
||||
hist_path = get_history_path(project_path)
|
||||
if hist_path.exists():
|
||||
with open(hist_path, "rb") as f:
|
||||
@@ -184,7 +204,10 @@ def load_history(project_path: Union[str, Path]) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
def clean_nones(data: Any) -> Any:
|
||||
"""Recursively remove None values from a dictionary/list."""
|
||||
"""
|
||||
Recursively remove None values from a dictionary/list.
|
||||
[C: tests/test_thinking_persistence.py:test_clean_nones_removes_thinking]
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
return {k: clean_nones(v) for k, v in data.items() if v is not None}
|
||||
elif isinstance(data, list):
|
||||
@@ -193,9 +216,11 @@ def clean_nones(data: Any) -> Any:
|
||||
|
||||
def save_project(proj: dict[str, Any], path: Union[str, Path], disc_data: Optional[dict[str, Any]] = None) -> None:
|
||||
"""
|
||||
Save the project TOML.
|
||||
If 'discussion' is present in proj, it is moved to the sibling history file.
|
||||
"""
|
||||
|
||||
Save the project TOML.
|
||||
If 'discussion' is present in proj, it is moved to the sibling history file.
|
||||
[C: tests/test_history_management.py:test_history_persistence_across_turns, tests/test_history_management.py:test_save_separation, tests/test_project_manager_modes.py:test_load_save_execution_mode, tests/test_project_serialization.py:TestProjectSerialization.test_fileitem_roundtrip, tests/test_thinking_persistence.py:test_save_and_load_history_with_thinking_segments]
|
||||
"""
|
||||
proj = clean_nones(proj)
|
||||
# Serialise FileItems
|
||||
if "files" in proj and "paths" in proj["files"]:
|
||||
@@ -253,7 +278,10 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id:
|
||||
# ── context presets ──────────────────────────────────────────────────────────
|
||||
|
||||
def save_context_preset(project_dict: dict, preset_name: str, files: list[str], screenshots: list[str]) -> None:
|
||||
"""Save a named context preset (files + screenshots) into the project dict."""
|
||||
"""
|
||||
Save a named context preset (files + screenshots) into the project dict.
|
||||
[C: tests/test_context_presets.py:test_save_context_preset]
|
||||
"""
|
||||
if "context_presets" not in project_dict:
|
||||
project_dict["context_presets"] = {}
|
||||
project_dict["context_presets"][preset_name] = {
|
||||
@@ -262,21 +290,29 @@ def save_context_preset(project_dict: dict, preset_name: str, files: list[str],
|
||||
}
|
||||
|
||||
def load_context_preset(project_dict: dict, preset_name: str) -> dict:
|
||||
"""Return the files and screenshots for a named preset."""
|
||||
"""
|
||||
Return the files and screenshots for a named preset.
|
||||
[C: tests/test_context_presets.py:test_load_context_preset, tests/test_context_presets.py:test_load_nonexistent_preset]
|
||||
"""
|
||||
if "context_presets" not in project_dict or preset_name not in project_dict["context_presets"]:
|
||||
raise KeyError(f"Preset '{preset_name}' not found in project context_presets.")
|
||||
return project_dict["context_presets"][preset_name]
|
||||
|
||||
def delete_context_preset(project_dict: dict, preset_name: str) -> None:
|
||||
"""Remove a named preset if it exists."""
|
||||
"""
|
||||
Remove a named preset if it exists.
|
||||
[C: tests/test_context_presets.py:test_delete_context_preset, tests/test_context_presets.py:test_delete_nonexistent_preset_no_error]
|
||||
"""
|
||||
if "context_presets" in project_dict:
|
||||
project_dict["context_presets"].pop(preset_name, None)
|
||||
# ── track state persistence ─────────────────────────────────────────────────
|
||||
|
||||
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
||||
"""
|
||||
Saves a TrackState object to conductor/tracks/<track_id>/state.toml.
|
||||
"""
|
||||
|
||||
Saves a TrackState object to conductor/tracks/<track_id>/state.toml.
|
||||
[C: tests/test_project_manager_tracks.py:test_get_all_tracks_with_state, tests/test_track_state_persistence.py:test_track_state_persistence]
|
||||
"""
|
||||
track_dir = paths.get_track_state_dir(track_id, project_path=str(base_dir))
|
||||
track_dir.mkdir(parents=True, exist_ok=True)
|
||||
state_file = track_dir / "state.toml"
|
||||
@@ -286,8 +322,10 @@ def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Pa
|
||||
|
||||
def load_track_state(track_id: str, base_dir: Union[str, Path] = ".") -> Optional['TrackState']:
|
||||
"""
|
||||
Loads a TrackState object from conductor/tracks/<track_id>/state.toml.
|
||||
"""
|
||||
|
||||
Loads a TrackState object from conductor/tracks/<track_id>/state.toml.
|
||||
[C: tests/test_track_state_persistence.py:test_track_state_persistence]
|
||||
"""
|
||||
from src.models import TrackState
|
||||
state_file = paths.get_track_state_dir(track_id, project_path=str(base_dir)) / 'state.toml'
|
||||
if not state_file.exists():
|
||||
@@ -298,9 +336,10 @@ def load_track_state(track_id: str, base_dir: Union[str, Path] = ".") -> Optiona
|
||||
|
||||
def load_track_history(track_id: str, base_dir: Union[str, Path] = ".") -> list[str]:
|
||||
"""
|
||||
Loads the discussion history for a specific track from its state.toml.
|
||||
Returns a list of entry strings formatted with @timestamp.
|
||||
"""
|
||||
|
||||
Loads the discussion history for a specific track from its state.toml.
|
||||
Returns a list of entry strings formatted with @timestamp.
|
||||
"""
|
||||
state = load_track_state(track_id, base_dir)
|
||||
if not state:
|
||||
return []
|
||||
@@ -315,9 +354,10 @@ def load_track_history(track_id: str, base_dir: Union[str, Path] = ".") -> list[
|
||||
|
||||
def save_track_history(track_id: str, history: list[str], base_dir: Union[str, Path] = ".") -> None:
|
||||
"""
|
||||
Saves the discussion history for a specific track to its state.toml.
|
||||
'history' is expected to be a list of formatted strings.
|
||||
"""
|
||||
|
||||
Saves the discussion history for a specific track to its state.toml.
|
||||
'history' is expected to be a list of formatted strings.
|
||||
"""
|
||||
state = load_track_state(track_id, base_dir)
|
||||
if not state:
|
||||
return
|
||||
@@ -328,12 +368,14 @@ def save_track_history(track_id: str, history: list[str], base_dir: Union[str, P
|
||||
|
||||
def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
|
||||
"""
|
||||
Scans the conductor/tracks/ directory and returns a list of dictionaries
|
||||
containing track metadata: 'id', 'title', 'status', 'complete', 'total',
|
||||
and 'progress' (0.0 to 1.0).
|
||||
Handles missing or malformed metadata.json or state.toml by falling back
|
||||
to available info or defaults.
|
||||
"""
|
||||
|
||||
Scans the conductor/tracks/ directory and returns a list of dictionaries
|
||||
containing track metadata: 'id', 'title', 'status', 'complete', 'total',
|
||||
and 'progress' (0.0 to 1.0).
|
||||
Handles missing or malformed metadata.json or state.toml by falling back
|
||||
to available info or defaults.
|
||||
[C: tests/test_project_manager_tracks.py:test_get_all_tracks_empty, tests/test_project_manager_tracks.py:test_get_all_tracks_malformed, tests/test_project_manager_tracks.py:test_get_all_tracks_with_metadata_json, tests/test_project_manager_tracks.py:test_get_all_tracks_with_state, tests/test_project_paths.py:test_get_all_tracks_project_specific]
|
||||
"""
|
||||
tracks_dir = paths.get_tracks_dir(project_path=str(base_dir))
|
||||
if not tracks_dir.exists():
|
||||
return []
|
||||
@@ -394,8 +436,10 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
|
||||
|
||||
def calculate_track_progress(tickets: list) -> dict:
|
||||
"""
|
||||
Calculates track progress based on ticket statuses.
|
||||
percentage (float), completed (int), total (int), in_progress (int), blocked (int), todo (int)
|
||||
|
||||
Calculates track progress based on ticket statuses.
|
||||
percentage (float), completed (int), total (int), in_progress (int), blocked (int), todo (int)
|
||||
[C: tests/test_progress_viz.py:test_calculate_track_progress_all_completed, tests/test_progress_viz.py:test_calculate_track_progress_all_todo, tests/test_progress_viz.py:test_calculate_track_progress_empty, tests/test_progress_viz.py:test_calculate_track_progress_mixed]
|
||||
"""
|
||||
total = len(tickets)
|
||||
if total == 0:
|
||||
@@ -427,8 +471,10 @@ def calculate_track_progress(tickets: list) -> dict:
|
||||
|
||||
def branch_discussion(project_dict: dict, source_id: str, new_id: str, message_index: int) -> None:
|
||||
"""
|
||||
Creates a new discussion in project_dict['discussion']['discussions'] by copying
|
||||
the history from source_id up to (and including) message_index, and sets active to new_id.
|
||||
|
||||
Creates a new discussion in project_dict['discussion']['discussions'] by copying
|
||||
the history from source_id up to (and including) message_index, and sets active to new_id.
|
||||
[C: tests/test_discussion_takes.py:TestDiscussionTakes.test_branch_discussion_creates_new_take]
|
||||
"""
|
||||
if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]:
|
||||
return
|
||||
@@ -445,7 +491,10 @@ def branch_discussion(project_dict: dict, source_id: str, new_id: str, message_i
|
||||
project_dict["discussion"]["active"] = new_id
|
||||
|
||||
def promote_take(project_dict: dict, take_id: str, new_id: str) -> None:
|
||||
"""Renames a take_id to new_id in the discussions dict."""
|
||||
"""
|
||||
Renames a take_id to new_id in the discussions dict.
|
||||
[C: tests/test_discussion_takes.py:TestDiscussionTakes.test_promote_take_renames_discussion]
|
||||
"""
|
||||
if "discussion" not in project_dict or "discussions" not in project_dict["discussion"]:
|
||||
return
|
||||
if take_id not in project_dict["discussion"]["discussions"]:
|
||||
@@ -456,4 +505,4 @@ def promote_take(project_dict: dict, take_id: str, new_id: str) -> None:
|
||||
|
||||
# If the take was active, update the active pointer
|
||||
if project_dict["discussion"].get("active") == take_id:
|
||||
project_dict["discussion"]["active"] = new_id
|
||||
project_dict["discussion"]["active"] = new_id
|
||||
+10
-1
@@ -94,6 +94,9 @@ class RAGEngine:
|
||||
return self.collection.count() == 0
|
||||
|
||||
def add_documents(self, ids: List[str], texts: List[str], metadatas: Optional[List[Dict[str, Any]]] = None):
|
||||
"""
|
||||
[C: tests/test_rag_engine.py:test_rag_engine_chroma]
|
||||
"""
|
||||
if not self.config.enabled or self.collection == "mock":
|
||||
return
|
||||
embeddings = self.embedding_provider.embed(texts)
|
||||
@@ -202,6 +205,9 @@ class RAGEngine:
|
||||
return asyncio.run(_async_search_mcp())
|
||||
|
||||
def search(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
[C: tests/mock_concurrent_mma.py:main, tests/test_rag_engine.py:test_rag_engine_chroma]
|
||||
"""
|
||||
if not self.config.enabled:
|
||||
return []
|
||||
if self.config.vector_store.provider == 'mcp':
|
||||
@@ -227,6 +233,9 @@ class RAGEngine:
|
||||
return ret
|
||||
|
||||
def delete_documents(self, ids: List[str]):
|
||||
"""
|
||||
[C: tests/test_rag_engine.py:test_rag_engine_chroma]
|
||||
"""
|
||||
if not self.config.enabled or self.collection == "mock":
|
||||
return
|
||||
self.collection.delete(ids=ids)
|
||||
@@ -243,4 +252,4 @@ class RAGEngine:
|
||||
if not self.config.enabled or self.collection == "mock":
|
||||
return
|
||||
for path in file_paths:
|
||||
self.collection.delete(where={"path": path})
|
||||
self.collection.delete(where={"path": path})
|
||||
+24
-14
@@ -43,9 +43,11 @@ def _now_ts() -> str:
|
||||
|
||||
def open_session(label: Optional[str] = None) -> None:
|
||||
"""
|
||||
Called once at GUI startup. Creates the log directories if needed and
|
||||
opens the log files for this session within a sub-directory.
|
||||
"""
|
||||
|
||||
Called once at GUI startup. Creates the log directories if needed and
|
||||
opens the log files for this session within a sub-directory.
|
||||
[C: tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_logging_e2e.py:test_logging_e2e, tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs, tests/test_session_logger_optimization.py:test_session_directory_and_subdirectories_creation, tests/test_session_logger_reset.py:test_reset_session, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry]
|
||||
"""
|
||||
global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq, _output_seq
|
||||
if _comms_fh is not None:
|
||||
return
|
||||
@@ -86,7 +88,10 @@ def open_session(label: Optional[str] = None) -> None:
|
||||
atexit.register(close_session)
|
||||
|
||||
def close_session() -> None:
|
||||
"""Flush and close all log files. Called on clean exit."""
|
||||
"""
|
||||
Flush and close all log files. Called on clean exit.
|
||||
[C: tests/test_app_controller_offloading.py:tmp_session_dir, tests/test_logging_e2e.py:e2e_setup, tests/test_logging_e2e.py:test_logging_e2e, tests/test_session_logger_optimization.py:temp_session_setup, tests/test_session_logger_reset.py:temp_logs, tests/test_session_logging.py:temp_logs]
|
||||
"""
|
||||
global _comms_fh, _tool_fh, _api_fh, _cli_fh, _session_id
|
||||
if _comms_fh is None:
|
||||
return
|
||||
@@ -130,9 +135,11 @@ def log_api_hook(method: str, path: str, payload: str) -> None:
|
||||
|
||||
def log_comms(entry: dict[str, Any]) -> None:
|
||||
"""
|
||||
Append one comms entry to the comms log file as a JSON-L line.
|
||||
Thread-safe (GIL + line-buffered file).
|
||||
"""
|
||||
|
||||
Append one comms entry to the comms log file as a JSON-L line.
|
||||
Thread-safe (GIL + line-buffered file).
|
||||
[C: tests/test_logging_e2e.py:test_logging_e2e]
|
||||
"""
|
||||
if _comms_fh is None:
|
||||
return
|
||||
try:
|
||||
@@ -142,9 +149,11 @@ def log_comms(entry: dict[str, Any]) -> None:
|
||||
|
||||
def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Append a tool-call record to the toolcalls log and write the PS1 script to
|
||||
the session's scripts directory. Returns the path of the written script file.
|
||||
"""
|
||||
|
||||
Append a tool-call record to the toolcalls log and write the PS1 script to
|
||||
the session's scripts directory. Returns the path of the written script file.
|
||||
[C: tests/test_session_logger_optimization.py:test_log_tool_call_saves_in_session_scripts]
|
||||
"""
|
||||
global _seq
|
||||
if _tool_fh is None:
|
||||
return script_path
|
||||
@@ -184,9 +193,11 @@ def log_tool_call(script: str, result: str, script_path: Optional[str]) -> Optio
|
||||
|
||||
def log_tool_output(content: str) -> Optional[str]:
|
||||
"""
|
||||
Save tool output content to a unique file in the session's outputs directory.
|
||||
Returns the path of the written file.
|
||||
"""
|
||||
|
||||
Save tool output content to a unique file in the session's outputs directory.
|
||||
Returns the path of the written file.
|
||||
[C: tests/test_session_logger_optimization.py:test_log_tool_output_returns_none_if_no_session, tests/test_session_logger_optimization.py:test_log_tool_output_saves_in_session_outputs]
|
||||
"""
|
||||
global _output_seq
|
||||
if _session_dir is None:
|
||||
return None
|
||||
@@ -222,4 +233,3 @@ def log_cli_call(command: str, stdin_content: Optional[str], stdout_content: Opt
|
||||
_cli_fh.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
+19
-1
@@ -7,6 +7,9 @@ class ShaderManager:
|
||||
self.pp_program = None
|
||||
|
||||
def compile_shader(self, vertex_src: str, fragment_src: str) -> int:
|
||||
"""
|
||||
[C: tests/test_shader_manager.py:test_shader_manager_initialization_and_compilation]
|
||||
"""
|
||||
program = gl.glCreateProgram()
|
||||
|
||||
def _compile(src, shader_type):
|
||||
@@ -41,6 +44,9 @@ class ShaderManager:
|
||||
return program
|
||||
|
||||
def update_uniforms(self, uniforms: dict):
|
||||
"""
|
||||
[C: tests/test_shader_manager.py:test_shader_manager_uniform_update]
|
||||
"""
|
||||
if self.program is None:
|
||||
return
|
||||
|
||||
@@ -62,6 +68,9 @@ class ShaderManager:
|
||||
gl.glUniform4f(loc, value[0], value[1], value[2], value[3])
|
||||
|
||||
def setup_background_shader(self):
|
||||
"""
|
||||
[C: tests/test_dynamic_background.py:test_dynamic_background_rendering]
|
||||
"""
|
||||
vertex_src = """
|
||||
#version 330 core
|
||||
const vec2 positions[4] = vec2[](
|
||||
@@ -88,6 +97,9 @@ void main() {
|
||||
self.bg_program = self.compile_shader(vertex_src, fragment_src)
|
||||
|
||||
def render_background(self, width, height, time):
|
||||
"""
|
||||
[C: tests/test_dynamic_background.py:test_dynamic_background_rendering]
|
||||
"""
|
||||
if not self.bg_program:
|
||||
return
|
||||
gl.glUseProgram(self.bg_program)
|
||||
@@ -101,6 +113,9 @@ void main() {
|
||||
gl.glUseProgram(0)
|
||||
|
||||
def setup_post_process_shader(self):
|
||||
"""
|
||||
[C: tests/test_post_process.py:TestPostProcess.test_setup_post_process_shader]
|
||||
"""
|
||||
vertex_src = """
|
||||
#version 330 core
|
||||
const vec2 positions[4] = vec2[](
|
||||
@@ -137,6 +152,9 @@ void main() {
|
||||
self.pp_program = self.compile_shader(vertex_src, fragment_src)
|
||||
|
||||
def render_post_process(self, texture_id, width, height, time):
|
||||
"""
|
||||
[C: tests/test_post_process.py:TestPostProcess.test_render_post_process]
|
||||
"""
|
||||
if not self.pp_program:
|
||||
return
|
||||
gl.glUseProgram(self.pp_program)
|
||||
@@ -150,4 +168,4 @@ void main() {
|
||||
gl.glUniform1f(u_time_loc, float(time))
|
||||
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
|
||||
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
|
||||
gl.glUseProgram(0)
|
||||
gl.glUseProgram(0)
|
||||
+6
-5
@@ -2,8 +2,9 @@ from imgui_bundle import imgui
|
||||
|
||||
def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: imgui.ImVec2, color: imgui.ImVec4, shadow_size: float = 10.0, rounding: float = 0.0) -> None:
|
||||
"""
|
||||
Simulates a soft shadow effect by drawing multiple concentric rounded rectangles
|
||||
with decreasing alpha values. This is a faux-shader effect using primitive batching.
|
||||
|
||||
Simulates a soft shadow effect by drawing multiple concentric rounded rectangles
|
||||
with decreasing alpha values. This is a faux-shader effect using primitive batching.
|
||||
"""
|
||||
r, g, b, a = color.x, color.y, color.z, color.w
|
||||
steps = int(shadow_size)
|
||||
@@ -38,8 +39,9 @@ def draw_soft_shadow(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: im
|
||||
|
||||
def apply_faux_acrylic_glass(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p_max: imgui.ImVec2, base_color: imgui.ImVec4, rounding: float = 0.0) -> None:
|
||||
"""
|
||||
Simulates a faux acrylic/glass effect by drawing a semi-transparent base,
|
||||
a gradient overlay for 'shine', and a subtle inner border for 'edge glow'.
|
||||
|
||||
Simulates a faux acrylic/glass effect by drawing a semi-transparent base,
|
||||
a gradient overlay for 'shine', and a subtle inner border for 'edge glow'.
|
||||
"""
|
||||
r, g, b, a = base_color.x, base_color.y, base_color.z, base_color.w
|
||||
|
||||
@@ -70,4 +72,3 @@ def apply_faux_acrylic_glass(draw_list: imgui.ImDrawList, p_min: imgui.ImVec2, p
|
||||
flags=imgui.ImDrawFlags_.round_corners_all if rounding > 0 else imgui.ImDrawFlags_.none,
|
||||
thickness=1.0
|
||||
)
|
||||
|
||||
|
||||
+9
-8
@@ -53,13 +53,15 @@ def _build_subprocess_env() -> dict[str, str]:
|
||||
|
||||
def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None, patch_callback: Optional[Callable[[str, str], Optional[str]]] = None) -> str:
|
||||
"""
|
||||
Run a PowerShell script with working directory set to base_dir.
|
||||
Returns a string combining stdout, stderr, and exit code.
|
||||
Environment is configured via mcp_env.toml (project root).
|
||||
If qa_callback is provided and the command fails or has stderr,
|
||||
the callback is called with the stderr content and its result is appended.
|
||||
If patch_callback is provided, it receives (error, file_context) and returns patch text.
|
||||
"""
|
||||
|
||||
Run a PowerShell script with working directory set to base_dir.
|
||||
Returns a string combining stdout, stderr, and exit code.
|
||||
Environment is configured via mcp_env.toml (project root).
|
||||
If qa_callback is provided and the command fails or has stderr,
|
||||
the callback is called with the stderr content and its result is appended.
|
||||
If patch_callback is provided, it receives (error, file_context) and returns patch text.
|
||||
[C: tests/test_tier4_interceptor.py:test_run_powershell_no_qa_callback_on_success, tests/test_tier4_interceptor.py:test_run_powershell_optional_qa_callback, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_failure, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_stderr_only]
|
||||
"""
|
||||
safe_dir: str = str(base_dir).replace("'", "''")
|
||||
full_script: str = f"Set-Location -LiteralPath '{safe_dir}'\n{script}"
|
||||
exe: Optional[str] = next((x for x in ["powershell.exe", "pwsh.exe", "powershell", "pwsh"] if shutil.which(x)), None)
|
||||
@@ -97,4 +99,3 @@ def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[s
|
||||
if 'process' in locals() and process:
|
||||
subprocess.run(["taskkill", "/F", "/T", "/PID", str(process.pid)], capture_output=True)
|
||||
return f"ERROR: {e}"
|
||||
|
||||
|
||||
+12
-9
@@ -153,8 +153,10 @@ _SUMMARISERS: dict[str, Callable[[Path, str], str]] = {
|
||||
|
||||
def summarise_file(path: Path, content: str) -> str:
|
||||
"""
|
||||
Return a compact markdown summary string for a single file.
|
||||
`content` is the already-read file text (or an error string).
|
||||
|
||||
Return a compact markdown summary string for a single file.
|
||||
`content` is the already-read file text (or an error string).
|
||||
[C: tests/test_subagent_summarization.py:test_summarise_file_integration]
|
||||
"""
|
||||
content_hash = get_file_hash(content)
|
||||
cached = _summary_cache.get_summary(str(path), content_hash)
|
||||
@@ -190,9 +192,10 @@ def summarise_file(path: Path, content: str) -> str:
|
||||
|
||||
def summarise_items(file_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Given a list of file_item dicts (as returned by aggregate.build_file_items),
|
||||
return a parallel list of dicts with an added `summary` key.
|
||||
"""
|
||||
|
||||
Given a list of file_item dicts (as returned by aggregate.build_file_items),
|
||||
return a parallel list of dicts with an added `summary` key.
|
||||
"""
|
||||
result = []
|
||||
for item in file_items:
|
||||
path = item.get("path")
|
||||
@@ -208,9 +211,10 @@ def summarise_items(file_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
|
||||
def build_summary_markdown(file_items: list[dict[str, Any]]) -> str:
|
||||
"""
|
||||
Build a compact markdown string of file summaries, suitable for the
|
||||
initial <context> block instead of full file contents.
|
||||
"""
|
||||
|
||||
Build a compact markdown string of file summaries, suitable for the
|
||||
initial <context> block instead of full file contents.
|
||||
"""
|
||||
summarised = summarise_items(file_items)
|
||||
parts = []
|
||||
for item in summarised:
|
||||
@@ -218,4 +222,3 @@ def build_summary_markdown(file_items: list[dict[str, Any]]) -> str:
|
||||
summary = item.get("summary", "")
|
||||
parts.append(f"### `{path}`\n\n{summary}")
|
||||
return "\n\n---\n\n".join(parts)
|
||||
|
||||
|
||||
+25
-9
@@ -4,14 +4,18 @@ from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
|
||||
def get_file_hash(content: str) -> str:
|
||||
"""Returns SHA256 hash of the content."""
|
||||
"""
|
||||
Returns SHA256 hash of the content.
|
||||
[C: tests/test_summary_cache.py:test_get_file_hash, tests/test_summary_cache.py:test_summary_cache]
|
||||
"""
|
||||
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||
|
||||
class SummaryCache:
|
||||
"""
|
||||
A hash-based cache for file summaries to avoid redundant processing.
|
||||
Invalidates when content hash changes.
|
||||
"""
|
||||
|
||||
A hash-based cache for file summaries to avoid redundant processing.
|
||||
Invalidates when content hash changes.
|
||||
"""
|
||||
def __init__(self, cache_file: Optional[str] = None, max_entries: int = 1000):
|
||||
if cache_file:
|
||||
self.cache_file = Path(cache_file)
|
||||
@@ -23,7 +27,10 @@ class SummaryCache:
|
||||
self.load()
|
||||
|
||||
def load(self) -> None:
|
||||
"""Loads cache from disk."""
|
||||
"""
|
||||
Loads cache from disk.
|
||||
[C: src/tool_presets.py:ToolPresetManager._read_raw, src/workspace_manager.py:WorkspaceManager._load_file, tests/test_gui_phase3.py:test_create_track, tests/test_history_management.py:test_save_separation, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_session_logging.py:test_open_session_creates_subdir_and_registry, tests/test_visual_sim_gui_ux.py:test_gui_track_creation]
|
||||
"""
|
||||
if self.cache_file.exists():
|
||||
try:
|
||||
with open(self.cache_file, "r", encoding="utf-8") as f:
|
||||
@@ -41,7 +48,10 @@ class SummaryCache:
|
||||
pass
|
||||
|
||||
def get_summary(self, file_path: str, content_hash: str) -> Optional[str]:
|
||||
"""Returns cached summary if hash matches, otherwise None."""
|
||||
"""
|
||||
Returns cached summary if hash matches, otherwise None.
|
||||
[C: tests/test_summary_cache.py:test_summary_cache, tests/test_summary_cache.py:test_summary_cache_lru]
|
||||
"""
|
||||
entry = self.cache.get(file_path)
|
||||
if entry and entry.get("hash") == content_hash:
|
||||
# LRU: move to end
|
||||
@@ -51,7 +61,10 @@ class SummaryCache:
|
||||
return None
|
||||
|
||||
def set_summary(self, file_path: str, content_hash: str, summary: str) -> None:
|
||||
"""Stores summary in cache and saves to disk."""
|
||||
"""
|
||||
Stores summary in cache and saves to disk.
|
||||
[C: tests/test_summary_cache.py:test_summary_cache, tests/test_summary_cache.py:test_summary_cache_lru]
|
||||
"""
|
||||
if file_path in self.cache:
|
||||
self.cache.pop(file_path)
|
||||
self.cache[file_path] = {
|
||||
@@ -66,7 +79,10 @@ class SummaryCache:
|
||||
self.save()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clears the cache both in-memory and on disk."""
|
||||
"""
|
||||
Clears the cache both in-memory and on disk.
|
||||
[C: tests/conftest.py:reset_ai_client]
|
||||
"""
|
||||
self.cache.clear()
|
||||
if self.cache_file.exists():
|
||||
try:
|
||||
@@ -85,4 +101,4 @@ class SummaryCache:
|
||||
return {
|
||||
"entries": len(self.cache),
|
||||
"size_bytes": size_bytes
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
def format_takes_diff(takes: dict[str, list[dict]]) -> str:
|
||||
"""
|
||||
[C: tests/test_synthesis_formatter.py:test_format_takes_diff_common_prefix, tests/test_synthesis_formatter.py:test_format_takes_diff_empty, tests/test_synthesis_formatter.py:test_format_takes_diff_no_common_prefix, tests/test_synthesis_formatter.py:test_format_takes_diff_single_take]
|
||||
"""
|
||||
if not takes:
|
||||
return ""
|
||||
|
||||
@@ -39,4 +42,4 @@ def format_takes_diff(takes: dict[str, list[dict]]) -> str:
|
||||
|
||||
variations_text = "=== Variations ===\n" + "\n".join(variation_lines)
|
||||
|
||||
return shared_text + "\n\n" + variations_text
|
||||
return shared_text + "\n\n" + variations_text
|
||||
+29
-15
@@ -293,11 +293,17 @@ def get_current_scale() -> float:
|
||||
return _current_scale
|
||||
|
||||
def get_shader_config(key: str) -> Any:
|
||||
"""Get a specific shader configuration value."""
|
||||
"""
|
||||
Get a specific shader configuration value.
|
||||
[C: tests/test_shader_config.py:test_shader_config_parsing]
|
||||
"""
|
||||
return _shader_config.get(key)
|
||||
|
||||
def get_window_frame_config() -> bool:
|
||||
"""Get the window frame configuration."""
|
||||
"""
|
||||
Get the window frame configuration.
|
||||
[C: tests/test_shader_config.py:test_shader_config_parsing]
|
||||
"""
|
||||
return _shader_config.get("custom_window_frame", False)
|
||||
|
||||
def get_palette_colours(name: str) -> dict[str, Any]:
|
||||
@@ -306,11 +312,13 @@ def get_palette_colours(name: str) -> dict[str, Any]:
|
||||
|
||||
def apply(palette_name: str, overrides: dict[str, Any] | None = None) -> None:
|
||||
"""
|
||||
Build a global DPG theme from the named palette plus optional per-colour
|
||||
overrides, and bind it as the default theme.
|
||||
|
||||
overrides: {colour_key: (R,G,B) or (R,G,B,A)} — merged on top of palette.
|
||||
"""
|
||||
|
||||
Build a global DPG theme from the named palette plus optional per-colour
|
||||
overrides, and bind it as the default theme.
|
||||
|
||||
overrides: {colour_key: (R,G,B) or (R,G,B,A)} — merged on top of palette.
|
||||
[C: src/theme_2.py:apply_current, src/theme_2.py:set_child_transparency, src/theme_2.py:set_transparency, tests/test_theme.py:test_theme_apply_sets_rounding_and_padding]
|
||||
"""
|
||||
global _current_theme_tag, _current_palette
|
||||
_current_palette = palette_name
|
||||
colours = dict(_PALETTES.get(palette_name, {}))
|
||||
@@ -349,11 +357,12 @@ def apply(palette_name: str, overrides: dict[str, Any] | None = None) -> None:
|
||||
|
||||
def apply_font(font_path: str, size: float = 14.0) -> None:
|
||||
"""
|
||||
Load the TTF at font_path at the given point size and bind it globally.
|
||||
Safe to call multiple times. Uses a single persistent font_registry; only
|
||||
the font *item* tag is tracked. Passing an empty path or a missing file
|
||||
resets to the DPG built-in font.
|
||||
"""
|
||||
|
||||
Load the TTF at font_path at the given point size and bind it globally.
|
||||
Safe to call multiple times. Uses a single persistent font_registry; only
|
||||
the font *item* tag is tracked. Passing an empty path or a missing file
|
||||
resets to the DPG built-in font.
|
||||
"""
|
||||
global _current_font_tag, _current_font_path, _current_font_size, _font_registry_tag
|
||||
_current_font_path = font_path
|
||||
_current_font_size = size
|
||||
@@ -378,7 +387,10 @@ def apply_font(font_path: str, size: float = 14.0) -> None:
|
||||
dpg.bind_font(font)
|
||||
|
||||
def set_scale(factor: float) -> None:
|
||||
"""Set the global Dear PyGui font/UI scale factor."""
|
||||
"""
|
||||
Set the global Dear PyGui font/UI scale factor.
|
||||
[C: src/theme_2.py:apply_current]
|
||||
"""
|
||||
global _current_scale
|
||||
_current_scale = factor
|
||||
dpg.set_global_font_scale(factor)
|
||||
@@ -392,7 +404,10 @@ def save_to_config(config: dict[str, Any]) -> None:
|
||||
config["theme"]["scale"] = _current_scale
|
||||
|
||||
def load_from_config(config: dict[str, Any]) -> None:
|
||||
"""Read [theme] from config and apply everything."""
|
||||
"""
|
||||
Read [theme] from config and apply everything.
|
||||
[C: tests/test_shader_config.py:test_shader_config_parsing]
|
||||
"""
|
||||
t = config.get("theme", {})
|
||||
palette = t.get("palette", "DPG Default")
|
||||
font_path = t.get("font_path", "")
|
||||
@@ -407,4 +422,3 @@ def load_from_config(config: dict[str, Any]) -> None:
|
||||
_shader_config["bloom"] = t.get("shader_bloom", False)
|
||||
_shader_config["bg"] = t.get("shader_bg", "none")
|
||||
_shader_config["custom_window_frame"] = t.get("custom_window_frame", False)
|
||||
|
||||
|
||||
+7
-4
@@ -18,7 +18,10 @@ import src.theme_nerv
|
||||
# Only keys that differ from the ImGui dark defaults need to be listed.
|
||||
|
||||
def _c(r: int, g: int, b: int, a: int = 255) -> tuple[float, float, float, float]:
|
||||
"""Convert 0-255 RGBA to 0.0-1.0 floats."""
|
||||
"""
|
||||
Convert 0-255 RGBA to 0.0-1.0 floats.
|
||||
[C: src/theme_nerv.py:module]
|
||||
"""
|
||||
return (r / 255.0, g / 255.0, b / 255.0, a / 255.0)
|
||||
|
||||
_PALETTES: dict[str, dict[int, tuple]] = {
|
||||
@@ -271,8 +274,9 @@ def set_child_transparency(val: float) -> None:
|
||||
|
||||
def apply(palette_name: str) -> None:
|
||||
"""
|
||||
Apply a named palette by setting all ImGui style colors and applying global professional styling.
|
||||
"""
|
||||
|
||||
Apply a named palette by setting all ImGui style colors and applying global professional styling.
|
||||
"""
|
||||
global _current_palette
|
||||
_current_palette = palette_name
|
||||
if palette_name == 'NERV':
|
||||
@@ -392,4 +396,3 @@ def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
|
||||
# Sync tweaks
|
||||
tt.tweaks.rounding = 6.0
|
||||
return tt
|
||||
|
||||
|
||||
+4
-2
@@ -62,7 +62,10 @@ NERV_PALETTE = {
|
||||
}
|
||||
|
||||
def apply_nerv() -> None:
|
||||
"""Apply NERV theme with hard edges and specific palette."""
|
||||
"""
|
||||
Apply NERV theme with hard edges and specific palette.
|
||||
[C: tests/test_theme_nerv.py:test_apply_nerv_sets_rounding_and_colors]
|
||||
"""
|
||||
style = imgui.get_style()
|
||||
for col_enum, rgba in NERV_PALETTE.items():
|
||||
style.set_color_(col_enum, imgui.ImVec4(*rgba))
|
||||
@@ -82,4 +85,3 @@ def apply_nerv() -> None:
|
||||
style.popup_border_size = 1.0
|
||||
style.child_border_size = 1.0
|
||||
style.tab_border_size = 1.0
|
||||
|
||||
|
||||
+12
-1
@@ -8,6 +8,9 @@ class CRTFilter:
|
||||
self.enabled = True
|
||||
|
||||
def render(self, width: float, height: float):
|
||||
"""
|
||||
[C: tests/test_theme_nerv_alert.py:test_alert_pulsing_render_active, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_inactive, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_render, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_disabled, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_render]
|
||||
"""
|
||||
if not self.enabled:
|
||||
return
|
||||
draw_list = imgui.get_foreground_draw_list()
|
||||
@@ -64,6 +67,9 @@ class CRTFilter:
|
||||
class StatusFlicker:
|
||||
def get_alpha(self) -> float:
|
||||
# Modulate between 0.7 and 1.0 using sin wave
|
||||
"""
|
||||
[C: tests/test_theme_nerv_fx.py:TestThemeNervFx.test_status_flicker_get_alpha]
|
||||
"""
|
||||
return 0.85 + 0.15 * math.sin(time.time() * 20.0)
|
||||
|
||||
class AlertPulsing:
|
||||
@@ -71,9 +77,15 @@ class AlertPulsing:
|
||||
self.active = False
|
||||
|
||||
def update(self, status: str):
|
||||
"""
|
||||
[C: tests/test_spawn_interception_v2.py:MockDialog.wait, tests/test_theme_nerv_alert.py:test_alert_pulsing_update, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_update]
|
||||
"""
|
||||
self.active = status.lower().startswith("error")
|
||||
|
||||
def render(self, width: float, height: float):
|
||||
"""
|
||||
[C: tests/test_theme_nerv_alert.py:test_alert_pulsing_render_active, tests/test_theme_nerv_alert.py:test_alert_pulsing_render_inactive, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_alert_pulsing_render, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_disabled, tests/test_theme_nerv_fx.py:TestThemeNervFx.test_crt_filter_render]
|
||||
"""
|
||||
if not self.active:
|
||||
return
|
||||
draw_list = imgui.get_foreground_draw_list()
|
||||
@@ -83,4 +95,3 @@ class AlertPulsing:
|
||||
alpha = 0.05 + 0.15 * ((math.sin(time.time() * 4.0) + 1.0) / 2.0)
|
||||
color = imgui.get_color_u32((1.0, 0.0, 0.0, alpha))
|
||||
draw_list.add_rect((0.0, 0.0), (width, height), color, 0.0, 0, 10.0)
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ from src.models import ThinkingSegment
|
||||
|
||||
def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
||||
"""
|
||||
Parses thinking segments from text and returns (segments, response_content).
|
||||
Support extraction of thinking traces from <thinking>...</thinking>, <thought>...</thought>,
|
||||
and blocks prefixed with Thinking:.
|
||||
|
||||
Parses thinking segments from text and returns (segments, response_content).
|
||||
Support extraction of thinking traces from <thinking>...</thinking>, <thought>...</thought>,
|
||||
and blocks prefixed with Thinking:.
|
||||
[C: tests/test_thinking_trace.py:test_parse_empty_response, tests/test_thinking_trace.py:test_parse_multiple_markers, tests/test_thinking_trace.py:test_parse_no_thinking, tests/test_thinking_trace.py:test_parse_text_thinking_prefix, tests/test_thinking_trace.py:test_parse_thinking_with_empty_response, tests/test_thinking_trace.py:test_parse_xml_thinking_tag, tests/test_thinking_trace.py:test_parse_xml_thought_tag]
|
||||
"""
|
||||
segments = []
|
||||
|
||||
@@ -50,4 +52,4 @@ def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
||||
colon_segments, final_remaining = extract_colon_blocks(remaining)
|
||||
segments.extend(colon_segments)
|
||||
|
||||
return segments, final_remaining.strip()
|
||||
return segments, final_remaining.strip()
|
||||
+6
-1
@@ -3,6 +3,9 @@ from src.models import Tool, ToolPreset, BiasProfile
|
||||
|
||||
class ToolBiasEngine:
|
||||
def apply_semantic_nudges(self, tool_definitions: List[Dict[str, Any]], preset: ToolPreset) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
[C: tests/test_tool_bias.py:test_apply_semantic_nudges, tests/test_tool_bias.py:test_parameter_bias_nudging]
|
||||
"""
|
||||
weight_map = {
|
||||
5: "[HIGH PRIORITY] ",
|
||||
4: "[PREFERRED] ",
|
||||
@@ -36,6 +39,9 @@ class ToolBiasEngine:
|
||||
return tool_definitions
|
||||
|
||||
def generate_tooling_strategy(self, preset: ToolPreset, global_bias: BiasProfile) -> str:
|
||||
"""
|
||||
[C: tests/test_tool_bias.py:test_generate_tooling_strategy]
|
||||
"""
|
||||
lines = ["### Tooling Strategy"]
|
||||
|
||||
preferred = []
|
||||
@@ -63,4 +69,3 @@ class ToolBiasEngine:
|
||||
lines.append(f"- {cat}: {mult}x")
|
||||
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
+25
-2
@@ -10,6 +10,9 @@ class ToolPresetManager:
|
||||
self.project_root = Path(project_root) if project_root else None
|
||||
|
||||
def _get_path(self, scope: str) -> Path:
|
||||
"""
|
||||
[C: src/workspace_manager.py:WorkspaceManager.delete_profile, src/workspace_manager.py:WorkspaceManager.save_profile]
|
||||
"""
|
||||
if scope == "global":
|
||||
return paths.get_global_tool_presets_path()
|
||||
elif scope == "project":
|
||||
@@ -34,6 +37,9 @@ class ToolPresetManager:
|
||||
tomli_w.dump(data, f)
|
||||
|
||||
def load_all_presets(self) -> Dict[str, ToolPreset]:
|
||||
"""
|
||||
[C: tests/test_tool_preset_manager.py:test_load_all_presets_merged]
|
||||
"""
|
||||
global_path = paths.get_global_tool_presets_path()
|
||||
global_data = self._read_raw(global_path).get("presets", {})
|
||||
|
||||
@@ -52,10 +58,16 @@ class ToolPresetManager:
|
||||
return presets
|
||||
|
||||
def load_all(self) -> Dict[str, ToolPreset]:
|
||||
"""Backward compatibility for load_all()."""
|
||||
"""
|
||||
Backward compatibility for load_all().
|
||||
[C: tests/test_persona_manager.py:test_delete_persona, tests/test_persona_manager.py:test_load_all_merged, tests/test_persona_manager.py:test_save_persona, tests/test_preset_manager.py:test_delete_preset, tests/test_preset_manager.py:test_load_all_merged, tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||
"""
|
||||
return self.load_all_presets()
|
||||
|
||||
def save_preset(self, preset: ToolPreset, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: tests/test_preset_manager.py:test_save_preset_global, tests/test_preset_manager.py:test_save_preset_project, tests/test_preset_manager.py:test_save_preset_project_no_root, tests/test_presets.py:TestPresetManager.test_delete_preset, tests/test_presets.py:TestPresetManager.test_project_overwrites_global, tests/test_presets.py:TestPresetManager.test_save_and_load_global, tests/test_presets.py:TestPresetManager.test_save_and_load_project]
|
||||
"""
|
||||
path = self._get_path(scope)
|
||||
data = self._read_raw(path)
|
||||
if "presets" not in data:
|
||||
@@ -64,6 +76,9 @@ class ToolPresetManager:
|
||||
self._write_raw(path, data)
|
||||
|
||||
def delete_preset(self, name: str, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: tests/test_preset_manager.py:test_delete_preset, tests/test_presets.py:TestPresetManager.test_delete_preset]
|
||||
"""
|
||||
path = self._get_path(scope)
|
||||
data = self._read_raw(path)
|
||||
if "presets" in data and name in data["presets"]:
|
||||
@@ -71,6 +86,9 @@ class ToolPresetManager:
|
||||
self._write_raw(path, data)
|
||||
|
||||
def load_all_bias_profiles(self) -> Dict[str, BiasProfile]:
|
||||
"""
|
||||
[C: tests/test_tool_preset_manager.py:test_bias_profiles_merged, tests/test_tool_preset_manager.py:test_delete_bias_profile, tests/test_tool_preset_manager.py:test_save_bias_profile]
|
||||
"""
|
||||
global_path = paths.get_global_tool_presets_path()
|
||||
global_data = self._read_raw(global_path).get("bias_profiles", {})
|
||||
|
||||
@@ -95,6 +113,9 @@ class ToolPresetManager:
|
||||
return profiles
|
||||
|
||||
def save_bias_profile(self, profile: BiasProfile, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: tests/test_tool_preset_manager.py:test_save_bias_profile]
|
||||
"""
|
||||
path = self._get_path(scope)
|
||||
data = self._read_raw(path)
|
||||
if "bias_profiles" not in data:
|
||||
@@ -103,9 +124,11 @@ class ToolPresetManager:
|
||||
self._write_raw(path, data)
|
||||
|
||||
def delete_bias_profile(self, name: str, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: tests/test_tool_preset_manager.py:test_delete_bias_profile]
|
||||
"""
|
||||
path = self._get_path(scope)
|
||||
data = self._read_raw(path)
|
||||
if "bias_profiles" in data and name in data["bias_profiles"]:
|
||||
del data["bias_profiles"][name]
|
||||
self._write_raw(path, data)
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ class WorkspaceManager:
|
||||
raise ValueError("Invalid scope, must be 'global' or 'project'")
|
||||
|
||||
def load_all_profiles(self) -> Dict[str, WorkspaceProfile]:
|
||||
"""Merges global and project profiles into a single dictionary."""
|
||||
"""
|
||||
Merges global and project profiles into a single dictionary.
|
||||
[C: tests/test_workspace_manager.py:test_delete_profile, tests/test_workspace_manager.py:test_load_all_profiles_merged, tests/test_workspace_manager.py:test_save_profile_global_and_project]
|
||||
"""
|
||||
profiles = {}
|
||||
|
||||
global_path = paths.get_global_workspace_profiles_path()
|
||||
@@ -42,6 +45,9 @@ class WorkspaceManager:
|
||||
return profiles
|
||||
|
||||
def save_profile(self, profile: WorkspaceProfile, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: tests/test_workspace_manager.py:test_delete_profile, tests/test_workspace_manager.py:test_save_profile_global_and_project]
|
||||
"""
|
||||
path = self._get_path(scope)
|
||||
data = self._load_file(path)
|
||||
if "profiles" not in data:
|
||||
@@ -51,6 +57,9 @@ class WorkspaceManager:
|
||||
self._save_file(path, data)
|
||||
|
||||
def delete_profile(self, name: str, scope: str = "project") -> None:
|
||||
"""
|
||||
[C: tests/test_workspace_manager.py:test_delete_profile]
|
||||
"""
|
||||
path = self._get_path(scope)
|
||||
data = self._load_file(path)
|
||||
if "profiles" in data and name in data["profiles"]:
|
||||
@@ -69,4 +78,4 @@ class WorkspaceManager:
|
||||
def _save_file(self, path: Path, data: Dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
tomli_w.dump(data, f)
|
||||
tomli_w.dump(data, f)
|
||||
+22
-10
@@ -28,6 +28,9 @@ class VerificationLogger:
|
||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def log_state(self, field: str, before: Any, after: Any) -> None:
|
||||
"""
|
||||
[C: tests/test_ai_style_formatter.py:test_multiple_top_level_definitions, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_conductor_tech_lead.py:test_topological_sort_vlog, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_failure, tests/test_vlogger_availability.py:test_vlogger_available]
|
||||
"""
|
||||
delta = ""
|
||||
if isinstance(before, (int, float)) and isinstance(after, (int, float)):
|
||||
diff = after - before
|
||||
@@ -40,6 +43,9 @@ class VerificationLogger:
|
||||
})
|
||||
|
||||
def finalize(self, title: str, status: str, result_msg: str) -> None:
|
||||
"""
|
||||
[C: tests/test_ai_style_formatter.py:test_multiple_top_level_definitions, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_conductor_tech_lead.py:test_topological_sort_vlog, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_tier4_interceptor.py:test_end_to_end_tier4_integration, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_failure, tests/test_tier4_interceptor.py:test_run_powershell_qa_callback_on_stderr_only, tests/test_vlogger_availability.py:test_vlogger_available]
|
||||
"""
|
||||
round(time.time() - self.start_time, 2)
|
||||
log_file = self.logs_dir / f"{self.script_name}.txt"
|
||||
with open(log_file, "w", encoding="utf-8") as f:
|
||||
@@ -57,7 +63,8 @@ class VerificationLogger:
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_paths() -> Generator[None, None, None]:
|
||||
"""
|
||||
Autouse fixture that resets the paths global state before each test.
|
||||
|
||||
Autouse fixture that resets the paths global state before each test.
|
||||
"""
|
||||
from src import paths
|
||||
paths.reset_resolved()
|
||||
@@ -67,8 +74,9 @@ def reset_paths() -> Generator[None, None, None]:
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_ai_client() -> Generator[None, None, None]:
|
||||
"""
|
||||
Autouse fixture that resets the ai_client global state before each test.
|
||||
This is critical for preventing state pollution between tests.
|
||||
|
||||
Autouse fixture that resets the ai_client global state before each test.
|
||||
This is critical for preventing state pollution between tests.
|
||||
"""
|
||||
from src import ai_client
|
||||
from src import mcp_client
|
||||
@@ -115,7 +123,8 @@ def kill_process_tree(pid: int | None) -> None:
|
||||
@pytest.fixture
|
||||
def mock_app() -> Generator[App, None, None]:
|
||||
"""
|
||||
Mock version of the App for simple unit tests that don't need a loop.
|
||||
|
||||
Mock version of the App for simple unit tests that don't need a loop.
|
||||
"""
|
||||
with (
|
||||
patch('src.models.load_config', return_value={
|
||||
@@ -146,8 +155,10 @@ def mock_app() -> Generator[App, None, None]:
|
||||
@pytest.fixture
|
||||
def app_instance() -> Generator[App, None, None]:
|
||||
"""
|
||||
Centralized App instance with all external side effects mocked.
|
||||
Matches the pattern used in test_token_viz.py and test_gui_phase4.py.
|
||||
|
||||
Centralized App instance with all external side effects mocked.
|
||||
Matches the pattern used in test_token_viz.py and test_gui_phase4.py.
|
||||
[C: tests/test_gui2_events.py:test_app_subscribes_to_events]
|
||||
"""
|
||||
with (
|
||||
patch('src.models.load_config', return_value={
|
||||
@@ -180,9 +191,10 @@ def app_instance() -> Generator[App, None, None]:
|
||||
@pytest.fixture(scope="session")
|
||||
def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
|
||||
"""
|
||||
Session-scoped fixture that starts sloppy.py with --enable-test-hooks.
|
||||
Includes high-signal environment telemetry and workspace isolation.
|
||||
"""
|
||||
|
||||
Session-scoped fixture that starts sloppy.py with --enable-test-hooks.
|
||||
Includes high-signal environment telemetry and workspace isolation.
|
||||
"""
|
||||
gui_script = os.path.abspath("sloppy.py")
|
||||
diag = VerificationLogger("live_gui_startup", "live_gui_diag")
|
||||
diag.log_state("GUI Script", "N/A", "gui_2.py")
|
||||
@@ -347,4 +359,4 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
|
||||
except PermissionError:
|
||||
time.sleep(0.5)
|
||||
except:
|
||||
break
|
||||
break
|
||||
@@ -9,7 +9,10 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
from src.api_hook_client import ApiHookClient
|
||||
|
||||
def wait_for_value(client, field, expected, timeout=5):
|
||||
"""Polls the GUI state until a field matches the expected value."""
|
||||
"""
|
||||
Polls the GUI state until a field matches the expected value.
|
||||
[C: tests/test_live_workflow.py:test_full_live_workflow]
|
||||
"""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
val = client.get_value(field)
|
||||
@@ -32,4 +35,4 @@ def test_status_hook(live_gui) -> None:
|
||||
# 3. Set mma_status to 'hook_mma_test'
|
||||
client.set_value('mma_status', 'hook_mma_test')
|
||||
# 4. Verify via get_value('mma_status') == 'hook_mma_test' (with retry)
|
||||
assert wait_for_value(client, 'mma_status', 'hook_mma_test'), f"Failed to set mma_status to hook_mma_test. Current value: {client.get_value('mma_status')}"
|
||||
assert wait_for_value(client, 'mma_status', 'hook_mma_test'), f"Failed to set mma_status to hook_mma_test. Current value: {client.get_value('mma_status')}"
|
||||
@@ -2,9 +2,10 @@ from src import ai_client
|
||||
|
||||
def test_list_models_gemini_cli() -> None:
|
||||
"""
|
||||
Verifies that 'ai_client.list_models' correctly returns a list of models
|
||||
for the 'gemini_cli' provider.
|
||||
"""
|
||||
|
||||
Verifies that 'ai_client.list_models' correctly returns a list of models
|
||||
for the 'gemini_cli' provider.
|
||||
"""
|
||||
models = ai_client.list_models("gemini_cli")
|
||||
assert "gemini-3.1-pro-preview" in models
|
||||
assert "gemini-3-flash-preview" in models
|
||||
@@ -12,4 +13,4 @@ def test_list_models_gemini_cli() -> None:
|
||||
assert "gemini-2.5-flash" in models
|
||||
assert "gemini-2.0-flash" in models
|
||||
assert "gemini-2.5-flash-lite" in models
|
||||
assert len(models) == 6
|
||||
assert len(models) == 6
|
||||
@@ -55,4 +55,4 @@ def test_set_params_via_custom_callback(live_gui) -> None:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
assert success, f"Params did not update via custom_callback. Got: {state}"
|
||||
assert success, f"Params did not update via custom_callback. Got: {state}"
|
||||
@@ -81,4 +81,4 @@ def test_get_node_status() -> None:
|
||||
}
|
||||
status = client.get_node_status("T1")
|
||||
assert status["status"] == "todo"
|
||||
mock_make.assert_any_call('GET', '/api/mma/node/T1')
|
||||
mock_make.assert_any_call('GET', '/api/mma/node/T1')
|
||||
@@ -36,7 +36,8 @@ def app_controller(tmp_session_dir):
|
||||
|
||||
def test_on_comms_entry_tool_result_offloading(app_controller, tmp_session_dir):
|
||||
"""
|
||||
Test that _on_comms_entry offloads tool_result output to a separate file.
|
||||
|
||||
Test that _on_comms_entry offloads tool_result output to a separate file.
|
||||
"""
|
||||
output_content = "This is a large tool output that should be offloaded."
|
||||
entry = {
|
||||
@@ -81,7 +82,8 @@ def test_on_comms_entry_tool_result_offloading(app_controller, tmp_session_dir):
|
||||
|
||||
def test_on_tool_log_offloading(app_controller, tmp_session_dir):
|
||||
"""
|
||||
Test that _on_tool_log calls session_logger.log_tool_call and log_tool_output.
|
||||
|
||||
Test that _on_tool_log calls session_logger.log_tool_call and log_tool_output.
|
||||
"""
|
||||
script = "Get-Process"
|
||||
result = "Process list..."
|
||||
@@ -107,4 +109,4 @@ def test_on_tool_log_offloading(app_controller, tmp_session_dir):
|
||||
assert len(app_controller._pending_tool_calls) == 1
|
||||
assert app_controller._pending_tool_calls[0]["script"] == script
|
||||
assert app_controller._pending_tool_calls[0]["result"] == result
|
||||
assert app_controller._pending_tool_calls[0]["source_tier"] == "Tier 3"
|
||||
assert app_controller._pending_tool_calls[0]["source_tier"] == "Tier 3"
|
||||
@@ -47,4 +47,4 @@ class TestArchBoundaryPhase1(unittest.TestCase):
|
||||
with open("scripts/claude_mma_exec.py", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
self.assertNotIn("C:\\Users\\Ed", content)
|
||||
self.assertNotIn("/Users/ed", content)
|
||||
self.assertNotIn("/Users/ed", content)
|
||||
@@ -97,4 +97,4 @@ class TestArchBoundaryPhase2(unittest.TestCase):
|
||||
self.assertTrue(ai_client._is_mutating_tool(t))
|
||||
|
||||
self.assertFalse(ai_client._is_mutating_tool("read_file"))
|
||||
self.assertFalse(ai_client._is_mutating_tool("list_directory"))
|
||||
self.assertFalse(ai_client._is_mutating_tool("list_directory"))
|
||||
@@ -88,4 +88,4 @@ class TestArchBoundaryPhase3(unittest.TestCase):
|
||||
engine = ExecutionEngine(dag)
|
||||
engine.tick()
|
||||
|
||||
self.assertEqual(t2.status, "blocked")
|
||||
self.assertEqual(t2.status, "blocked")
|
||||
@@ -324,4 +324,3 @@ public:
|
||||
assert 'int y = 2;' in updated
|
||||
assert 'int x = 1;' not in updated
|
||||
assert 'class MyClass {' in updated
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ from src import mcp_client
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool_calls_concurrently_timing():
|
||||
"""
|
||||
Verifies that _execute_tool_calls_concurrently runs tools in parallel.
|
||||
Total time should be approx 0.5s for 3 tools each taking 0.5s.
|
||||
|
||||
Verifies that _execute_tool_calls_concurrently runs tools in parallel.
|
||||
Total time should be approx 0.5s for 3 tools each taking 0.5s.
|
||||
"""
|
||||
# 1. Setup mock tool calls (Gemini style)
|
||||
class MockGeminiCall:
|
||||
@@ -65,8 +66,9 @@ async def test_execute_tool_calls_concurrently_timing():
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_tool_calls_concurrently_exception_handling():
|
||||
"""
|
||||
Verifies that if one tool call fails, it doesn't crash the whole group if caught,
|
||||
but currently gather is used WITHOUT return_exceptions=True, so it should re-raise.
|
||||
|
||||
Verifies that if one tool call fails, it doesn't crash the whole group if caught,
|
||||
but currently gather is used WITHOUT return_exceptions=True, so it should re-raise.
|
||||
"""
|
||||
class MockGeminiCall:
|
||||
def __init__(self, name, args):
|
||||
@@ -97,4 +99,4 @@ async def test_execute_tool_calls_concurrently_exception_handling():
|
||||
qa_callback=None,
|
||||
r_idx=0,
|
||||
provider="gemini"
|
||||
)
|
||||
)
|
||||
@@ -21,9 +21,10 @@ class TestCliToolBridgeMapping(unittest.TestCase):
|
||||
@patch('api_hook_client.ApiHookClient.request_confirmation')
|
||||
def test_mapping_from_api_format(self, mock_request: MagicMock, mock_stdout: MagicMock, mock_stdin: MagicMock) -> None:
|
||||
"""
|
||||
Verify that bridge correctly maps 'id', 'name', 'input' (Gemini API format)
|
||||
into tool_name and tool_input for the hook client.
|
||||
"""
|
||||
|
||||
Verify that bridge correctly maps 'id', 'name', 'input' (Gemini API format)
|
||||
into tool_name and tool_input for the hook client.
|
||||
"""
|
||||
api_tool_call = {
|
||||
'id': 'call123',
|
||||
'name': 'read_file',
|
||||
@@ -46,4 +47,4 @@ class TestCliToolBridgeMapping(unittest.TestCase):
|
||||
self.assertEqual(output.get('decision'), 'allow')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
@@ -6,7 +6,8 @@ import threading
|
||||
|
||||
def test_conductor_abort_event_populated():
|
||||
"""
|
||||
Test that ConductorEngine populates _abort_events when spawning a worker.
|
||||
|
||||
Test that ConductorEngine populates _abort_events when spawning a worker.
|
||||
"""
|
||||
# 1. Mock WorkerPool.spawn to return a mock thread
|
||||
with patch('src.multi_agent_conductor.WorkerPool.spawn') as mock_spawn:
|
||||
@@ -29,4 +30,4 @@ def test_conductor_abort_event_populated():
|
||||
|
||||
# 5. Assert that self._abort_events has an entry for the ticket ID
|
||||
assert ticket_id in engine._abort_events
|
||||
assert isinstance(engine._abort_events[ticket_id], threading.Event)
|
||||
assert isinstance(engine._abort_events[ticket_id], threading.Event)
|
||||
@@ -3,8 +3,9 @@ from src.api_hook_client import ApiHookClient
|
||||
|
||||
def simulate_conductor_phase_completion(client: ApiHookClient, track_id: str, phase_name: str) -> bool:
|
||||
"""
|
||||
Simulates the Conductor agent's logic for phase completion using ApiHookClient.
|
||||
"""
|
||||
|
||||
Simulates the Conductor agent's logic for phase completion using ApiHookClient.
|
||||
"""
|
||||
try:
|
||||
# 1. Poll for state
|
||||
state = client.get_gui_state()
|
||||
@@ -22,8 +23,10 @@ def simulate_conductor_phase_completion(client: ApiHookClient, track_id: str, ph
|
||||
return False
|
||||
|
||||
def test_conductor_integrates_api_hook_client_for_verification(live_gui) -> None:
|
||||
"""Verify that Conductor's simulated phase completion logic properly integrates
|
||||
with the ApiHookClient and the live Hook Server."""
|
||||
"""
|
||||
Verify that Conductor's simulated phase completion logic properly integrates
|
||||
with the ApiHookClient and the live Hook Server.
|
||||
"""
|
||||
client = ApiHookClient()
|
||||
assert client.wait_for_server(timeout=10)
|
||||
|
||||
@@ -44,4 +47,4 @@ def test_conductor_handles_api_hook_connection_error() -> None:
|
||||
"""Verify Conductor handles a simulated API hook connection error (server down)."""
|
||||
client = ApiHookClient(base_url="http://127.0.0.1:9999") # Invalid port
|
||||
result = simulate_conductor_phase_completion(client, "any", "any")
|
||||
assert result is False
|
||||
assert result is False
|
||||
@@ -7,7 +7,8 @@ from src.models import Track
|
||||
|
||||
def test_conductor_engine_initializes_empty_worker_and_abort_dicts() -> None:
|
||||
"""
|
||||
Test that ConductorEngine correctly initializes _active_workers and _abort_events as empty dictionaries.
|
||||
|
||||
Test that ConductorEngine correctly initializes _active_workers and _abort_events as empty dictionaries.
|
||||
"""
|
||||
# Mock the track object
|
||||
mock_track = MagicMock(spec=Track)
|
||||
@@ -22,8 +23,9 @@ def test_conductor_engine_initializes_empty_worker_and_abort_dicts() -> None:
|
||||
|
||||
def test_kill_worker_sets_abort_and_joins_thread() -> None:
|
||||
"""
|
||||
Test kill_worker: mock a running thread in _active_workers, call kill_worker,
|
||||
assert abort_event is set and thread is joined.
|
||||
|
||||
Test kill_worker: mock a running thread in _active_workers, call kill_worker,
|
||||
assert abort_event is set and thread is joined.
|
||||
"""
|
||||
mock_track = MagicMock(spec=Track)
|
||||
mock_track.tickets = []
|
||||
@@ -35,6 +37,9 @@ def test_kill_worker_sets_abort_and_joins_thread() -> None:
|
||||
|
||||
# Create a thread that waits for the abort event
|
||||
def worker():
|
||||
"""
|
||||
[C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols]
|
||||
"""
|
||||
abort_event.wait(timeout=2.0)
|
||||
|
||||
thread = threading.Thread(target=worker)
|
||||
@@ -50,4 +55,4 @@ def test_kill_worker_sets_abort_and_joins_thread() -> None:
|
||||
assert abort_event.is_set()
|
||||
assert not thread.is_alive()
|
||||
with engine._workers_lock:
|
||||
assert ticket_id not in engine._active_workers
|
||||
assert ticket_id not in engine._active_workers
|
||||
@@ -9,8 +9,9 @@ from src import ai_client
|
||||
|
||||
def test_conductor_engine_initialization() -> None:
|
||||
"""
|
||||
Test that ConductorEngine can be initialized with a Track.
|
||||
"""
|
||||
|
||||
Test that ConductorEngine can be initialized with a Track.
|
||||
"""
|
||||
track = Track(id="test_track", description="Test Track")
|
||||
from src.multi_agent_conductor import ConductorEngine
|
||||
engine = ConductorEngine(track=track, auto_queue=True)
|
||||
@@ -18,8 +19,9 @@ def test_conductor_engine_initialization() -> None:
|
||||
|
||||
def test_conductor_engine_run_executes_tickets_in_order(monkeypatch: pytest.MonkeyPatch, vlogger) -> None:
|
||||
"""
|
||||
Test that run iterates through executable tickets and calls the worker lifecycle.
|
||||
"""
|
||||
|
||||
Test that run iterates through executable tickets and calls the worker lifecycle.
|
||||
"""
|
||||
ticket1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
||||
ticket2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker2", depends_on=["T1"])
|
||||
track = Track(id="track1", description="Track 1", tickets=[ticket1, ticket2])
|
||||
@@ -64,8 +66,9 @@ def test_conductor_engine_run_executes_tickets_in_order(monkeypatch: pytest.Monk
|
||||
|
||||
def test_run_worker_lifecycle_calls_ai_client_send(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Test that run_worker_lifecycle triggers the AI client and updates ticket status on success.
|
||||
"""
|
||||
|
||||
Test that run_worker_lifecycle triggers the AI client and updates ticket status on success.
|
||||
"""
|
||||
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
||||
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
||||
from src.multi_agent_conductor import run_worker_lifecycle
|
||||
@@ -84,8 +87,9 @@ def test_run_worker_lifecycle_calls_ai_client_send(monkeypatch: pytest.MonkeyPat
|
||||
|
||||
def test_run_worker_lifecycle_context_injection(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Test that run_worker_lifecycle can take a context_files list and injects AST views into the prompt.
|
||||
"""
|
||||
|
||||
Test that run_worker_lifecycle can take a context_files list and injects AST views into the prompt.
|
||||
"""
|
||||
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
||||
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
||||
context_files = ["primary.py", "secondary.py"]
|
||||
@@ -129,8 +133,9 @@ def test_run_worker_lifecycle_context_injection(monkeypatch: pytest.MonkeyPatch)
|
||||
|
||||
def test_run_worker_lifecycle_handles_blocked_response(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Test that run_worker_lifecycle marks the ticket as blocked if the AI indicates it cannot proceed.
|
||||
"""
|
||||
|
||||
Test that run_worker_lifecycle marks the ticket as blocked if the AI indicates it cannot proceed.
|
||||
"""
|
||||
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
||||
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
||||
from src.multi_agent_conductor import run_worker_lifecycle
|
||||
@@ -145,10 +150,11 @@ def test_run_worker_lifecycle_handles_blocked_response(monkeypatch: pytest.Monke
|
||||
|
||||
def test_run_worker_lifecycle_step_mode_confirmation(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Test that run_worker_lifecycle passes confirm_execution to ai_client.send when step_mode is True.
|
||||
Verify that if confirm_execution is called (simulated by mocking ai_client.send to call its callback),
|
||||
the flow works as expected.
|
||||
"""
|
||||
|
||||
Test that run_worker_lifecycle passes confirm_execution to ai_client.send when step_mode is True.
|
||||
Verify that if confirm_execution is called (simulated by mocking ai_client.send to call its callback),
|
||||
the flow works as expected.
|
||||
"""
|
||||
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1", step_mode=True)
|
||||
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
||||
from src.multi_agent_conductor import run_worker_lifecycle
|
||||
@@ -181,9 +187,10 @@ def test_run_worker_lifecycle_step_mode_confirmation(monkeypatch: pytest.MonkeyP
|
||||
|
||||
def test_run_worker_lifecycle_step_mode_rejection(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Verify that if confirm_execution returns False, the logic (in ai_client, which we simulate here)
|
||||
would prevent execution. In run_worker_lifecycle, we just check if it's passed.
|
||||
"""
|
||||
|
||||
Verify that if confirm_execution returns False, the logic (in ai_client, which we simulate here)
|
||||
would prevent execution. In run_worker_lifecycle, we just check if it's passed.
|
||||
"""
|
||||
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1", step_mode=True)
|
||||
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
||||
from src.multi_agent_conductor import run_worker_lifecycle
|
||||
@@ -205,8 +212,9 @@ def test_run_worker_lifecycle_step_mode_rejection(monkeypatch: pytest.MonkeyPatc
|
||||
|
||||
def test_conductor_engine_dynamic_parsing_and_execution(monkeypatch: pytest.MonkeyPatch, vlogger) -> None:
|
||||
"""
|
||||
Test that parse_json_tickets correctly populates the track and run executes them in dependency order.
|
||||
"""
|
||||
|
||||
Test that parse_json_tickets correctly populates the track and run executes them in dependency order.
|
||||
"""
|
||||
import json
|
||||
from src.multi_agent_conductor import ConductorEngine
|
||||
track = Track(id="dynamic_track", description="Dynamic Track")
|
||||
@@ -272,9 +280,10 @@ def test_conductor_engine_dynamic_parsing_and_execution(monkeypatch: pytest.Monk
|
||||
|
||||
def test_run_worker_lifecycle_pushes_response_via_queue(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Test that run_worker_lifecycle pushes a 'response' event with the correct stream_id
|
||||
via _queue_put when event_queue is provided.
|
||||
"""
|
||||
|
||||
Test that run_worker_lifecycle pushes a 'response' event with the correct stream_id
|
||||
via _queue_put when event_queue is provided.
|
||||
"""
|
||||
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
||||
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
||||
mock_event_queue = MagicMock()
|
||||
@@ -297,9 +306,10 @@ def test_run_worker_lifecycle_pushes_response_via_queue(monkeypatch: pytest.Monk
|
||||
|
||||
def test_run_worker_lifecycle_token_usage_from_comms_log(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Test that run_worker_lifecycle reads token usage from the comms log and
|
||||
updates engine.tier_usage['Tier 3'] with real input/output token counts.
|
||||
"""
|
||||
|
||||
Test that run_worker_lifecycle reads token usage from the comms log and
|
||||
updates engine.tier_usage['Tier 3'] with real input/output token counts.
|
||||
"""
|
||||
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
||||
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
||||
fake_comms = [
|
||||
@@ -320,4 +330,4 @@ def test_run_worker_lifecycle_token_usage_from_comms_log(monkeypatch: pytest.Mon
|
||||
mock_spawn.return_value = (True, "prompt", "ctx")
|
||||
run_worker_lifecycle(ticket, context, event_queue=MagicMock(), engine=engine)
|
||||
assert engine.tier_usage["Tier 3"]["input"] == 120
|
||||
assert engine.tier_usage["Tier 3"]["output"] == 45
|
||||
assert engine.tier_usage["Tier 3"]["output"] == 45
|
||||
+20
-11
@@ -9,7 +9,8 @@ from src.dag_engine import TrackDAG
|
||||
|
||||
def test_get_ready_tasks_linear():
|
||||
"""
|
||||
Verifies ready tasks detection in a simple linear dependency chain.
|
||||
|
||||
Verifies ready tasks detection in a simple linear dependency chain.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="todo", assigned_to="worker1")
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
@@ -20,8 +21,9 @@ def test_get_ready_tasks_linear():
|
||||
|
||||
def test_get_ready_tasks_branching():
|
||||
"""
|
||||
Verifies ready tasks detection in a branching dependency graph where multiple tasks
|
||||
are unlocked simultaneously after a prerequisite is met.
|
||||
|
||||
Verifies ready tasks detection in a branching dependency graph where multiple tasks
|
||||
are unlocked simultaneously after a prerequisite is met.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="completed", assigned_to="worker1")
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
@@ -35,7 +37,8 @@ def test_get_ready_tasks_branching():
|
||||
|
||||
def test_has_cycle_no_cycle():
|
||||
"""
|
||||
Validates that an acyclic graph is correctly identified as not having cycles.
|
||||
|
||||
Validates that an acyclic graph is correctly identified as not having cycles.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="todo", assigned_to="worker1")
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
@@ -44,7 +47,8 @@ def test_has_cycle_no_cycle():
|
||||
|
||||
def test_has_cycle_direct_cycle():
|
||||
"""
|
||||
Validates that a direct cycle (A depends on B, B depends on A) is correctly detected.
|
||||
|
||||
Validates that a direct cycle (A depends on B, B depends on A) is correctly detected.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="todo", assigned_to="worker1", depends_on=["T2"])
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
@@ -53,7 +57,8 @@ def test_has_cycle_direct_cycle():
|
||||
|
||||
def test_has_cycle_indirect_cycle():
|
||||
"""
|
||||
Validates that an indirect cycle (A->B->C->A) is correctly detected.
|
||||
|
||||
Validates that an indirect cycle (A->B->C->A) is correctly detected.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="todo", assigned_to="worker1", depends_on=["T3"])
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
@@ -63,7 +68,8 @@ def test_has_cycle_indirect_cycle():
|
||||
|
||||
def test_has_cycle_complex_no_cycle():
|
||||
"""
|
||||
Validates cycle detection in a complex graph that merges branches but remains acyclic.
|
||||
|
||||
Validates cycle detection in a complex graph that merges branches but remains acyclic.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="todo", assigned_to="worker1")
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
@@ -74,7 +80,8 @@ def test_has_cycle_complex_no_cycle():
|
||||
|
||||
def test_get_ready_tasks_multiple_deps():
|
||||
"""
|
||||
Validates that a task is not marked ready until ALL of its dependencies are completed.
|
||||
|
||||
Validates that a task is not marked ready until ALL of its dependencies are completed.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="completed", assigned_to="worker1")
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1")
|
||||
@@ -87,7 +94,8 @@ def test_get_ready_tasks_multiple_deps():
|
||||
|
||||
def test_topological_sort():
|
||||
"""
|
||||
Verifies that tasks are correctly ordered by dependencies regardless of input order.
|
||||
|
||||
Verifies that tasks are correctly ordered by dependencies regardless of input order.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="todo", assigned_to="worker1")
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
@@ -98,10 +106,11 @@ def test_topological_sort():
|
||||
|
||||
def test_topological_sort_cycle():
|
||||
"""
|
||||
Verifies that topological sorting safely aborts and raises ValueError when a cycle is present.
|
||||
|
||||
Verifies that topological sorting safely aborts and raises ValueError when a cycle is present.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="desc", status="todo", assigned_to="worker1", depends_on=["T2"])
|
||||
t2 = Ticket(id="T2", description="desc", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
dag = TrackDAG([t1, t2])
|
||||
with pytest.raises(ValueError, match="Dependency cycle detected"):
|
||||
dag.topological_sort()
|
||||
dag.topological_sort()
|
||||
@@ -12,9 +12,10 @@ from src import project_manager
|
||||
|
||||
def test_credentials_error_mentions_deepseek(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
Verify that the error message shown when credentials.toml is missing
|
||||
includes deepseek instructions.
|
||||
"""
|
||||
|
||||
Verify that the error message shown when credentials.toml is missing
|
||||
includes deepseek instructions.
|
||||
"""
|
||||
# Monkeypatch SLOP_CREDENTIALS to a non-existent file
|
||||
monkeypatch.setenv("SLOP_CREDENTIALS", "non_existent_credentials_file.toml")
|
||||
with pytest.raises(FileNotFoundError) as excinfo:
|
||||
@@ -25,32 +26,36 @@ def test_credentials_error_mentions_deepseek(monkeypatch: pytest.MonkeyPatch) ->
|
||||
|
||||
def test_default_project_includes_reasoning_role() -> None:
|
||||
"""
|
||||
Verify that 'Reasoning' is included in the default discussion roles
|
||||
to support DeepSeek-R1 reasoning traces.
|
||||
"""
|
||||
|
||||
Verify that 'Reasoning' is included in the default discussion roles
|
||||
to support DeepSeek-R1 reasoning traces.
|
||||
"""
|
||||
proj = project_manager.default_project("test")
|
||||
roles = proj["discussion"]["roles"]
|
||||
assert "Reasoning" in roles
|
||||
|
||||
def test_gui_providers_list() -> None:
|
||||
"""
|
||||
Check if 'deepseek' is in the GUI's provider list.
|
||||
"""
|
||||
|
||||
Check if 'deepseek' is in the GUI's provider list.
|
||||
"""
|
||||
from src.models import PROVIDERS
|
||||
assert "deepseek" in PROVIDERS
|
||||
|
||||
def test_deepseek_model_listing() -> None:
|
||||
"""
|
||||
Verify that list_models for deepseek returns expected models.
|
||||
"""
|
||||
|
||||
Verify that list_models for deepseek returns expected models.
|
||||
"""
|
||||
models = ai_client.list_models("deepseek")
|
||||
assert "deepseek-chat" in models
|
||||
assert "deepseek-reasoner" in models
|
||||
|
||||
def test_gui_provider_list_via_hooks(live_gui: Any) -> None:
|
||||
"""
|
||||
Verify 'deepseek' is present in the GUI provider list using API hooks.
|
||||
"""
|
||||
|
||||
Verify 'deepseek' is present in the GUI provider list using API hooks.
|
||||
"""
|
||||
from api_hook_client import ApiHookClient
|
||||
import time
|
||||
client = ApiHookClient()
|
||||
@@ -58,4 +63,4 @@ def test_gui_provider_list_via_hooks(live_gui: Any) -> None:
|
||||
# Attempt to set provider to deepseek to verify it's an allowed value
|
||||
client.set_value('current_provider', 'deepseek')
|
||||
time.sleep(0.5)
|
||||
assert client.get_value('current_provider') == 'deepseek'
|
||||
assert client.get_value('current_provider') == 'deepseek'
|
||||
@@ -4,8 +4,9 @@ from src import ai_client
|
||||
|
||||
def test_deepseek_model_selection() -> None:
|
||||
"""
|
||||
Verifies that ai_client.set_provider('deepseek', 'deepseek-chat') correctly updates the internal state.
|
||||
"""
|
||||
|
||||
Verifies that ai_client.set_provider('deepseek', 'deepseek-chat') correctly updates the internal state.
|
||||
"""
|
||||
ai_client.set_provider("deepseek", "deepseek-chat")
|
||||
assert ai_client._provider == "deepseek"
|
||||
assert ai_client._model == "deepseek-chat"
|
||||
@@ -13,8 +14,9 @@ def test_deepseek_model_selection() -> None:
|
||||
@patch("requests.post")
|
||||
def test_deepseek_completion_logic(mock_post: MagicMock) -> None:
|
||||
"""
|
||||
Verifies that ai_client.send() correctly calls the DeepSeek API and returns content.
|
||||
"""
|
||||
|
||||
Verifies that ai_client.send() correctly calls the DeepSeek API and returns content.
|
||||
"""
|
||||
ai_client.set_provider("deepseek", "deepseek-chat")
|
||||
with patch("src.ai_client._load_credentials", return_value={"deepseek": {"api_key": "test-key"}}):
|
||||
mock_response = MagicMock()
|
||||
@@ -31,8 +33,9 @@ def test_deepseek_completion_logic(mock_post: MagicMock) -> None:
|
||||
@patch("requests.post")
|
||||
def test_deepseek_reasoning_logic(mock_post: MagicMock) -> None:
|
||||
"""
|
||||
Verifies that reasoning_content is captured and wrapped in <thinking> tags.
|
||||
"""
|
||||
|
||||
Verifies that reasoning_content is captured and wrapped in <thinking> tags.
|
||||
"""
|
||||
ai_client.set_provider("deepseek", "deepseek-reasoner")
|
||||
with patch("src.ai_client._load_credentials", return_value={"deepseek": {"api_key": "test-key"}}):
|
||||
mock_response = MagicMock()
|
||||
@@ -52,8 +55,9 @@ def test_deepseek_reasoning_logic(mock_post: MagicMock) -> None:
|
||||
@patch("requests.post")
|
||||
def test_deepseek_tool_calling(mock_post: MagicMock) -> None:
|
||||
"""
|
||||
Verifies that DeepSeek provider correctly identifies and executes tool calls.
|
||||
"""
|
||||
|
||||
Verifies that DeepSeek provider correctly identifies and executes tool calls.
|
||||
"""
|
||||
ai_client.set_provider("deepseek", "deepseek-chat")
|
||||
with patch("src.ai_client._load_credentials", return_value={"deepseek": {"api_key": "test-key"}}), \
|
||||
patch("src.mcp_client.async_dispatch", new_callable=unittest.mock.AsyncMock) as mock_dispatch:
|
||||
@@ -93,8 +97,9 @@ def test_deepseek_tool_calling(mock_post: MagicMock) -> None:
|
||||
@patch("requests.post")
|
||||
def test_deepseek_streaming(mock_post: MagicMock) -> None:
|
||||
"""
|
||||
Verifies that DeepSeek provider correctly aggregates streaming chunks.
|
||||
"""
|
||||
|
||||
Verifies that DeepSeek provider correctly aggregates streaming chunks.
|
||||
"""
|
||||
ai_client.set_provider("deepseek", "deepseek-chat")
|
||||
with patch("src.ai_client._load_credentials", return_value={"deepseek": {"api_key": "test-key"}}):
|
||||
mock_response = MagicMock()
|
||||
@@ -115,8 +120,9 @@ def test_deepseek_streaming(mock_post: MagicMock) -> None:
|
||||
@patch("requests.post")
|
||||
def test_deepseek_payload_verification(mock_post: MagicMock) -> None:
|
||||
"""
|
||||
Verifies that the correct JSON payload (tools, history, params) is sent to DeepSeek.
|
||||
"""
|
||||
|
||||
Verifies that the correct JSON payload (tools, history, params) is sent to DeepSeek.
|
||||
"""
|
||||
ai_client.set_provider("deepseek", "deepseek-chat")
|
||||
ai_client.reset_session()
|
||||
with patch("src.ai_client._load_credentials", return_value={"deepseek": {"api_key": "test-key"}}):
|
||||
@@ -142,8 +148,9 @@ def test_deepseek_payload_verification(mock_post: MagicMock) -> None:
|
||||
@patch("requests.post")
|
||||
def test_deepseek_reasoner_payload_verification(mock_post: MagicMock) -> None:
|
||||
"""
|
||||
Verifies that deepseek-reasoner payload excludes tools and temperature.
|
||||
"""
|
||||
|
||||
Verifies that deepseek-reasoner payload excludes tools and temperature.
|
||||
"""
|
||||
ai_client.set_provider("deepseek", "deepseek-reasoner")
|
||||
ai_client.reset_session()
|
||||
with patch("src.ai_client._load_credentials", return_value={"deepseek": {"api_key": "test-key"}}):
|
||||
@@ -162,4 +169,4 @@ def test_deepseek_reasoner_payload_verification(mock_post: MagicMock) -> None:
|
||||
assert payload["model"] == "deepseek-reasoner"
|
||||
assert "tools" not in payload
|
||||
assert "temperature" not in payload
|
||||
assert "max_tokens" not in payload
|
||||
assert "max_tokens" not in payload
|
||||
@@ -47,4 +47,4 @@ class TestDiscussionTakes(unittest.TestCase):
|
||||
self.assertEqual(self.project_dict["discussion"]["discussions"][new_id]["history"], ["User: Experimental"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
@@ -94,4 +94,4 @@ def test_switching_discussion_via_tabs(app_instance):
|
||||
app_instance._render_discussion_panel()
|
||||
|
||||
# If implemented with tabs, this should be called
|
||||
mock_switch.assert_called_with("main_take_1")
|
||||
mock_switch.assert_called_with("main_take_1")
|
||||
@@ -70,4 +70,3 @@ def test_execution_sim_live(live_gui: Any) -> None:
|
||||
sim.run()
|
||||
time.sleep(2)
|
||||
sim.teardown()
|
||||
|
||||
|
||||
@@ -41,4 +41,4 @@ def test_file_item_from_dict_defaults():
|
||||
assert item.path == "test.py"
|
||||
assert item.auto_aggregate is True
|
||||
assert item.force_full is False
|
||||
assert item.injected_at is None
|
||||
assert item.injected_at is None
|
||||
@@ -11,9 +11,10 @@ from src.ai_client import get_gemini_cache_stats, reset_session
|
||||
|
||||
def test_get_gemini_cache_stats_with_mock_client() -> None:
|
||||
"""
|
||||
Test that get_gemini_cache_stats correctly processes cache lists
|
||||
from a mocked client instance.
|
||||
"""
|
||||
|
||||
Test that get_gemini_cache_stats correctly processes cache lists
|
||||
from a mocked client instance.
|
||||
"""
|
||||
# Ensure a clean state before the test by resetting the session
|
||||
reset_session()
|
||||
# 1. Create a mock for the cache object that the client will return
|
||||
@@ -40,4 +41,4 @@ def test_get_gemini_cache_stats_with_mock_client() -> None:
|
||||
assert "cache_count" in stats
|
||||
assert "total_size_bytes" in stats
|
||||
assert stats["cache_count"] == 1
|
||||
assert stats["total_size_bytes"] == 1024
|
||||
assert stats["total_size_bytes"] == 1024
|
||||
@@ -11,9 +11,10 @@ def app_instance(monkeypatch: pytest.MonkeyPatch) -> type[App]:
|
||||
|
||||
def test_app_subscribes_to_events(app_instance: type[App]) -> None:
|
||||
"""
|
||||
This test checks that the App's __init__ method subscribes the necessary
|
||||
event handlers to the ai_client.events emitter.
|
||||
"""
|
||||
|
||||
This test checks that the App's __init__ method subscribes the necessary
|
||||
event handlers to the ai_client.events emitter.
|
||||
"""
|
||||
with patch.object(ai_client.events, 'on') as mock_on:
|
||||
app = app_instance()
|
||||
mock_on.assert_called()
|
||||
@@ -22,4 +23,4 @@ def test_app_subscribes_to_events(app_instance: type[App]) -> None:
|
||||
assert "request_start" in event_names
|
||||
assert "response_received" in event_names
|
||||
assert "tool_execution" in event_names
|
||||
# We don't check for __self__ anymore as they might be lambdas
|
||||
# We don't check for __self__ anymore as they might be lambdas
|
||||
@@ -2,9 +2,10 @@ from src.gui_2 import App
|
||||
|
||||
def test_gui2_hubs_exist_in_show_windows(app_instance: App) -> None:
|
||||
"""
|
||||
Verifies that the new consolidated Hub windows are defined in the App's show_windows.
|
||||
This ensures they will be available in the 'Windows' menu.
|
||||
"""
|
||||
|
||||
Verifies that the new consolidated Hub windows are defined in the App's show_windows.
|
||||
This ensures they will be available in the 'Windows' menu.
|
||||
"""
|
||||
expected_hubs = [
|
||||
"Project Settings",
|
||||
"AI Settings",
|
||||
@@ -18,13 +19,14 @@ def test_gui2_hubs_exist_in_show_windows(app_instance: App) -> None:
|
||||
|
||||
def test_gui2_old_windows_removed_from_show_windows(app_instance: App) -> None:
|
||||
"""
|
||||
Verifies that the old fragmented windows are removed from show_windows.
|
||||
Note: Message, Response, and Tool Calls are kept as they are now optional standalone windows.
|
||||
"""
|
||||
|
||||
Verifies that the old fragmented windows are removed from show_windows.
|
||||
Note: Message, Response, and Tool Calls are kept as they are now optional standalone windows.
|
||||
"""
|
||||
old_windows = [
|
||||
"Projects", "Files", "Screenshots",
|
||||
"Provider", "System Prompts",
|
||||
"Comms History"
|
||||
]
|
||||
for old_win in old_windows:
|
||||
assert old_win not in app_instance.show_windows, f"Old window '{old_win}' should have been removed from show_windows"
|
||||
assert old_win not in app_instance.show_windows, f"Old window '{old_win}' should have been removed from show_windows"
|
||||
@@ -5,10 +5,11 @@ from src import ai_client
|
||||
|
||||
def test_mcp_tool_call_is_dispatched(app_instance: App) -> None:
|
||||
"""
|
||||
This test verifies that when the AI returns a tool call for an MCP function,
|
||||
the ai_client correctly dispatches it to mcp_client.
|
||||
This will fail until mcp_client is properly integrated.
|
||||
"""
|
||||
|
||||
This test verifies that when the AI returns a tool call for an MCP function,
|
||||
the ai_client correctly dispatches it to mcp_client.
|
||||
This will fail until mcp_client is properly integrated.
|
||||
"""
|
||||
# 1. Define the mock tool call from the AI
|
||||
mock_fc = MagicMock()
|
||||
mock_fc.name = "read_file"
|
||||
@@ -50,4 +51,4 @@ def test_mcp_tool_call_is_dispatched(app_instance: App) -> None:
|
||||
discussion_history=""
|
||||
)
|
||||
# 6. Assert that the MCP dispatch function was called
|
||||
mock_dispatch.assert_called_once_with("read_file", {"file_path": "test.txt"})
|
||||
mock_dispatch.assert_called_once_with("read_file", {"file_path": "test.txt"})
|
||||
@@ -25,8 +25,9 @@ def cleanup_callback_file() -> None:
|
||||
|
||||
def test_gui2_set_value_hook_works(live_gui: Any) -> None:
|
||||
"""
|
||||
Tests that the 'set_value' GUI hook is correctly implemented.
|
||||
"""
|
||||
|
||||
Tests that the 'set_value' GUI hook is correctly implemented.
|
||||
"""
|
||||
client = ApiHookClient()
|
||||
assert client.wait_for_server(timeout=10)
|
||||
test_value = f"New value set by test: {uuid.uuid4()}"
|
||||
@@ -40,8 +41,9 @@ def test_gui2_set_value_hook_works(live_gui: Any) -> None:
|
||||
|
||||
def test_gui2_click_hook_works(live_gui: Any) -> None:
|
||||
"""
|
||||
Tests that the 'click' GUI hook for the 'Reset' button is implemented.
|
||||
"""
|
||||
|
||||
Tests that the 'click' GUI hook for the 'Reset' button is implemented.
|
||||
"""
|
||||
client = ApiHookClient()
|
||||
assert client.wait_for_server(timeout=10)
|
||||
# First, set some state that 'Reset' would clear.
|
||||
@@ -57,8 +59,9 @@ def test_gui2_click_hook_works(live_gui: Any) -> None:
|
||||
|
||||
def test_gui2_custom_callback_hook_works(live_gui: Any) -> None:
|
||||
"""
|
||||
Tests that the 'custom_callback' GUI hook is correctly implemented.
|
||||
"""
|
||||
|
||||
Tests that the 'custom_callback' GUI hook is correctly implemented.
|
||||
"""
|
||||
client = ApiHookClient()
|
||||
assert client.wait_for_server(timeout=10)
|
||||
test_data = f"Callback executed: {uuid.uuid4()}"
|
||||
@@ -76,4 +79,3 @@ def test_gui2_custom_callback_hook_works(live_gui: Any) -> None:
|
||||
with open(temp_workspace_file, "r") as f:
|
||||
content = f.read()
|
||||
assert content == test_data, "Callback executed, but file content is incorrect."
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ _shared_metrics = {}
|
||||
|
||||
def test_performance_benchmarking(live_gui: tuple) -> None:
|
||||
"""
|
||||
Collects performance metrics for the current GUI script over a 5-second window.
|
||||
Ensures the application does not lock up and can report its internal state.
|
||||
|
||||
Collects performance metrics for the current GUI script over a 5-second window.
|
||||
Ensures the application does not lock up and can report its internal state.
|
||||
"""
|
||||
process, gui_script = live_gui
|
||||
client = ApiHookClient()
|
||||
@@ -65,8 +66,9 @@ def test_performance_benchmarking(live_gui: tuple) -> None:
|
||||
|
||||
def test_performance_baseline_check() -> None:
|
||||
"""
|
||||
Verifies that we have successfully collected performance metrics for sloppy.py
|
||||
and that they meet the minimum 30 FPS baseline.
|
||||
|
||||
Verifies that we have successfully collected performance metrics for sloppy.py
|
||||
and that they meet the minimum 30 FPS baseline.
|
||||
"""
|
||||
# Key is full path, find it by basename
|
||||
gui_key = next((k for k in _shared_metrics if "sloppy.py" in k), None)
|
||||
@@ -77,4 +79,4 @@ def test_performance_baseline_check() -> None:
|
||||
# A 0 FPS indicates the render loop is completely frozen or the API hook is dead.
|
||||
assert gui2_m["avg_fps"] > 0, "No performance metrics collected - GUI may be frozen"
|
||||
assert gui2_m["avg_fps"] >= 30
|
||||
assert gui2_m["avg_ft"] <= 33.3
|
||||
assert gui2_m["avg_ft"] <= 33.3
|
||||
@@ -32,4 +32,4 @@ def test_gui_context_preset_save_load(live_gui) -> None:
|
||||
context = client.get_context_state()
|
||||
loaded_files = [f["path"] if isinstance(f, dict) else str(f) for f in context.get("files", [])]
|
||||
assert loaded_files == test_files
|
||||
assert context.get("screenshots", []) == test_screenshots
|
||||
assert context.get("screenshots", []) == test_screenshots
|
||||
@@ -14,9 +14,10 @@ def test_diagnostics_panel_initialization(app_instance: Any) -> None:
|
||||
|
||||
def test_diagnostics_history_updates(app_instance: Any) -> None:
|
||||
"""
|
||||
Verifies that the internal performance history is updated correctly.
|
||||
This logic is inside the render loop in gui_2.py, but we can test
|
||||
the data structure and initialization.
|
||||
|
||||
Verifies that the internal performance history is updated correctly.
|
||||
This logic is inside the render loop in gui_2.py, but we can test
|
||||
the data structure and initialization.
|
||||
"""
|
||||
assert "fps" in app_instance.perf_history
|
||||
assert len(app_instance.perf_history["fps"]) == 100
|
||||
assert len(app_instance.perf_history["fps"]) == 100
|
||||
@@ -12,6 +12,9 @@ class MockApp:
|
||||
self.ai_status = ""
|
||||
|
||||
def init_state(self):
|
||||
"""
|
||||
[C: tests/test_system_prompt_exposure.py:TestSystemPromptExposure.test_app_controller_init_state_loads_prompts]
|
||||
"""
|
||||
pass
|
||||
|
||||
from src.gui_2 import App
|
||||
@@ -38,4 +41,4 @@ def test_save_paths():
|
||||
mock_copy.assert_called_once()
|
||||
assert 'applied' in mock_app.ai_status
|
||||
mock_reset.assert_called_once()
|
||||
mock_init.assert_called_once()
|
||||
mock_init.assert_called_once()
|
||||
@@ -10,8 +10,9 @@ from api_hook_client import ApiHookClient
|
||||
|
||||
def test_idle_performance_requirements(live_gui) -> None:
|
||||
"""
|
||||
Requirement: GUI must maintain stable performance on idle.
|
||||
"""
|
||||
|
||||
Requirement: GUI must maintain stable performance on idle.
|
||||
"""
|
||||
# Warmup to ensure GUI is ready
|
||||
time.sleep(5.0)
|
||||
client = ApiHookClient()
|
||||
@@ -39,4 +40,4 @@ def test_idle_performance_requirements(live_gui) -> None:
|
||||
print("[Warning] Frame time is 0.0. This is expected in headless CI/CD environments.")
|
||||
print(f"[Test] Valid frame time samples: {valid_ft_count}/5")
|
||||
# In some CI environments without a real display, frame time might remain 0
|
||||
# but we've verified the hook is returning the dictionary.
|
||||
# but we've verified the hook is returning the dictionary.
|
||||
@@ -11,8 +11,9 @@ from src import paths
|
||||
|
||||
def test_track_proposal_editing(app_instance):
|
||||
"""
|
||||
Verifies the structural integrity of track proposal items.
|
||||
Ensures that track proposals can be edited and removed from the active list.
|
||||
|
||||
Verifies the structural integrity of track proposal items.
|
||||
Ensures that track proposals can be edited and removed from the active list.
|
||||
"""
|
||||
app_instance.proposed_tracks = [
|
||||
{"title": "Old Title", "goal": "Old Goal"},
|
||||
@@ -33,8 +34,9 @@ def test_track_proposal_editing(app_instance):
|
||||
|
||||
def test_conductor_setup_scan(app_instance, tmp_path, monkeypatch):
|
||||
"""
|
||||
Verifies that the conductor setup scan properly iterates through the conductor directory,
|
||||
counts files and lines, and identifies active tracks.
|
||||
|
||||
Verifies that the conductor setup scan properly iterates through the conductor directory,
|
||||
counts files and lines, and identifies active tracks.
|
||||
"""
|
||||
old_cwd = os.getcwd()
|
||||
os.chdir(tmp_path)
|
||||
@@ -60,8 +62,9 @@ def test_conductor_setup_scan(app_instance, tmp_path, monkeypatch):
|
||||
|
||||
def test_create_track(app_instance, tmp_path):
|
||||
"""
|
||||
Verifies that _cb_create_track properly creates the track folder
|
||||
and populates the necessary boilerplate files (spec.md, plan.md, metadata.json).
|
||||
|
||||
Verifies that _cb_create_track properly creates the track folder
|
||||
and populates the necessary boilerplate files (spec.md, plan.md, metadata.json).
|
||||
"""
|
||||
old_cwd = os.getcwd()
|
||||
os.chdir(tmp_path)
|
||||
@@ -89,4 +92,3 @@ def test_create_track(app_instance, tmp_path):
|
||||
assert data['id'] == track_dir.name
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import time
|
||||
|
||||
def test_gui_startup_smoke(live_gui):
|
||||
"""
|
||||
Smoke test to ensure the GUI starts and remains running.
|
||||
|
||||
Smoke test to ensure the GUI starts and remains running.
|
||||
"""
|
||||
proc, _ = live_gui
|
||||
|
||||
@@ -13,4 +14,4 @@ def test_gui_startup_smoke(live_gui):
|
||||
time.sleep(2)
|
||||
|
||||
# Verify it's still running after 2 seconds
|
||||
assert proc.poll() is None, "GUI process crashed within 2 seconds of startup"
|
||||
assert proc.poll() is None, "GUI process crashed within 2 seconds of startup"
|
||||
@@ -53,4 +53,4 @@ def test_render_synthesis_panel(app_instance):
|
||||
mock_imgui.input_text_multiline.assert_called_with("##synthesis_prompt", app_instance.ui_synthesis_prompt, ANY)
|
||||
|
||||
# 3. Assert imgui.button is called for 'Generate Synthesis'
|
||||
mock_imgui.button.assert_any_call("Generate Synthesis")
|
||||
mock_imgui.button.assert_any_call("Generate Synthesis")
|
||||
@@ -4,7 +4,8 @@ from src.api_hook_client import ApiHookClient
|
||||
|
||||
def test_text_viewer_state_update(live_gui) -> None:
|
||||
"""
|
||||
Verifies that we can set text viewer state and it is reflected in GUI state.
|
||||
|
||||
Verifies that we can set text viewer state and it is reflected in GUI state.
|
||||
"""
|
||||
client = ApiHookClient()
|
||||
label = "Test Viewer Label"
|
||||
@@ -21,4 +22,4 @@ def test_text_viewer_state_update(live_gui) -> None:
|
||||
assert state is not None
|
||||
assert state.get('show_text_viewer') == True
|
||||
assert state.get('text_viewer_title') == label
|
||||
assert state.get('text_viewer_type') == text_type
|
||||
assert state.get('text_viewer_type') == text_type
|
||||
+13
-10
@@ -16,9 +16,10 @@ from src.gui_2 import App
|
||||
|
||||
def test_telemetry_data_updates_correctly(app_instance: Any) -> None:
|
||||
"""
|
||||
Tests that the _refresh_api_metrics method correctly updates
|
||||
the internal state for display by querying the ai_client.
|
||||
Verifies the boundary between GUI state and API state.
|
||||
|
||||
Tests that the _refresh_api_metrics method correctly updates
|
||||
the internal state for display by querying the ai_client.
|
||||
Verifies the boundary between GUI state and API state.
|
||||
"""
|
||||
# 1. Set the provider to anthropic
|
||||
app_instance._current_provider = "anthropic"
|
||||
@@ -41,9 +42,10 @@ def test_telemetry_data_updates_correctly(app_instance: Any) -> None:
|
||||
|
||||
def test_performance_history_updates(app_instance: Any) -> None:
|
||||
"""
|
||||
Verify the data structure that feeds the sparkline.
|
||||
This ensures that the rolling buffer for performance telemetry maintains
|
||||
the correct size and default initialization to prevent GUI rendering crashes.
|
||||
|
||||
Verify the data structure that feeds the sparkline.
|
||||
This ensures that the rolling buffer for performance telemetry maintains
|
||||
the correct size and default initialization to prevent GUI rendering crashes.
|
||||
"""
|
||||
# ANTI-SIMPLIFICATION: Verifying exactly 100 elements ensures the sparkline won't overflow
|
||||
assert len(app_instance.perf_history["frame_time"]) == 100
|
||||
@@ -51,9 +53,10 @@ def test_performance_history_updates(app_instance: Any) -> None:
|
||||
|
||||
def test_gui_updates_on_event(app_instance: App) -> None:
|
||||
"""
|
||||
Verifies that when an API event is received (e.g. from ai_client),
|
||||
the _on_api_event handler correctly updates internal metrics and
|
||||
queues the update to be processed by the GUI event loop.
|
||||
|
||||
Verifies that when an API event is received (e.g. from ai_client),
|
||||
the _on_api_event handler correctly updates internal metrics and
|
||||
queues the update to be processed by the GUI event loop.
|
||||
"""
|
||||
mock_stats = {"percentage": 50.0, "current": 500, "limit": 1000}
|
||||
app_instance.last_md = "mock_md"
|
||||
@@ -75,4 +78,4 @@ def test_gui_updates_on_event(app_instance: App) -> None:
|
||||
app_instance._process_pending_gui_tasks()
|
||||
# ANTI-SIMPLIFICATION: This assertion proves that the event pipeline
|
||||
# successfully transmitted state from the background thread to the GUI state.
|
||||
assert app_instance._token_stats["percentage"] == 50.0
|
||||
assert app_instance._token_stats["percentage"] == 50.0
|
||||
@@ -5,10 +5,11 @@ from src.api_hook_client import ApiHookClient
|
||||
@pytest.mark.asyncio
|
||||
async def test_mma_track_lifecycle_simulation():
|
||||
"""
|
||||
This test simulates the sequence of API calls an external orchestrator
|
||||
would make to manage an MMA track lifecycle via the Hook API.
|
||||
It verifies that ApiHookClient correctly routes requests to the
|
||||
corresponding endpoints in src/api_hooks.py.
|
||||
|
||||
This test simulates the sequence of API calls an external orchestrator
|
||||
would make to manage an MMA track lifecycle via the Hook API.
|
||||
It verifies that ApiHookClient correctly routes requests to the
|
||||
corresponding endpoints in src/api_hooks.py.
|
||||
"""
|
||||
|
||||
client = ApiHookClient("http://localhost:8999")
|
||||
@@ -114,4 +115,4 @@ async def test_mma_track_lifecycle_simulation():
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(test_mma_track_lifecycle_simulation())
|
||||
asyncio.run(test_mma_track_lifecycle_simulation())
|
||||
@@ -9,11 +9,12 @@ from src import ai_client
|
||||
@pytest.mark.asyncio
|
||||
async def test_headless_verification_full_run(vlogger) -> None:
|
||||
"""
|
||||
1. Initialize a ConductorEngine with a Track containing multiple dependent Tickets.
|
||||
2. Simulate a full execution run using engine.run().
|
||||
3. Mock ai_client.send to simulate successful tool calls and final responses.
|
||||
4. Specifically verify that 'Context Amnesia' is maintained.
|
||||
"""
|
||||
|
||||
1. Initialize a ConductorEngine with a Track containing multiple dependent Tickets.
|
||||
2. Simulate a full execution run using engine.run().
|
||||
3. Mock ai_client.send to simulate successful tool calls and final responses.
|
||||
4. Specifically verify that 'Context Amnesia' is maintained.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
||||
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker1", depends_on=["T1"])
|
||||
track = Track(id="track_verify", description="Verification Track", tickets=[t1, t2])
|
||||
@@ -47,9 +48,10 @@ async def test_headless_verification_full_run(vlogger) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_headless_verification_error_and_qa_interceptor(vlogger) -> None:
|
||||
"""
|
||||
5. Simulate a shell error and verify that the Tier 4 QA interceptor is triggered
|
||||
and its summary is injected into the worker's history for the next retry.
|
||||
"""
|
||||
|
||||
5. Simulate a shell error and verify that the Tier 4 QA interceptor is triggered
|
||||
and its summary is injected into the worker's history for the next retry.
|
||||
"""
|
||||
t1 = Ticket(id="T1", description="Task with error", status="todo", assigned_to="worker1")
|
||||
track = Track(id="track_error", description="Error Track", tickets=[t1])
|
||||
from src.events import SyncEventQueue
|
||||
@@ -140,4 +142,4 @@ async def test_headless_verification_error_and_qa_interceptor(vlogger) -> None:
|
||||
if "QA ANALYSIS:" in part_str and "FIX: Check if path exists." in part_str:
|
||||
found_qa = True
|
||||
assert found_qa, "QA Analysis was not injected into the next round"
|
||||
vlogger.finalize("Tier 4 QA Injection", "PASS", "QA summary injected into next worker round.")
|
||||
vlogger.finalize("Tier 4 QA Injection", "PASS", "QA summary injected into next worker round.")
|
||||
@@ -130,4 +130,4 @@ def test_get_history_bleed_stats_basic() -> None:
|
||||
assert "current" in stats, "Stats dictionary should contain 'current' token usage"
|
||||
assert 'limit' in stats, "Stats dictionary should contain 'limit'"
|
||||
assert stats['limit'] == 500
|
||||
assert isinstance(stats['current'], int) and stats['current'] >= 0
|
||||
assert isinstance(stats['current'], int) and stats['current'] >= 0
|
||||
+1
-1
@@ -54,4 +54,4 @@ def test_live_hook_server_responses(live_gui) -> None:
|
||||
# 4. Performance
|
||||
# diagnostics are available via get_gui_diagnostics or get_gui_state
|
||||
perf = client.get_gui_diagnostics() if hasattr(client, 'get_gui_diagnostics') else client.get_gui_state()
|
||||
assert "fps" in perf or "thinking" in perf
|
||||
assert "fps" in perf or "thinking" in perf
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user