3 Commits

11 changed files with 263 additions and 81 deletions
+1 -1
View File
@@ -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.
- **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).
- **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/`.
+1 -1
View File
@@ -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. 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. 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/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.
+2 -2
View File
@@ -105,11 +105,11 @@ This file tracks all major tracks for the project. Each track has its own detail
### Path Configuration
1. [ ] **Track: Project-Specific Conductor Directory**
1. [x] **Track: Project-Specific Conductor Directory**
*Link: [./tracks/project_conductor_dir_20260308/](./tracks/project_conductor_dir_20260308/)*
*Goal: Make conductor directory per-project. Each project TOML can specify custom conductor dir for isolated track/state management.*
2. [ ] **Track: GUI Path Configuration in Context Hub**
2. [x] **Track: GUI Path Configuration in Context Hub**
*Link: [./tracks/gui_path_config_20260308/](./tracks/gui_path_config_20260308/)*
*Goal: Add path configuration UI to Context Hub. Allow users to view and edit configurable paths directly from the GUI.*
@@ -1,42 +1,42 @@
# Implementation Plan: External MCP Server Support
## Phase 1: Configuration & Data Modeling
## Phase 1: Configuration & Data Modeling [checkpoint: 4ba1bd9]
- [x] Task: Define the schema for external MCP server configuration. [1c863f0]
- [x] Update `src/models.py` to include `MCPServerConfig` and `MCPConfiguration` classes.
- [x] Implement logic to load `mcp_config.json` from global and project-specific paths.
- [x] Task: Integrate configuration loading into `AppController`. [c09e0f5]
- [x] Ensure the MCP config path is correctly resolved from `config.toml` and `manual_slop.toml`.
- [x] Task: Write unit tests for configuration loading and validation. [c09e0f5]
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Configuration & Data Modeling' (Protocol in workflow.md)
- [x] Task: Conductor - User Manual Verification 'Phase 1: Configuration & Data Modeling' [4ba1bd9]
## Phase 2: MCP Client Extension
- [ ] Task: Implement `ExternalMCPManager` in `src/mcp_client.py`.
- [ ] Add support for managing multiple MCP server sessions.
- [ ] Implement the `StdioMCPClient` for local subprocess communication.
- [ ] Implement the `RemoteMCPClient` for SSE/WebSocket communication.
- [ ] Task: Update Tool Discovery.
- [ ] Implement `list_external_tools()` to aggregate tools from all active external servers.
- [ ] Task: Update Tool Dispatch.
- [ ] Modify `mcp_client.dispatch()` and `mcp_client.async_dispatch()` to route tool calls to either native tools or the appropriate external server.
- [ ] Task: Write integration tests for stdio and remote MCP client communication (using mock servers).
- [ ] Task: Conductor - User Manual Verification 'Phase 2: MCP Client Extension' (Protocol in workflow.md)
## Phase 2: MCP Client Extension [checkpoint: 828fadf]
- [x] Task: Implement `ExternalMCPManager` in `src/mcp_client.py`. [828fadf]
- [x] Add support for managing multiple MCP server sessions.
- [x] Implement the `StdioMCPClient` for local subprocess communication.
- [x] Implement the `RemoteMCPClient` for SSE/WebSocket communication (stub).
- [x] Task: Update Tool Discovery. [828fadf]
- [x] Implement `list_external_tools()` to aggregate tools from all active external servers.
- [x] Task: Update Tool Dispatch. [828fadf]
- [x] Modify `mcp_client.dispatch()` and `mcp_client.async_dispatch()` to route tool calls to either native tools or the appropriate external server.
- [x] Task: Write integration tests for stdio and remote MCP client communication (using mock servers). [828fadf]
- [x] Task: Conductor - User Manual Verification 'Phase 2: MCP Client Extension' [828fadf]
## Phase 3: GUI Integration & Lifecycle
- [ ] Task: Update the **Operations** panel in `src/gui_2.py`.
- [ ] Create a new "External Tools" section.
- [ ] List discovered tools from active external servers.
- [ ] Add a "Refresh External MCPs" button to reload configuration and rediscover tools.
- [ ] Task: Implement Lifecycle Management.
- [ ] Add the "Auto-start on Project Load" logic to start servers when a project is initialized.
- [ ] Add status indicators (e.g., color-coded dots) for each external server in the GUI.
- [ ] Task: Write visual regression tests or simulation scripts to verify the updated Operations panel.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Lifecycle' (Protocol in workflow.md)
## Phase 3: GUI Integration & Lifecycle [checkpoint: 3b2588a]
- [x] Task: Update the **Operations** panel in `src/gui_2.py`. [3b2588a]
- [x] Create a new "External Tools" section.
- [x] List discovered tools from active external servers.
- [x] Add a "Refresh External MCPs" button to reload configuration and rediscover tools.
- [x] Task: Implement Lifecycle Management. [3b2588a]
- [x] Add the "Auto-start on Project Load" logic to start servers when a project is initialized.
- [x] Add status indicators (e.g., color-coded dots) for each external server in the GUI.
- [x] Task: Write visual regression tests or simulation scripts to verify the updated Operations panel. [3b2588a]
- [x] Task: Conductor - User Manual Verification 'Phase 3: GUI Integration & Lifecycle' [3b2588a]
## Phase 4: Agent Integration & HITL
- [ ] Task: Update AI tool declarations.
- [ ] Ensure `ai_client.py` includes external tools in the tool definitions sent to Gemini/Anthropic.
- [ ] Task: Verify HITL Approval Flow.
- [ ] Ensure that calling an external tool correctly triggers the `ConfirmDialog` modal.
- [ ] Verify that approved external tool results are correctly returned to the AI.
- [ ] Task: Perform a final end-to-end verification with a real external MCP server.
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Agent Integration & HITL' (Protocol in workflow.md)
## Phase 4: Agent Integration & HITL [checkpoint: f4c5a0b]
- [x] Task: Update AI tool declarations. [f4c5a0b]
- [x] Ensure `ai_client.py` includes external tools in the tool definitions sent to Gemini/Anthropic.
- [x] Task: Verify HITL Approval Flow. [f4c5a0b]
- [x] Ensure that calling an external tool correctly triggers the `ConfirmDialog` modal.
- [x] Verify that approved external tool results are correctly returned to the AI.
- [x] Task: Perform a final end-to-end verification with a real external MCP server. [f4c5a0b]
- [x] Task: Conductor - User Manual Verification 'Phase 4: Agent Integration & HITL' [f4c5a0b]
@@ -3,13 +3,13 @@
## Phase 1: Path Info Display
Focus: Show current path resolution in GUI
- [ ] Task 1.1: Add path info functions to paths.py
- [x] Task 1.1: Add path info functions to paths.py [d237d3b]
- WHERE: src/paths.py
- WHAT: Add functions to get path resolution source (default/env/config)
- HOW: Return tuple of (resolved_path, source)
- SAFETY: New functions, no modifications
- [ ] Task 1.2: Create path display helper
- [x] Task 1.2: Create path display helper [d237d3b]
- WHERE: src/paths.py
- WHAT: Function to get all paths with resolution info
- HOW: Returns dict of path_name -> (resolved, source)
@@ -18,25 +18,25 @@ Focus: Show current path resolution in GUI
## Phase 2: Context Hub Panel
Focus: Add Path Configuration panel to GUI
- [ ] Task 2.1: Add Paths tab to Context Hub
- [x] Task 2.1: Add Paths tab to Context Hub [d237d3b]
- WHERE: src/gui_2.py (Context Hub section)
- WHAT: New tab/section for path configuration
- HOW: Add ImGui tab item, follow existing panel patterns
- SAFETY: New panel, no modifications to existing
- [ ] Task 2.2: Display current paths
- [x] Task 2.2: Display current paths [d237d3b]
- WHERE: src/gui_2.py (new paths panel)
- WHAT: Show resolved paths and their sources
- HOW: Call paths.py functions, display in read-only text
- SAFETY: New code
- [ ] Task 2.3: Add path text inputs
- [x] Task 2.3: Add path text inputs [d237d3b]
- WHERE: src/gui_2.py (paths panel)
- WHAT: Editable text inputs for each path
- HOW: ImGui input_text for conductor_dir, logs_dir, scripts_dir
- SAFETY: New code
- [ ] Task 2.4: Add browse buttons
- [x] Task 2.4: Add browse buttons [d237d3b]
- WHERE: src/gui_2.py (paths panel)
- WHAT: File dialog buttons to browse for directories
- HOW: Use existing file dialog patterns in gui_2.py
@@ -45,19 +45,19 @@ Focus: Add Path Configuration panel to GUI
## Phase 3: Persistence
Focus: Save path changes to config.toml
- [ ] Task 3.1: Add config write function
- [x] Task 3.1: Add config write function [d237d3b]
- WHERE: src/gui_2.py or new utility
- WHAT: Write [paths] section to config.toml
- HOW: Read existing config, update paths section, write back
- SAFETY: Backup before write, handle errors
- [ ] Task 3.2: Add Apply button
- [x] Task 3.2: Add Apply button [d237d3b]
- WHERE: src/gui_2.py (paths panel)
- WHAT: Button to save changes
- HOW: Call config write function, show success/error message
- SAFETY: Confirmation dialog
- [ ] Task 3.3: Add Reset button
- [x] Task 3.3: Add Reset button [d237d3b]
- WHERE: src/gui_2.py (paths panel)
- WHAT: Reset paths to defaults
- HOW: Clear custom values, show confirmation
@@ -66,13 +66,13 @@ Focus: Save path changes to config.toml
## Phase 4: UX Polish
Focus: Improve user experience
- [ ] Task 4.1: Add restart warning
- [x] Task 4.1: Add restart warning [d237d3b]
- WHERE: src/gui_2.py (paths panel)
- WHAT: Show warning that changes require restart
- HOW: Text label after Apply
- SAFETY: New code
- [ ] Task 4.2: Add tooltips
- [x] Task 4.2: Add tooltips [d237d3b]
- WHERE: src/gui_2.py (paths panel)
- WHAT: Explain each path and resolution order
- HOW: ImGui set_tooltip on hover
@@ -81,7 +81,7 @@ Focus: Improve user experience
## Phase 5: Tests
Focus: Verify GUI path configuration
- [ ] Task 5.1: Test path display
- [x] Task 5.1: Test path display [d237d3b]
- WHERE: tests/test_gui_paths.py (new file)
- WHAT: Verify paths panel shows correct values
- HOW: Mock paths.py, verify display
@@ -3,13 +3,13 @@
## Phase 1: Extend paths.py
Focus: Add project-specific path resolution
- [ ] Task 1.1: Add project-aware conductor path functions
- [x] Task 1.1: Add project-aware conductor path functions [48e2ed8]
- WHERE: src/paths.py
- WHAT: Add optional project_path parameter to get_conductor_dir, get_tracks_dir, get_track_state_dir
- HOW: If project_path provided, resolve relative to project root; otherwise use global
- SAFETY: Maintain backward compatibility with no-arg calls
- [ ] Task 1.2: Add project conductor path resolution
- [x] Task 1.2: Add project conductor path resolution [48e2ed8]
- WHERE: src/paths.py
- WHAT: New function `_resolve_project_conductor_dir(project_path)` that reads from project TOML
- HOW: Load project TOML, check `[conductor].dir` key
@@ -18,18 +18,18 @@ Focus: Add project-specific path resolution
## Phase 2: Update project_manager.py
Focus: Use project-specific paths for track operations
- [ ] Task 2.1: Update save_track_state to use project conductor dir
- [x] Task 2.1: Update save_track_state to use project conductor dir [3999e9c]
- WHERE: src/project_manager.py (around line 240)
- WHAT: Pass project base_dir to paths.get_track_state_dir()
- HOW: Get base_dir from project_path, call paths with project_path param
- SAFETY: Maintain existing function signature compatibility
- [ ] Task 2.2: Update load_track_state to use project conductor dir
- [x] Task 2.2: Update load_track_state to use project conductor dir [3999e9c]
- WHERE: src/project_manager.py (around line 252)
- WHAT: Load track state from project-specific directory
- HOW: Same as above
- [ ] Task 2.3: Update get_all_tracks to use project conductor dir
- [x] Task 2.3: Update get_all_tracks to use project conductor dir [3999e9c]
- WHERE: src/project_manager.py (around line 297)
- WHAT: List tracks from project-specific directory
- HOW: Accept optional project_path param
@@ -37,7 +37,7 @@ Focus: Use project-specific paths for track operations
## Phase 3: Update app_controller.py
Focus: Pass project path to track operations
- [ ] Task 3.1: Update track creation to use project conductor dir
- [x] Task 3.1: Update track creation to use project conductor dir [3999e9c]
- WHERE: src/app_controller.py (around line 1907, 1937)
- WHAT: Pass active_project_path to track path functions
- HOW: Get active_project_path, pass to paths.get_tracks_dir()
@@ -46,13 +46,13 @@ Focus: Pass project path to track operations
## Phase 4: Tests
Focus: Verify project-specific behavior
- [ ] Task 4.1: Write test for project-specific conductor dir
- [x] Task 4.1: Write test for project-specific conductor dir [48e2ed8]
- WHERE: tests/test_project_paths.py (new file)
- WHAT: Create mock project with custom conductor dir, verify tracks saved there
- HOW: Mock project_manager, verify path resolution
- SAFETY: New test file
- [ ] Task 4.2: Test backward compatibility
- [x] Task 4.2: Test backward compatibility [3999e9c]
- WHERE: tests/test_project_paths.py
- WHAT: Verify global paths still work without project_path
- HOW: Call functions without project_path, verify defaults
+4
View File
@@ -851,6 +851,10 @@ class AppController:
self.ui_separate_tier3 = False
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)
ai_cfg = self.config.get("ai", {})
self._current_provider = ai_cfg.get("provider", "gemini")
+64
View File
@@ -6,6 +6,7 @@ import math
import json
import sys
import os
import shutil
import copy
from pathlib import Path
from tkinter import filedialog, Tk
@@ -451,7 +452,14 @@ class App:
exp, opened = imgui.begin("Context Hub", self.show_windows["Context Hub"])
self.show_windows["Context Hub"] = bool(opened)
if exp:
if imgui.begin_tab_bar('context_hub_tabs'):
if imgui.begin_tab_item('Projects')[0]:
self._render_projects_panel()
imgui.end_tab_item()
if imgui.begin_tab_item('Paths')[0]:
self._render_paths_panel()
imgui.end_tab_item()
imgui.end_tab_bar()
imgui.end()
if self.show_windows.get("Files & Media", False):
exp, opened = imgui.begin("Files & Media", self.show_windows["Files & Media"])
@@ -1462,6 +1470,62 @@ class App:
ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms)
ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls)
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel")
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
}
cfg_path = paths.get_config_path()
if cfg_path.exists():
shutil.copy(cfg_path, str(cfg_path) + ".bak")
models.save_config(self.config)
paths.reset_resolved()
self.ai_status = "paths saved - restart required"
def _render_paths_panel(self) -> None:
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel")
path_info = paths.get_full_path_info()
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'})
imgui.text(label)
if imgui.is_item_hovered(): imgui.set_tooltip(tooltip)
imgui.same_line()
imgui.text_disabled(f"(Source: {info['source']})")
val = getattr(self, attr)
changed, new_val = imgui.input_text(f"##{key}", val)
if imgui.is_item_hovered(): imgui.set_tooltip(tooltip)
if changed: setattr(self, attr, new_val)
imgui.same_line()
if imgui.button(f"Browse##{key}"):
r = hide_tk_root()
d = filedialog.askdirectory(title=f"Select {label}")
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.")
imgui.separator()
if imgui.button("Apply", imgui.ImVec2(120, 0)):
self._save_paths()
imgui.same_line()
if imgui.button("Reset", imgui.ImVec2(120, 0)):
self.init_state()
self.ai_status = "paths reset to defaults"
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel")
def _render_track_proposal_modal(self) -> None:
if self._show_track_proposal_modal:
imgui.open_popup("Track Proposal")
+50 -5
View File
@@ -45,7 +45,7 @@ See Also:
from pathlib import Path
import os
import tomllib
from typing import Optional
from typing import Optional, Any
_RESOLVED: dict[str, Path] = {}
@@ -105,21 +105,43 @@ def _get_project_conductor_dir_from_toml(project_root: Path) -> Optional[Path]:
if c_dir:
p = Path(c_dir)
if not p.is_absolute(): p = project_root / p
return p
return p.resolve()
except: pass
return None
def get_conductor_dir(project_path: Optional[str] = None) -> Path:
if project_path:
project_root = Path(project_path)
project_root = Path(project_path).resolve()
p = _get_project_conductor_dir_from_toml(project_root)
if p: return p
return project_root / 'conductor'
if "conductor_dir" not in _RESOLVED:
_RESOLVED["conductor_dir"] = _resolve_path("SLOP_CONDUCTOR_DIR", "conductor_dir", "conductor")
# 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"]
if project_path:
return (Path(project_path).resolve() / "conductor").resolve()
root_dir = Path(__file__).resolve().parent.parent
return (root_dir / "conductor").resolve()
def get_logs_dir() -> Path:
if "logs_dir" not in _RESOLVED:
_RESOLVED["logs_dir"] = _resolve_path("SLOP_LOGS_DIR", "logs_dir", "logs/sessions")
@@ -139,6 +161,29 @@ def get_track_state_dir(track_id: str, project_path: Optional[str] = None) -> Pa
def get_archive_dir(project_path: Optional[str] = None) -> Path:
return get_conductor_dir(project_path) / "archive"
def _resolve_path_info(env_var: str, config_key: str, default: str) -> dict[str, Any]:
if env_var in os.environ:
return {'path': Path(os.environ[env_var]).resolve(), 'source': f'env:{env_var}'}
try:
with open(get_config_path(), 'rb') as f:
cfg = tomllib.load(f)
if 'paths' in cfg and config_key in cfg['paths']:
p = Path(cfg['paths'][config_key])
if not p.is_absolute():
p = (Path(__file__).resolve().parent.parent / p).resolve()
return {'path': p, 'source': 'config.toml'}
except: pass
root_dir = Path(__file__).resolve().parent.parent
p = (root_dir / default).resolve()
return {'path': p, 'source': 'default'}
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')
}
def reset_resolved() -> None:
"""For testing only - clear cached resolutions."""
_RESOLVED.clear()
+36
View File
@@ -0,0 +1,36 @@
import pytest
from unittest.mock import MagicMock, patch
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 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()
+52 -19
View File
@@ -1,25 +1,17 @@
import os
import pytest
import tomllib
import json
import tomli_w
from pathlib import Path
from src import paths
from src import project_manager
def test_get_conductor_dir_default():
paths.reset_resolved()
# Should return default "conductor" relative to root
# 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_no_toml(tmp_path):
paths.reset_resolved()
project_root = tmp_path / "my_project"
project_root.mkdir()
# Should default to project_root / "conductor" if no manual_slop.toml
res = paths.get_conductor_dir(project_path=str(project_root))
assert res == project_root / "conductor"
def test_get_conductor_dir_project_specific_with_toml(tmp_path):
paths.reset_resolved()
project_root = tmp_path / "my_project"
@@ -38,18 +30,59 @@ def test_get_conductor_dir_project_specific_with_toml(tmp_path):
res = paths.get_conductor_dir(project_path=str(project_root))
assert res == project_root / "custom_tracks"
def test_get_tracks_dir_project_specific(tmp_path):
def test_get_all_tracks_project_specific(tmp_path):
paths.reset_resolved()
project_root = tmp_path / "my_project"
project_root.mkdir()
res = paths.get_tracks_dir(project_path=str(project_root))
assert res == project_root / "conductor" / "tracks"
# Custom conductor dir
custom_dir = project_root / "my_conductor"
custom_dir.mkdir()
tracks_dir = custom_dir / "tracks"
tracks_dir.mkdir()
def test_get_track_state_dir_project_specific(tmp_path):
# 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()
project_root = tmp_path / "my_project"
project_root.mkdir()
res = paths.get_track_state_dir("track-123", project_path=str(project_root))
assert res == project_root / "conductor" / "tracks" / "track-123"
# 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"]