checkpoint(Saved system prompt presets)

This commit is contained in:
2026-03-09 22:27:40 -04:00
parent d8a4ec121d
commit e2a403a187
11 changed files with 649 additions and 35 deletions

View File

@@ -200,14 +200,14 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
temp_workspace.mkdir(parents=True, exist_ok=True)
# Create minimal project files to avoid cluttering root
# NOTE: Do NOT create config.toml here - we use SLOP_CONFIG env var
# to point to the actual project root config.toml
(temp_workspace / "manual_slop.toml").write_text("[project]\nname = 'TestProject'\n", encoding="utf-8")
(temp_workspace / "conductor" / "tracks").mkdir(parents=True, exist_ok=True)
# Resolve absolute paths for shared resources
project_root = Path(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
config_file = project_root / "config.toml"
config_file = temp_workspace / "config.toml"
if not config_file.exists():
config_file = project_root / "config.toml"
cred_file = project_root / "credentials.toml"
mcp_file = project_root / "mcp_env.toml"
@@ -249,6 +249,7 @@ def live_gui() -> Generator[tuple[subprocess.Popen, str], None, None]:
env["SLOP_CREDENTIALS"] = str(cred_file.absolute())
if mcp_file.exists():
env["SLOP_MCP_ENV"] = str(mcp_file.absolute())
env["SLOP_GLOBAL_PRESETS"] = str((temp_workspace / "presets.toml").absolute())
process = subprocess.Popen(
["uv", "run", "python", "-u", gui_script, "--enable-test-hooks"],

View File

@@ -0,0 +1,134 @@
import pytest
from pathlib import Path
from src.presets import PresetManager
from src.models import Preset
def test_load_all_merged(tmp_path, monkeypatch):
"""Tests that load_all correctly merges global and project presets."""
global_file = tmp_path / "global_presets.toml"
project_root = tmp_path / "project"
project_root.mkdir()
project_file = project_root / "project_presets.toml"
# Setup global presets
global_file.write_text("""
[presets.global_only]
system_prompt = "global prompt"
temperature = 0.5
[presets.override_me]
system_prompt = "original prompt"
""", encoding="utf-8")
# Setup project presets
project_file.write_text("""
[presets.project_only]
system_prompt = "project prompt"
max_output_tokens = 100
[presets.override_me]
system_prompt = "overridden prompt"
""", encoding="utf-8")
monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file)
monkeypatch.setattr("src.presets.get_project_presets_path", lambda p: project_file)
pm = PresetManager(project_root=project_root)
presets = pm.load_all()
assert len(presets) == 3
assert presets["global_only"].system_prompt == "global prompt"
assert presets["global_only"].temperature == 0.5
assert presets["project_only"].system_prompt == "project prompt"
assert presets["project_only"].max_output_tokens == 100
assert presets["override_me"].system_prompt == "overridden prompt"
def test_save_preset_global(tmp_path, monkeypatch):
"""Tests saving a preset to the global scope."""
global_file = tmp_path / "global_presets.toml"
monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file)
pm = PresetManager()
preset = Preset(name="new_global", system_prompt="new global prompt", temperature=0.7)
pm.save_preset(preset, scope="global")
assert global_file.exists()
loaded_presets = pm.load_all()
assert "new_global" in loaded_presets
assert loaded_presets["new_global"].system_prompt == "new global prompt"
assert loaded_presets["new_global"].temperature == 0.7
def test_save_preset_project(tmp_path, monkeypatch):
"""Tests saving a preset to the project scope."""
project_root = tmp_path / "project"
project_root.mkdir()
project_file = project_root / "project_presets.toml"
global_file = tmp_path / "global_presets.toml"
monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file)
monkeypatch.setattr("src.presets.get_project_presets_path", lambda p: project_file)
pm = PresetManager(project_root=project_root)
preset = Preset(name="new_project", system_prompt="new project prompt", max_output_tokens=500)
pm.save_preset(preset, scope="project")
assert project_file.exists()
# Global file should NOT have been created/modified
assert not global_file.exists()
loaded_presets = pm.load_all()
assert "new_project" in loaded_presets
assert loaded_presets["new_project"].system_prompt == "new project prompt"
assert loaded_presets["new_project"].max_output_tokens == 500
def test_save_preset_project_no_root():
"""Tests that saving to project scope fails if no project root is provided."""
pm = PresetManager(project_root=None)
preset = Preset(name="fail", system_prompt="fail")
with pytest.raises(ValueError, match="Project scope requested but no project_root provided"):
pm.save_preset(preset, scope="project")
def test_delete_preset(tmp_path, monkeypatch):
"""Tests deleting a preset from both scopes."""
global_file = tmp_path / "global_presets.toml"
project_root = tmp_path / "project"
project_root.mkdir()
project_file = project_root / "project_presets.toml"
global_file.write_text("""
[presets.global1]
system_prompt = "g1"
[presets.both]
system_prompt = "both_g"
""", encoding="utf-8")
project_file.write_text("""
[presets.project1]
system_prompt = "p1"
[presets.both]
system_prompt = "both_p"
""", encoding="utf-8")
monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file)
monkeypatch.setattr("src.presets.get_project_presets_path", lambda p: project_file)
pm = PresetManager(project_root=project_root)
# Delete from project
pm.delete_preset("both", scope="project")
presets = pm.load_all()
assert "project1" in presets
# "both" should now show the global version because project override is gone
assert presets["both"].system_prompt == "both_g"
# Delete from global
pm.delete_preset("global1", scope="global")
presets = pm.load_all()
assert "global1" not in presets
assert "both" in presets
# Delete last project preset
pm.delete_preset("project1", scope="project")
presets = pm.load_all()
assert "project1" not in presets
assert "both" in presets # still in global

