feat(headless): Implement Phase 1 - Project Setup & Headless Scaffold
This commit is contained in:
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
## Phase 1: Project Setup & Headless Scaffold
|
## Phase 1: Project Setup & Headless Scaffold
|
||||||
- [x] Task: Update dependencies (02fc847)
|
- [x] Task: Update dependencies (02fc847)
|
||||||
- [ ] Add `fastapi` and `uvicorn` to `pyproject.toml` (and sync `requirements.txt` via `uv`).
|
- [x] Add `fastapi` and `uvicorn` to `pyproject.toml` (and sync `requirements.txt` via `uv`).
|
||||||
- [~] Task: Implement headless startup
|
- [x] Task: Implement headless startup
|
||||||
- [ ] Modify `gui_2.py` (or create `headless.py`) to parse a `--headless` CLI flag.
|
- [x] Modify `gui_2.py` (or create `headless.py`) to parse a `--headless` CLI flag.
|
||||||
- [ ] Update config parsing in `config.toml` to support headless configuration sections.
|
- [x] Update config parsing in `config.toml` to support headless configuration sections.
|
||||||
- [ ] Bypass Dear PyGui initialization if headless mode is active.
|
- [x] Bypass Dear PyGui initialization if headless mode is active.
|
||||||
- [ ] Task: Create foundational API application
|
- [~] Task: Create foundational API application
|
||||||
- [ ] Set up the core FastAPI application instance.
|
- [ ] Set up the core FastAPI application instance.
|
||||||
- [ ] Implement `/health` and `/status` endpoints for Docker lifecycle checks.
|
- [ ] Implement `/health` and `/status` endpoints for Docker lifecycle checks.
|
||||||
- [ ] Task: Conductor - User Manual Verification 'Project Setup & Headless Scaffold' (Protocol in workflow.md)
|
- [ ] Task: Conductor - User Manual Verification 'Project Setup & Headless Scaffold' (Protocol in workflow.md)
|
||||||
|
|||||||
@@ -32,3 +32,7 @@ active = "C:\\projects\\manual_slop\\tests\\temp_project.toml"
|
|||||||
"Operations Hub" = true
|
"Operations Hub" = true
|
||||||
Theme = true
|
Theme = true
|
||||||
Diagnostics = true
|
Diagnostics = true
|
||||||
|
|
||||||
|
[headless]
|
||||||
|
port = 8000
|
||||||
|
api_key = ""
|
||||||
|
|||||||
32
gui_2.py
32
gui_2.py
@@ -22,6 +22,7 @@ import api_hooks
|
|||||||
import mcp_client
|
import mcp_client
|
||||||
from performance_monitor import PerformanceMonitor
|
from performance_monitor import PerformanceMonitor
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
from imgui_bundle import imgui, hello_imgui, immapp
|
from imgui_bundle import imgui, hello_imgui, immapp
|
||||||
|
|
||||||
CONFIG_PATH = Path("config.toml")
|
CONFIG_PATH = Path("config.toml")
|
||||||
@@ -303,6 +304,25 @@ class App:
|
|||||||
self._discussion_names_cache = []
|
self._discussion_names_cache = []
|
||||||
self._discussion_names_dirty = True
|
self._discussion_names_dirty = True
|
||||||
|
|
||||||
|
def create_api(self) -> FastAPI:
|
||||||
|
api = FastAPI(title="Manual Slop Headless API")
|
||||||
|
|
||||||
|
@api.get("/health")
|
||||||
|
def health():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
@api.get("/status")
|
||||||
|
def status():
|
||||||
|
return {
|
||||||
|
"provider": self.current_provider,
|
||||||
|
"model": self.current_model,
|
||||||
|
"active_project": self.active_project_path,
|
||||||
|
"ai_status": self.ai_status,
|
||||||
|
"session_usage": self.session_usage
|
||||||
|
}
|
||||||
|
|
||||||
|
return api
|
||||||
|
|
||||||
# ---------------------------------------------------------------- project loading
|
# ---------------------------------------------------------------- project loading
|
||||||
|
|
||||||
def _cb_new_project_automated(self, user_data):
|
def _cb_new_project_automated(self, user_data):
|
||||||
@@ -2001,6 +2021,17 @@ class App:
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Initializes the ImGui runner and starts the main application loop."""
|
"""Initializes the ImGui runner and starts the main application loop."""
|
||||||
|
if "--headless" in sys.argv:
|
||||||
|
print("Headless mode active")
|
||||||
|
self._fetch_models(self.current_provider)
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
headless_cfg = self.config.get("headless", {})
|
||||||
|
port = headless_cfg.get("port", 8000)
|
||||||
|
|
||||||
|
api = self.create_api()
|
||||||
|
uvicorn.run(api, host="0.0.0.0", port=port)
|
||||||
|
else:
|
||||||
theme.load_from_config(self.config)
|
theme.load_from_config(self.config)
|
||||||
|
|
||||||
self.runner_params = hello_imgui.RunnerParams()
|
self.runner_params = hello_imgui.RunnerParams()
|
||||||
@@ -2028,6 +2059,7 @@ class App:
|
|||||||
|
|
||||||
# On exit
|
# On exit
|
||||||
self.hook_server.stop()
|
self.hook_server.stop()
|
||||||
|
|
||||||
self.perf_monitor.stop()
|
self.perf_monitor.stop()
|
||||||
ai_client.cleanup() # Destroy active API caches to stop billing
|
ai_client.cleanup() # Destroy active API caches to stop billing
|
||||||
self._flush_to_project()
|
self._flush_to_project()
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ active = "main"
|
|||||||
|
|
||||||
[discussions.main]
|
[discussions.main]
|
||||||
git_commit = ""
|
git_commit = ""
|
||||||
last_updated = "2026-02-25T11:19:43"
|
last_updated = "2026-02-25T13:02:29"
|
||||||
history = []
|
history = []
|
||||||
|
|||||||
@@ -9,7 +9,56 @@ auto_add = true
|
|||||||
|
|
||||||
[discussions.main]
|
[discussions.main]
|
||||||
git_commit = ""
|
git_commit = ""
|
||||||
last_updated = "2026-02-25T11:21:27"
|
last_updated = "2026-02-25T13:02:29"
|
||||||
history = [
|
history = [
|
||||||
"@2026-02-25T11:21:23\nSystem:\n[PERFORMANCE ALERT] CPU usage high: 82.8%. Please consider optimizing recent changes or reducing load.",
|
"@1772042443.1159382\nUser:\nStress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 2 Stress test entry 2 Stress test entry 2 Stress test entry 2 Stress test entry 2",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 3 Stress test entry 3 Stress test entry 3 Stress test entry 3 Stress test entry 3",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 4 Stress test entry 4 Stress test entry 4 Stress test entry 4 Stress test entry 4",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 5 Stress test entry 5 Stress test entry 5 Stress test entry 5 Stress test entry 5",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 6 Stress test entry 6 Stress test entry 6 Stress test entry 6 Stress test entry 6",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 7 Stress test entry 7 Stress test entry 7 Stress test entry 7 Stress test entry 7",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 8 Stress test entry 8 Stress test entry 8 Stress test entry 8 Stress test entry 8",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 9 Stress test entry 9 Stress test entry 9 Stress test entry 9 Stress test entry 9",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 10 Stress test entry 10 Stress test entry 10 Stress test entry 10 Stress test entry 10",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 11 Stress test entry 11 Stress test entry 11 Stress test entry 11 Stress test entry 11",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 12 Stress test entry 12 Stress test entry 12 Stress test entry 12 Stress test entry 12",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 13 Stress test entry 13 Stress test entry 13 Stress test entry 13 Stress test entry 13",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 14 Stress test entry 14 Stress test entry 14 Stress test entry 14 Stress test entry 14",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 15 Stress test entry 15 Stress test entry 15 Stress test entry 15 Stress test entry 15",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 16 Stress test entry 16 Stress test entry 16 Stress test entry 16 Stress test entry 16",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 17 Stress test entry 17 Stress test entry 17 Stress test entry 17 Stress test entry 17",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 18 Stress test entry 18 Stress test entry 18 Stress test entry 18 Stress test entry 18",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 19 Stress test entry 19 Stress test entry 19 Stress test entry 19 Stress test entry 19",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 20 Stress test entry 20 Stress test entry 20 Stress test entry 20 Stress test entry 20",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 21 Stress test entry 21 Stress test entry 21 Stress test entry 21 Stress test entry 21",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 22 Stress test entry 22 Stress test entry 22 Stress test entry 22 Stress test entry 22",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 23 Stress test entry 23 Stress test entry 23 Stress test entry 23 Stress test entry 23",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 24 Stress test entry 24 Stress test entry 24 Stress test entry 24 Stress test entry 24",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 25 Stress test entry 25 Stress test entry 25 Stress test entry 25 Stress test entry 25",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 26 Stress test entry 26 Stress test entry 26 Stress test entry 26 Stress test entry 26",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 27 Stress test entry 27 Stress test entry 27 Stress test entry 27 Stress test entry 27",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 28 Stress test entry 28 Stress test entry 28 Stress test entry 28 Stress test entry 28",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 29 Stress test entry 29 Stress test entry 29 Stress test entry 29 Stress test entry 29",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 30 Stress test entry 30 Stress test entry 30 Stress test entry 30 Stress test entry 30",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 31 Stress test entry 31 Stress test entry 31 Stress test entry 31 Stress test entry 31",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 32 Stress test entry 32 Stress test entry 32 Stress test entry 32 Stress test entry 32",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 33 Stress test entry 33 Stress test entry 33 Stress test entry 33 Stress test entry 33",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 34 Stress test entry 34 Stress test entry 34 Stress test entry 34 Stress test entry 34",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 35 Stress test entry 35 Stress test entry 35 Stress test entry 35 Stress test entry 35",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 36 Stress test entry 36 Stress test entry 36 Stress test entry 36 Stress test entry 36",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 37 Stress test entry 37 Stress test entry 37 Stress test entry 37 Stress test entry 37",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 38 Stress test entry 38 Stress test entry 38 Stress test entry 38 Stress test entry 38",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 39 Stress test entry 39 Stress test entry 39 Stress test entry 39 Stress test entry 39",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 40 Stress test entry 40 Stress test entry 40 Stress test entry 40 Stress test entry 40",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 41 Stress test entry 41 Stress test entry 41 Stress test entry 41 Stress test entry 41",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 42 Stress test entry 42 Stress test entry 42 Stress test entry 42 Stress test entry 42",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 43 Stress test entry 43 Stress test entry 43 Stress test entry 43 Stress test entry 43",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 44 Stress test entry 44 Stress test entry 44 Stress test entry 44 Stress test entry 44",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 45 Stress test entry 45 Stress test entry 45 Stress test entry 45 Stress test entry 45",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 46 Stress test entry 46 Stress test entry 46 Stress test entry 46 Stress test entry 46",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 47 Stress test entry 47 Stress test entry 47 Stress test entry 47 Stress test entry 47",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48",
|
||||||
|
"@1772042443.1159382\nUser:\nStress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49",
|
||||||
]
|
]
|
||||||
|
|||||||
35
tests/test_headless_api.py
Normal file
35
tests/test_headless_api.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import unittest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
import gui_2
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
class TestHeadlessAPI(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
# We need an App instance to initialize the API, but we want to avoid GUI stuff
|
||||||
|
with patch('gui_2.session_logger.open_session'), \
|
||||||
|
patch('gui_2.ai_client.set_provider'), \
|
||||||
|
patch('gui_2.session_logger.close_session'):
|
||||||
|
cls.app_instance = gui_2.App()
|
||||||
|
# We will implement create_api method in App
|
||||||
|
if hasattr(cls.app_instance, 'create_api'):
|
||||||
|
cls.api = cls.app_instance.create_api()
|
||||||
|
else:
|
||||||
|
cls.api = MagicMock()
|
||||||
|
cls.client = TestClient(cls.api)
|
||||||
|
|
||||||
|
def test_health_endpoint(self):
|
||||||
|
response = self.client.get("/health")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json(), {"status": "ok"})
|
||||||
|
|
||||||
|
def test_status_endpoint(self):
|
||||||
|
response = self.client.get("/status")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
self.assertIn("provider", data)
|
||||||
|
self.assertIn("model", data)
|
||||||
|
self.assertIn("active_project", data)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
48
tests/test_headless_startup.py
Normal file
48
tests/test_headless_startup.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
import gui_2
|
||||||
|
|
||||||
|
class TestHeadlessStartup(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch('gui_2.immapp.run')
|
||||||
|
@patch('gui_2.api_hooks.HookServer')
|
||||||
|
@patch('gui_2.save_config')
|
||||||
|
@patch('gui_2.ai_client.cleanup')
|
||||||
|
@patch('uvicorn.run') # Mock uvicorn.run to prevent hanging
|
||||||
|
def test_headless_flag_prevents_gui_run(self, mock_uvicorn_run, mock_cleanup, mock_save_config, mock_hook_server, mock_immapp_run):
|
||||||
|
# Setup mock argv with --headless
|
||||||
|
test_args = ["gui_2.py", "--headless"]
|
||||||
|
|
||||||
|
with patch.object(sys, 'argv', test_args):
|
||||||
|
with patch('gui_2.session_logger.close_session'), \
|
||||||
|
patch('gui_2.session_logger.open_session'):
|
||||||
|
app = gui_2.App()
|
||||||
|
|
||||||
|
# Mock _fetch_models to avoid network calls
|
||||||
|
app._fetch_models = MagicMock()
|
||||||
|
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
# Expectation: immapp.run should NOT be called in headless mode
|
||||||
|
mock_immapp_run.assert_not_called()
|
||||||
|
# Expectation: uvicorn.run SHOULD be called
|
||||||
|
mock_uvicorn_run.assert_called_once()
|
||||||
|
|
||||||
|
@patch('gui_2.immapp.run')
|
||||||
|
def test_normal_startup_calls_gui_run(self, mock_immapp_run):
|
||||||
|
test_args = ["gui_2.py"]
|
||||||
|
with patch.object(sys, 'argv', test_args):
|
||||||
|
# In normal mode, it should still call immapp.run
|
||||||
|
with patch('gui_2.api_hooks.HookServer'), \
|
||||||
|
patch('gui_2.save_config'), \
|
||||||
|
patch('gui_2.ai_client.cleanup'), \
|
||||||
|
patch('gui_2.session_logger.close_session'), \
|
||||||
|
patch('gui_2.session_logger.open_session'):
|
||||||
|
app = gui_2.App()
|
||||||
|
app._fetch_models = MagicMock()
|
||||||
|
app.run()
|
||||||
|
mock_immapp_run.assert_called_once()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user