diff --git a/conductor/product.md b/conductor/product.md index 3b73fff..a8908d6 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -33,7 +33,7 @@ For deep implementation details when planning or implementing tracks, consult `d - **Track Browser:** Real-time visualization of all implementation tracks with status indicators and progress bars. Includes a dedicated **Active Track Summary** featuring a color-coded progress bar, precise ticket status breakdown (Completed, In Progress, Blocked, Todo), and dynamic **ETA estimation** based on historical completion times. - **Visual Task DAG:** An interactive, node-based visualizer for the active track's task dependencies using `imgui-node-editor`. Features color-coded state tracking (Ready, Running, Blocked, Done), drag-and-drop dependency creation, and right-click deletion. - **Strategy Visualization:** Dedicated real-time output streams for Tier 1 (Strategic Planning) and Tier 2/3 (Execution) agents, allowing the user to follow the agent's reasoning chains alongside the task DAG. - - **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, allowing projects to define their own conductor path in `manual_slop.toml` (`[conductor].dir`) for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective. + - **Track-Scoped State Management:** Segregates discussion history and task progress into per-track state files. Supports **Project-Specific Conductor Directories**, defaulting to `./conductor` relative to each project's TOML file. Projects can define their own conductor path override in `manual_slop.toml` (`[conductor].dir`) via the Projects tab for isolated track management. This prevents global context pollution and ensures the Tech Lead session is isolated to the specific track's objective. **Native DAG Execution Engine:** Employs a Python-based Directed Acyclic Graph (DAG) engine to manage complex task dependencies. Supports automated topological sorting, robust cycle detection, and **transitive blocking propagation** (cascading `blocked` status to downstream dependents to prevent execution stalls). - **Programmable Execution State machine:** Governing the transition between "Auto-Queue" (autonomous worker spawning) and "Step Mode" (explicit manual approval for each task transition). @@ -60,7 +60,7 @@ For deep implementation details when planning or implementing tracks, consult `d - **Dedicated Diagnostics Hub:** Consolidates real-time telemetry (FPS, CPU, Frame Time) and transient system warnings into a standalone **Diagnostics panel**, providing deep visibility into application health without polluting the discussion history. - **Improved MMA Observability:** Enhances sub-agent logging by injecting precise ticket IDs and descriptive roles into communication metadata, enabling granular filtering and tracking of parallel worker activities within the Comms History. - **In-Depth Toolset Access:** MCP-like file exploration, URL fetching, search, and dynamic context aggregation embedded within a multi-viewport Dear PyGui/ImGui interface. -- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows. Features **GUI-Based Path Configuration** within the Context Hub, allowing users to view and edit system paths (conductor, logs, scripts) with real-time resolution source tracking (default, env, or config). +- **Integrated Workspace:** A consolidated Hub-based layout (Context, AI Settings, Discussion, Operations) designed for expert multi-monitor workflows. Features **GUI-Based Path Configuration** within the Context Hub, allowing users to view and edit system paths (conductor, logs, scripts) with real-time resolution source tracking (default, env, or config). Changes are applied immediately at runtime without requiring an application restart. - **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode. - **Structured Log Taxonomy:** Automated session-based log organization into configurable directories (defaulting to `logs/sessions/`). Includes a dedicated GUI panel for monitoring and manual whitelisting. Features an intelligent heuristic-based pruner that automatically cleans up insignificant logs older than 24 hours while preserving valuable sessions. - **Clean Project Root:** Enforces a "Cruft-Free Root" policy by organizing core implementation into a `src/` directory and redirecting all temporary test data, configurations, and AI-generated artifacts to `tests/artifacts/`. diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index 22a0db4..d31a55b 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -30,7 +30,7 @@ - **ai_style_formatter.py:** Custom Python formatter specifically designed to enforce 1-space indentation and ultra-compact whitespace to minimize token consumption. -- **src/paths.py:** Centralized module for path resolution. Supports project-specific conductor directory overrides via project TOML (`[conductor].dir`), enabling isolated track management per project. All paths are resolved to absolute objects. Provides **Path Resolution Metadata**, exposing the source of each resolved path (default, environment variable, or configuration file) for high-fidelity GUI display. Path configuration (logs, conductor, scripts) can also be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies. +- **src/paths.py:** Centralized module for path resolution. Supports project-specific conductor directory overrides via project TOML (`[conductor].dir`), enabling isolated track management per project. If not specified, conductor paths default to `./conductor` relative to each project's TOML file. All paths are resolved to absolute objects. Provides **Path Resolution Metadata**, exposing the source of each resolved path (default, environment variable, or configuration file) for high-fidelity GUI display. Supports **Runtime Re-Resolution** via `reset_resolved()`, allowing path changes to be applied immediately without an application restart. Path configuration (logs, scripts) can also be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies. - **src/presets.py:** Implements `PresetManager` for high-performance CRUD operations on system prompt presets stored in TOML format (`presets.toml`, `project_presets.toml`). Supports dynamic path resolution and scope-based inheritance. diff --git a/config.toml b/config.toml index ad6536f..7034d31 100644 --- a/config.toml +++ b/config.toml @@ -1,12 +1,12 @@ [ai] -provider = "gemini_cli" -model = "gemini-2.5-flash-lite" -temperature = 0.85 +provider = "minimax" +model = "MiniMax-M2.5" +temperature = 0.0 top_p = 1.0 -max_tokens = 1024 +max_tokens = 32000 history_trunc_limit = 900000 -active_preset = "" -system_prompt = "Overridden Prompt" +active_preset = "Default" +system_prompt = "" [projects] paths = [ @@ -17,7 +17,7 @@ paths = [ "C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveexecutionsim.toml", "C:\\projects\\manual_slop\\tests\\artifacts\\temp_project.toml", ] -active = "C:\\projects\\manual_slop\\tests\\artifacts\\live_gui_workspace\\manual_slop.toml" +active = "C:/projects/gencpp/gencpp_sloppy.toml" [gui] separate_message_panel = false @@ -38,7 +38,7 @@ separate_tier4 = false "AI Settings" = true "MMA Dashboard" = true "Task DAG" = true -"Usage Analytics" = true +"Usage Analytics" = false "Tier 1" = false "Tier 2" = false "Tier 3" = false @@ -54,7 +54,6 @@ Response = false "Tool Calls" = false Theme = true "Log Management" = true -Diagnostics = false [theme] palette = "Nord Dark" @@ -69,3 +68,8 @@ max_workers = 4 [headless] api_key = "test-secret-key" + +[paths] +conductor_dir = "C:\\projects\\gencpp\\.ai\\conductor" +logs_dir = "C:\\projects\\manual_slop\\logs" +scripts_dir = "C:\\projects\\manual_slop\\scripts" diff --git a/manualslop_layout.ini b/manualslop_layout.ini index 1301a83..de1856b 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -54,8 +54,8 @@ Size=1111,224 Collapsed=0 [Window][Tool Calls] -Pos=790,1483 -Size=876,654 +Pos=855,1482 +Size=1014,655 Collapsed=0 DockId=0x00000006,0 @@ -74,8 +74,8 @@ Collapsed=0 DockId=0xAFC85805,2 [Window][Theme] -Pos=0,1786 -Size=676,351 +Pos=0,1212 +Size=853,925 Collapsed=0 DockId=0x00000002,2 @@ -91,8 +91,8 @@ Collapsed=0 DockId=0x00000010,2 [Window][Context Hub] -Pos=0,1786 -Size=676,351 +Pos=0,1212 +Size=853,925 Collapsed=0 DockId=0x00000002,1 @@ -103,26 +103,26 @@ Collapsed=0 DockId=0x0000000D,0 [Window][Discussion Hub] -Pos=1668,22 -Size=915,2115 +Pos=1871,22 +Size=949,2115 Collapsed=0 DockId=0x00000013,0 [Window][Operations Hub] -Pos=678,22 -Size=988,2115 +Pos=855,22 +Size=1014,2115 Collapsed=0 DockId=0x00000005,0 [Window][Files & Media] -Pos=0,1786 -Size=676,351 +Pos=0,1212 +Size=853,925 Collapsed=0 DockId=0x00000002,0 [Window][AI Settings] Pos=0,22 -Size=676,1762 +Size=853,1188 Collapsed=0 DockId=0x00000001,0 @@ -132,14 +132,14 @@ Size=416,325 Collapsed=0 [Window][MMA Dashboard] -Pos=2585,22 -Size=1255,2115 +Pos=2822,22 +Size=1018,2115 Collapsed=0 DockId=0x00000010,0 [Window][Log Management] -Pos=2585,22 -Size=1255,2115 +Pos=2822,22 +Size=1018,2115 Collapsed=0 DockId=0x00000010,1 @@ -333,8 +333,8 @@ Size=967,499 Collapsed=0 [Window][Usage Analytics] -Pos=2641,1719 -Size=1199,418 +Pos=2822,1716 +Size=1018,421 Collapsed=0 DockId=0x0000000F,0 @@ -384,11 +384,11 @@ Column 3 Width=20 Column 4 Weight=1.0000 [Table][0x2A6000B6,4] -RefScale=24 -Column 0 Width=72 -Column 1 Width=106 +RefScale=14 +Column 0 Width=42 +Column 1 Width=61 Column 2 Weight=1.0000 -Column 3 Width=180 +Column 3 Width=105 [Table][0x8BCC69C7,6] RefScale=13 @@ -407,11 +407,11 @@ Column 2 Weight=1.0000 Column 3 Width=105 [Table][0x2C515046,4] -RefScale=24 -Column 0 Width=73 +RefScale=14 +Column 0 Width=42 Column 1 Weight=1.0000 -Column 2 Width=181 -Column 3 Width=72 +Column 2 Width=105 +Column 3 Width=42 [Table][0xD99F45C5,4] Column 0 Sort=0v @@ -467,18 +467,18 @@ DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,22 Size=3840,2115 Split=X - DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2583,1183 Split=X + DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2820,1183 Split=X DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2 - DockNode ID=0x00000007 Parent=0x0000000B SizeRef=676,858 Split=Y Selected=0x8CA2375C - DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,1759 CentralNode=1 Selected=0x7BD57D6A - DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,351 Selected=0x1DCB2623 - DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1905,858 Split=X Selected=0x418C7449 - DockNode ID=0x00000012 Parent=0x0000000E SizeRef=988,402 Split=Y Selected=0x418C7449 + DockNode ID=0x00000007 Parent=0x0000000B SizeRef=853,858 Split=Y Selected=0x8CA2375C + DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,1188 CentralNode=1 Selected=0x7BD57D6A + DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,925 Selected=0xF4139CA2 + DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1965,858 Split=X Selected=0x418C7449 + DockNode ID=0x00000012 Parent=0x0000000E SizeRef=1014,402 Split=Y Selected=0x418C7449 DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1455 Selected=0x418C7449 DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,654 Selected=0x1D56B311 - DockNode ID=0x00000013 Parent=0x0000000E SizeRef=915,402 Selected=0x6F2B5B04 + DockNode ID=0x00000013 Parent=0x0000000E SizeRef=949,402 Selected=0x6F2B5B04 DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 - DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1255,1183 Split=Y Selected=0x3AEC3498 + DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=1018,1183 Split=Y Selected=0x3AEC3498 DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x2C0206CE DockNode ID=0x00000011 Parent=0x00000004 SizeRef=1199,420 Split=X Selected=0xDEB547B6 DockNode ID=0x0000000C Parent=0x00000011 SizeRef=916,380 Selected=0x655BC6E9 diff --git a/mock_debug_prompt.txt b/mock_debug_prompt.txt index fb839e7..d521119 100644 --- a/mock_debug_prompt.txt +++ b/mock_debug_prompt.txt @@ -2416,3 +2416,49 @@ PROMPT: role: tool Here are the results: {"content": "done"} ------------------ +--- MOCK INVOKED --- +ARGS: ['tests/mock_gemini_cli.py'] +PROMPT: +PATH: Epic Initialization — please produce tracks +------------------ +--- MOCK INVOKED --- +ARGS: ['tests/mock_gemini_cli.py'] +PROMPT: +Please generate the implementation tickets for this track. +------------------ +--- MOCK INVOKED --- +ARGS: ['tests/mock_gemini_cli.py'] +PROMPT: +Please read test.txt +You are assigned to Ticket T1. +Task Description: do something +------------------ +--- MOCK INVOKED --- +ARGS: ['tests/mock_gemini_cli.py'] +PROMPT: +role: tool +Here are the results: {"content": "done"} +------------------ +--- MOCK INVOKED --- +ARGS: ['tests/mock_gemini_cli.py'] +PROMPT: +PATH: Epic Initialization — please produce tracks +------------------ +--- MOCK INVOKED --- +ARGS: ['tests/mock_gemini_cli.py'] +PROMPT: +Please generate the implementation tickets for this track. +------------------ +--- MOCK INVOKED --- +ARGS: ['tests/mock_gemini_cli.py'] +PROMPT: +Please read test.txt +You are assigned to Ticket T1. +Task Description: do something +------------------ +--- MOCK INVOKED --- +ARGS: ['tests/mock_gemini_cli.py'] +PROMPT: +role: tool +Here are the results: {"content": "done"} +------------------ diff --git a/project_history.toml b/project_history.toml index f89ced8..57a61d1 100644 --- a/project_history.toml +++ b/project_history.toml @@ -9,5 +9,5 @@ active = "main" [discussions.main] git_commit = "" -last_updated = "2026-03-11T23:45:09" +last_updated = "2026-03-12T18:41:55" history = [] diff --git a/src/app_controller.py b/src/app_controller.py index eb46881..0a9ebd8 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -852,7 +852,6 @@ class AppController: self.ui_separate_tier4 = False self.config = models.load_config() path_info = paths.get_full_path_info() - self.ui_conductor_dir = str(path_info['conductor_dir']['path']) self.ui_logs_dir = str(path_info['logs_dir']['path']) self.ui_scripts_dir = str(path_info['scripts_dir']['path']) theme.load_from_config(self.config) @@ -890,6 +889,7 @@ class AppController: self.ui_shots_base_dir = self.project.get("screenshots", {}).get("base_dir", ".") proj_meta = self.project.get("project", {}) self.ui_project_git_dir = proj_meta.get("git_dir", "") + self.ui_project_conductor_dir = self.project.get('conductor', {}).get('dir', 'conductor') self.ui_project_main_context = proj_meta.get("main_context", "") self.ui_project_system_prompt = proj_meta.get("system_prompt", "") self.ui_gemini_cli_path = self.project.get("gemini_cli", {}).get("binary_path", "gemini") @@ -962,7 +962,7 @@ class AppController: agent_tools_cfg = self.project.get("agent", {}).get("tools", {}) self.ui_agent_tools = {t: agent_tools_cfg.get(t, True) for t in models.AGENT_TOOL_NAMES} label = self.project.get("project", {}).get("name", "") - session_logger.open_session(label=label) + session_logger.reset_session(label=label) # Trigger auto-start of MCP servers self.event_queue.put('refresh_external_mcps', None) @@ -2295,6 +2295,7 @@ class AppController: proj["screenshots"]["paths"] = self.screenshots proj.setdefault("project", {}) proj["project"]["git_dir"] = self.ui_project_git_dir + proj.setdefault("conductor", {})["dir"] = self.ui_project_conductor_dir proj["project"]["system_prompt"] = self.ui_project_system_prompt proj["project"]["main_context"] = self.ui_project_main_context proj["project"]["active_preset"] = self.ui_project_preset_name diff --git a/src/gui_2.py b/src/gui_2.py index ee13dfa..1e4ea56 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -1419,6 +1419,15 @@ class App: r.destroy() if d: self.ui_output_dir = d imgui.separator() + imgui.text("Conductor Directory") + ch, self.ui_project_conductor_dir = imgui.input_text("##cond_dir", self.ui_project_conductor_dir) + imgui.same_line() + if imgui.button("Browse##cond"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Conductor Directory") + r.destroy() + if d: self.ui_project_conductor_dir = d + imgui.separator() imgui.text("Project Files") imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True) for i, pp in enumerate(self.project_paths): @@ -1473,7 +1482,6 @@ class App: def _save_paths(self): self.config["paths"] = { - "conductor_dir": self.ui_conductor_dir, "logs_dir": self.ui_logs_dir, "scripts_dir": self.ui_scripts_dir } @@ -1482,7 +1490,8 @@ class App: shutil.copy(cfg_path, str(cfg_path) + ".bak") models.save_config(self.config) paths.reset_resolved() - self.ai_status = "paths saved - restart required" + self.init_state() + self.ai_status = 'paths applied and session reset' def _render_paths_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel") @@ -1490,10 +1499,6 @@ class App: imgui.text_colored(C_IN, "System Path Configuration") imgui.separator() - - if self.ai_status == "paths saved - restart required": - imgui.text_colored(vec4(255, 50, 50), "Restart required for path changes to take effect.") - imgui.separator() def render_path_field(label: str, attr: str, key: str, tooltip: str): info = path_info.get(key, {'source': 'unknown'}) @@ -1513,7 +1518,6 @@ class App: r.destroy() if d: setattr(self, attr, d) - render_path_field("Conductor Directory", "ui_conductor_dir", "conductor_dir", "Base directory for implementation tracks and project state.") render_path_field("Logs Directory", "ui_logs_dir", "logs_dir", "Directory where session JSON-L logs and artifacts are stored.") render_path_field("Scripts Directory", "ui_scripts_dir", "scripts_dir", "Directory for AI-generated PowerShell scripts.") diff --git a/src/paths.py b/src/paths.py index 315adee..cc86c18 100644 --- a/src/paths.py +++ b/src/paths.py @@ -6,13 +6,11 @@ This module provides centralized path resolution for all configurable paths in t Environment Variables: SLOP_CONFIG: Path to config.toml - SLOP_CONDUCTOR_DIR: Path to conductor directory SLOP_LOGS_DIR: Path to logs directory SLOP_SCRIPTS_DIR: Path to generated scripts directory Configuration (config.toml): [paths] - conductor_dir = "conductor" logs_dir = "logs/sessions" scripts_dir = "scripts/generated" @@ -27,8 +25,8 @@ Path Functions: Resolution Order: 1. Check project-specific manual_slop.toml (for conductor paths) - 2. Check environment variable - 3. Check config.toml [paths] section + 2. Check environment variable (for logs/scripts) + 3. Check config.toml [paths] section (for logs/scripts) 4. Fall back to default Usage: @@ -110,37 +108,15 @@ def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]: return None def get_conductor_dir(project_path: Optional[str] = None) -> Path: - if project_path: - project_root = Path(project_path).resolve() - p = _get_project_conductor_dir_from_toml(project_root) - if p: return p + if not project_path: + # Fallback for legacy/tests, but we should avoid this + return Path('conductor').resolve() - if "conductor_dir" not in _RESOLVED: - # Check env and config - root_dir = Path(__file__).resolve().parent.parent - env_val = os.environ.get("SLOP_CONDUCTOR_DIR") - if env_val: - p = Path(env_val) - if not p.is_absolute(): p = root_dir / p - _RESOLVED["conductor_dir"] = p.resolve() - else: - try: - with open(get_config_path(), "rb") as f: - cfg = tomllib.load(f) - if "paths" in cfg and "conductor_dir" in cfg["paths"]: - p = Path(cfg["paths"]["conductor_dir"]) - if not p.is_absolute(): p = root_dir / p - _RESOLVED["conductor_dir"] = p.resolve() - except: pass - - if "conductor_dir" in _RESOLVED: - return _RESOLVED["conductor_dir"] + project_root = Path(project_path).resolve() + p = _get_project_conductor_dir_from_toml(project_root) + if p: return p - if project_path: - return (Path(project_path).resolve() / "conductor").resolve() - - root_dir = Path(__file__).resolve().parent.parent - return (root_dir / "conductor").resolve() + return (project_root / "conductor").resolve() def get_logs_dir() -> Path: if "logs_dir" not in _RESOLVED: @@ -179,7 +155,6 @@ def _resolve_path_info(env_var: str, config_key: str, default: str) -> dict[str, def get_full_path_info() -> dict[str, dict[str, Any]]: return { - 'conductor_dir': _resolve_path_info('SLOP_CONDUCTOR_DIR', 'conductor_dir', 'conductor'), 'logs_dir': _resolve_path_info('SLOP_LOGS_DIR', 'logs_dir', 'logs/sessions'), 'scripts_dir': _resolve_path_info('SLOP_SCRIPTS_DIR', 'scripts_dir', 'scripts/generated') } diff --git a/src/session_logger.py b/src/session_logger.py index b917c65..ed2d478 100644 --- a/src/session_logger.py +++ b/src/session_logger.py @@ -112,6 +112,11 @@ def close_session() -> None: except Exception as e: print(f"Warning: Could not update auto-whitelist on close: {e}") +def reset_session(label: Optional[str] = None) -> None: + """Closes the current session and opens a new one with the given label.""" + close_session() + open_session(label) + def log_api_hook(method: str, path: str, payload: str) -> None: """Log an API hook invocation.""" if _api_fh is None: diff --git a/tests/test_gui_paths.py b/tests/test_gui_paths.py index 8cce80e..3aafece 100644 --- a/tests/test_gui_paths.py +++ b/tests/test_gui_paths.py @@ -4,33 +4,38 @@ from src import paths # We mock App to avoid the heavy initialization logic class MockApp: - def __init__(self): - self.ui_conductor_dir = '/mock/conductor' - self.ui_logs_dir = '/mock/logs' - self.ui_scripts_dir = '/mock/scripts' - self.config = {"paths": {}} - self.ai_status = "" - - from src.gui_2 import App - _save_paths = App._save_paths + def __init__(self): + self.ui_conductor_dir = '/mock/conductor' + self.ui_logs_dir = '/mock/logs' + self.ui_scripts_dir = '/mock/scripts' + self.config = {"paths": {}} + self.ai_status = "" + + def init_state(self): + pass + + from src.gui_2 import App + _save_paths = App._save_paths def test_save_paths(): - mock_app = MockApp() - - with patch('src.models.save_config') as mock_save, \ - patch('shutil.copy') as mock_copy, \ - patch('src.paths.get_config_path') as mock_get_cfg, \ - patch('src.paths.reset_resolved') as mock_reset: - - mock_get_cfg.return_value = MagicMock() - mock_get_cfg.return_value.exists.return_value = True - - mock_app.ui_conductor_dir = '/new/conductor' - mock_app._save_paths() - - # Verify config update - assert mock_app.config['paths']['conductor_dir'] == '/new/conductor' - mock_save.assert_called_once() - mock_copy.assert_called_once() - assert 'restart required' in mock_app.ai_status - mock_reset.assert_called_once() + mock_app = MockApp() + + with patch('src.models.save_config') as mock_save, \ + patch('shutil.copy') as mock_copy, \ + patch('src.paths.get_config_path') as mock_get_cfg, \ + patch('src.paths.reset_resolved') as mock_reset, \ + patch.object(MockApp, 'init_state') as mock_init: + + mock_get_cfg.return_value = MagicMock() + mock_get_cfg.return_value.exists.return_value = True + + mock_app.ui_conductor_dir = '/new/conductor' + mock_app._save_paths() + + # Verify config update + assert 'conductor_dir' not in mock_app.config['paths'] + mock_save.assert_called_once() + mock_copy.assert_called_once() + assert 'applied' in mock_app.ai_status + mock_reset.assert_called_once() + mock_init.assert_called_once() diff --git a/tests/test_paths.py b/tests/test_paths.py index a1c2279..7c526a8 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -9,25 +9,15 @@ def reset_paths(): yield paths.reset_resolved() -def test_default_paths(): +def test_default_paths(tmp_path, monkeypatch): + monkeypatch.setenv("SLOP_CONFIG", str(tmp_path / "non_existent.toml")) root_dir = Path(paths.__file__).resolve().parent.parent - assert paths.get_conductor_dir() == root_dir / "conductor" assert paths.get_logs_dir() == root_dir / "logs/sessions" assert paths.get_scripts_dir() == root_dir / "scripts/generated" - # config path should now be an absolute path relative to src/paths.py - assert paths.get_config_path() == root_dir / "config.toml" - assert paths.get_tracks_dir() == root_dir / "conductor/tracks" - assert paths.get_archive_dir() == root_dir / "conductor/archive" + # config path should be what we set in env + assert paths.get_config_path() == tmp_path / "non_existent.toml" def test_env_var_overrides(tmp_path, monkeypatch): - root_dir = Path(paths.__file__).resolve().parent.parent - - # Relative env var (resolved against root_dir) - monkeypatch.setenv("SLOP_CONDUCTOR_DIR", "custom_conductor") - assert paths.get_conductor_dir() == (root_dir / "custom_conductor").resolve() - - paths.reset_resolved() - # Absolute env var abs_logs = (tmp_path / "abs_logs").resolve() monkeypatch.setenv("SLOP_LOGS_DIR", str(abs_logs)) @@ -38,14 +28,12 @@ def test_config_overrides(tmp_path, monkeypatch): config_file = tmp_path / "custom_config.toml" content = """ [paths] -conductor_dir = "cfg_conductor" logs_dir = "cfg_logs" scripts_dir = "cfg_scripts" """ config_file.write_text(content) monkeypatch.setenv("SLOP_CONFIG", str(config_file)) - assert paths.get_conductor_dir() == (root_dir / "cfg_conductor").resolve() assert paths.get_logs_dir() == root_dir / "cfg_logs" assert paths.get_scripts_dir() == root_dir / "cfg_scripts" @@ -54,11 +42,20 @@ def test_precedence(tmp_path, monkeypatch): config_file = tmp_path / "custom_config.toml" content = """ [paths] -conductor_dir = "cfg_conductor" +logs_dir = "cfg_logs" """ config_file.write_text(content) monkeypatch.setenv("SLOP_CONFIG", str(config_file)) - monkeypatch.setenv("SLOP_CONDUCTOR_DIR", "env_conductor") + monkeypatch.setenv("SLOP_LOGS_DIR", "env_logs") # Env var should take precedence over config - assert paths.get_conductor_dir() == (root_dir / "env_conductor").resolve() + assert paths.get_logs_dir() == (root_dir / "env_logs").resolve() + +def test_conductor_dir_project_relative(tmp_path): + # Should default to tmp_path/conductor + project_path = str(tmp_path) + base = (tmp_path / 'conductor').resolve() + assert paths.get_conductor_dir(project_path) == base + assert paths.get_tracks_dir(project_path) == base / "tracks" + assert paths.get_archive_dir(project_path) == base / "archive" + assert paths.get_track_state_dir("test_track", project_path) == base / "tracks" / "test_track" diff --git a/tests/test_project_paths.py b/tests/test_project_paths.py index b8de62e..0adc4fe 100644 --- a/tests/test_project_paths.py +++ b/tests/test_project_paths.py @@ -7,82 +7,53 @@ from src import paths from src import project_manager def test_get_conductor_dir_default(): - paths.reset_resolved() - # Should return absolute path to "conductor" in project root - expected = Path(__file__).resolve().parent.parent / "conductor" - assert paths.get_conductor_dir() == expected + paths.reset_resolved() + # Should return absolute path to "conductor" in project root + expected = Path(__file__).resolve().parent.parent / "conductor" + assert paths.get_conductor_dir() == expected def test_get_conductor_dir_project_specific_with_toml(tmp_path): - paths.reset_resolved() - project_root = tmp_path / "my_project" - project_root.mkdir() - - # Create manual_slop.toml with custom conductor dir - toml_path = project_root / "manual_slop.toml" - config = { - "conductor": { - "dir": "custom_tracks" - } - } - with open(toml_path, "wb") as f: - f.write(tomli_w.dumps(config).encode()) - - res = paths.get_conductor_dir(project_path=str(project_root)) - assert res == project_root / "custom_tracks" + paths.reset_resolved() + project_root = tmp_path / "my_project" + project_root.mkdir() + + # Create manual_slop.toml with custom conductor dir + toml_path = project_root / "manual_slop.toml" + config = { + "conductor": { + "dir": "custom_tracks" + } + } + with open(toml_path, "wb") as f: + f.write(tomli_w.dumps(config).encode()) + + res = paths.get_conductor_dir(project_path=str(project_root)) + assert res == project_root / "custom_tracks" def test_get_all_tracks_project_specific(tmp_path): - paths.reset_resolved() - project_root = tmp_path / "my_project" - project_root.mkdir() - - # Custom conductor dir - custom_dir = project_root / "my_conductor" - custom_dir.mkdir() - tracks_dir = custom_dir / "tracks" - tracks_dir.mkdir() - - # Create a dummy track - track_dir = tracks_dir / "test_track_20260312" - track_dir.mkdir() - with open(track_dir / "metadata.json", "w") as f: - json.dump({"id": "test_track", "title": "Test Track"}, f) - - # Setup manual_slop.toml - toml_path = project_root / "manual_slop.toml" - config = {"conductor": {"dir": "my_conductor"}} - with open(toml_path, "wb") as f: - f.write(tomli_w.dumps(config).encode()) - - # project_manager.get_all_tracks(base_dir) should now find it - tracks = project_manager.get_all_tracks(str(project_root)) - assert len(tracks) == 1 - assert tracks[0]["title"] == "Test Track" - -def test_get_all_tracks_global_fallback(tmp_path): - paths.reset_resolved() - - # Create a directory without manual_slop.toml - empty_dir = tmp_path / "empty_project" - empty_dir.mkdir() - - # Setup a fake global conductor - global_conductor = tmp_path / "global_conductor" - global_conductor.mkdir() - global_tracks = global_conductor / "tracks" - global_tracks.mkdir() - - track_dir = global_tracks / "global_track" - track_dir.mkdir() - with open(track_dir / "metadata.json", "w") as f: - json.dump({"id": "global_track", "title": "Global Track"}, f) - - # Override global conductor dir via env var - os.environ["SLOP_CONDUCTOR_DIR"] = str(global_conductor) - try: - paths.reset_resolved() - # Pass project_path pointing to a dir without TOML - tracks = project_manager.get_all_tracks(str(empty_dir)) - # paths.get_conductor_dir(str(empty_dir)) should fall back to global - assert any(t["id"] == "global_track" for t in tracks) - finally: - del os.environ["SLOP_CONDUCTOR_DIR"] + paths.reset_resolved() + project_root = tmp_path / "my_project" + project_root.mkdir() + + # Custom conductor dir + custom_dir = project_root / "my_conductor" + custom_dir.mkdir() + tracks_dir = custom_dir / "tracks" + tracks_dir.mkdir() + + # Create a dummy track + track_dir = tracks_dir / "test_track_20260312" + track_dir.mkdir() + with open(track_dir / "metadata.json", "w") as f: + json.dump({"id": "test_track", "title": "Test Track"}, f) + + # Setup manual_slop.toml + toml_path = project_root / "manual_slop.toml" + config = {"conductor": {"dir": "my_conductor"}} + with open(toml_path, "wb") as f: + f.write(tomli_w.dumps(config).encode()) + + # project_manager.get_all_tracks(base_dir) should now find it + tracks = project_manager.get_all_tracks(str(project_root)) + assert len(tracks) == 1 + assert tracks[0]["title"] == "Test Track" diff --git a/tests/test_session_logger_reset.py b/tests/test_session_logger_reset.py new file mode 100644 index 0000000..3143249 --- /dev/null +++ b/tests/test_session_logger_reset.py @@ -0,0 +1,51 @@ +import pytest +import tomllib +from pathlib import Path +from typing import Generator +from src import session_logger +import time + +@pytest.fixture +def temp_logs(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Generator[Path, None, None]: + # Ensure closed before starting + session_logger.close_session() + monkeypatch.setattr(session_logger, "_comms_fh", None) + + log_dir = tmp_path / "logs" + scripts_dir = tmp_path / "scripts" / "generated" + log_dir.mkdir(parents=True, exist_ok=True) + scripts_dir.mkdir(parents=True, exist_ok=True) + + from src import paths + monkeypatch.setattr(paths, "get_logs_dir", lambda: log_dir) + monkeypatch.setattr(paths, "get_scripts_dir", lambda: scripts_dir) + + yield log_dir + # Cleanup: Close handles if open + session_logger.close_session() + +def test_reset_session(temp_logs: Path) -> None: + # 1. Open first session + label1 = "label1" + session_logger.open_session(label=label1) + + subdirs = [d for d in temp_logs.iterdir() if d.is_dir()] + assert len(subdirs) == 1 + session1_dir = subdirs[0] + assert session1_dir.name.endswith(f"_{label1}") + assert session_logger._comms_fh is not None + + # 2. Reset to second session + time.sleep(1.1) + + label2 = "label2" + session_logger.reset_session(label=label2) + + subdirs = sorted([d for d in temp_logs.iterdir() if d.is_dir()], key=lambda x: x.name) + assert len(subdirs) == 2 + session2_dir = subdirs[1] + assert session2_dir.name.endswith(f"_{label2}") + assert session_logger._comms_fh is not None + + # Verify new handle points to new dir + assert str(session2_dir) in str(Path(session_logger._comms_fh.name).resolve())