77
tests/test_presets.py Normal file
View File

@@ -0,0 +1,77 @@
import os
import unittest
from pathlib import Path
import tempfile
import shutil
from src.presets import PresetManager
from src.models import Preset
class TestPresetManager(unittest.TestCase):
def setUp(self):
self.test_dir = Path(tempfile.mkdtemp())
self.project_root = self.test_dir / "project"
self.project_root.mkdir()
# Mocking global path is harder since it's hardcoded in paths.py
# But we can at least test project-specific ones and the manager's logic.
# For the sake of this test, we will only test what we can without
# affecting the real global_presets.toml if possible.
self.manager = PresetManager(project_root=self.project_root)
# Override paths for testing to avoid touching real files
self.manager.global_path = self.test_dir / "global_presets.toml"
def tearDown(self):
shutil.rmtree(self.test_dir)
def test_save_and_load_global(self):
preset = Preset(name="test_global", system_prompt="You are a global assistant")
self.manager.save_preset(preset, scope="global")
presets = self.manager.load_all()
self.assertIn("test_global", presets)
self.assertEqual(presets["test_global"].system_prompt, "You are a global assistant")
def test_save_and_load_project(self):
preset = Preset(name="test_project", system_prompt="You are a project assistant")
self.manager.save_preset(preset, scope="project")
presets = self.manager.load_all()
self.assertIn("test_project", presets)
self.assertEqual(presets["test_project"].system_prompt, "You are a project assistant")
def test_project_overwrites_global(self):
g_preset = Preset(name="shared", system_prompt="Global version")
p_preset = Preset(name="shared", system_prompt="Project version")
self.manager.save_preset(g_preset, scope="global")
self.manager.save_preset(p_preset, scope="project")
presets = self.manager.load_all()
self.assertEqual(presets["shared"].system_prompt, "Project version")
def test_delete_preset(self):
preset = Preset(name="to_delete", system_prompt="Delete me")
self.manager.save_preset(preset, scope="project")
presets = self.manager.load_all()
self.assertIn("to_delete", presets)
self.manager.delete_preset("to_delete", scope="project")
presets = self.manager.load_all()
self.assertNotIn("to_delete", presets)
def test_dynamic_project_path(self):
"""Verifies that project_path updates when project_root changes."""
initial_root = self.test_dir / "project1"
initial_root.mkdir()
manager = PresetManager(project_root=initial_root)
self.assertEqual(manager.project_path, initial_root / "project_presets.toml")
new_root = self.test_dir / "project2"
new_root.mkdir()
manager.project_root = new_root
self.assertEqual(manager.project_path, new_root / "project_presets.toml")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,103 @@
import pytest
import time
import tomli_w
import os
import json
from pathlib import Path
from src.api_hook_client import ApiHookClient
def test_preset_switching(live_gui):
client = ApiHookClient()
# Paths for presets
temp_workspace = Path("tests/artifacts/live_gui_workspace")
global_presets_path = temp_workspace / "presets.toml"
project_presets_path = temp_workspace / "project_presets.toml"
manual_slop_path = temp_workspace / "manual_slop.toml"
# Cleanup before test
if global_presets_path.exists(): global_presets_path.unlink()
if project_presets_path.exists(): project_presets_path.unlink()
try:
# Create a global preset
global_presets_path.write_text(tomli_w.dumps({
"presets": {
"TestGlobal": {
"system_prompt": "Global Prompt",
"temperature": 0.7
}
}
}))
# Create a project preset
project_presets_path.write_text(tomli_w.dumps({
"presets": {
"TestProject": {
"system_prompt": "Project Prompt",
"temperature": 0.3
},
"TestGlobal": { # Override
"system_prompt": "Overridden Prompt",
"temperature": 0.5
}
}
}))
# Switch to the local project to ensure context is correct
client.push_event("custom_callback", {
"callback": "_switch_project",
"args": [str(manual_slop_path.absolute())]
})
time.sleep(2)
# Trigger reload of presets (just in case)
client.push_event("custom_callback", {
"callback": "_refresh_from_project",
"args": []
})
time.sleep(2) # Wait for processing
# Verify where it thinks the project is
state = client.get_gui_state()
print(f"DEBUG: ui_files_base_dir={state.get('files_base_dir')}")
# Apply Global Preset (should use override from project if available in merged list)
client.push_event("custom_callback", {
"callback": "_apply_preset",
"args": ["TestGlobal", "global"]
})
time.sleep(1)
# Verify state
state = client.get_gui_state()
assert state["global_preset_name"] == "TestGlobal"
assert state["global_system_prompt"] == "Overridden Prompt"
assert state["temperature"] == 0.5
# Apply Project Preset
client.push_event("custom_callback", {
"callback": "_apply_preset",
"args": ["TestProject", "project"]
})
time.sleep(1)
state = client.get_gui_state()
assert state["project_preset_name"] == "TestProject"
assert state["project_system_prompt"] == "Project Prompt"
assert state["temperature"] == 0.3
# Select "None"
client.push_event("custom_callback", {
"callback": "_apply_preset",
"args": ["None", "global"]
})
time.sleep(1)
state = client.get_gui_state()
assert not state.get("global_preset_name") # Should be None or ""
# Prompt remains from previous application
assert state["global_system_prompt"] == "Overridden Prompt"
finally:
# Cleanup
if global_presets_path.exists(): global_presets_path.unlink()
if project_presets_path.exists(): project_presets_path.unlink()