Compare commits

...

7 Commits

17 changed files with 593 additions and 81 deletions

21
.dockerignore Normal file
View File

@@ -0,0 +1,21 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.git
.gitignore
logs
gallery
md_gen
credentials.toml
manual_slop.toml
manual_slop_history.toml
manualslop_layout.ini
dpg_layout.ini
.pytest_cache
scripts/generated
.gemini
conductor/archive
.editorconfig
*.log

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Use python:3.11-slim as a base
FROM python:3.11-slim
# Set environment variables
# UV_SYSTEM_PYTHON=1 allows uv to install into the system site-packages
ENV PYTHONDONTWRITEBYTECODE=1
PYTHONUNBUFFERED=1
UV_SYSTEM_PYTHON=1
# Install system dependencies and uv
RUN apt-get update && apt-get install -y --no-install-recommends
curl
ca-certificates
&& rm -rf /var/lib/apt/lists/*
&& curl -LsSf https://astral.sh/uv/install.sh | sh
&& mv /root/.local/bin/uv /usr/local/bin/uv
# Set the working directory in the container
WORKDIR /app
# Copy dependency files first to leverage Docker layer caching
COPY pyproject.toml requirements.txt* ./
# Install dependencies via uv
RUN if [ -f requirements.txt ]; then uv pip install --no-cache -r requirements.txt; fi
# Copy the rest of the application code
COPY . .
# Expose port 8000 for the headless API/service
EXPOSE 8000
# Set the entrypoint to run the app in headless mode
ENTRYPOINT ["python", "gui_2.py", "--headless"]

View File

@@ -19,3 +19,5 @@ To serve as an expert-level utility for personal developer use on small projects
- **Session Analysis:** Ability to load and visualize historical session logs with a dedicated tinted "Prior Session" viewing mode.
- **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis.
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
- **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid).
- **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety.

View File

@@ -9,6 +9,11 @@
- **Dear PyGui:** For immediate/retained mode GUI rendering and node mapping.
- **ImGui Bundle (`imgui-bundle`):** To provide advanced multi-viewport and dockable panel capabilities on top of Dear ImGui.
## Web & Service Frameworks
- **FastAPI:** High-performance REST API framework for providing the headless backend service.
- **Uvicorn:** ASGI server for serving the FastAPI application.
## AI Integration SDKs
- **google-genai:** For Google Gemini API interaction and explicit context caching.

View File

@@ -41,6 +41,6 @@ This file tracks all major tracks for the project. Each track has its own detail
---
- [~] **Track: Support headless manual_slop for making an unraid gui docker frontend and a unraid server backend down the line.**
- [x] **Track: Support headless manual_slop for making an unraid gui docker frontend and a unraid server backend down the line.**
*Link: [./tracks/manual_slop_headless_20260225/](./tracks/manual_slop_headless_20260225/)*

View File

@@ -1,49 +1,49 @@
# Implementation Plan: Manual Slop Headless Backend
## Phase 1: Project Setup & Headless Scaffold
## Phase 1: Project Setup & Headless Scaffold [checkpoint: d5f056c]
- [x] Task: Update dependencies (02fc847)
- [ ] Add `fastapi` and `uvicorn` to `pyproject.toml` (and sync `requirements.txt` via `uv`).
- [~] Task: Implement headless startup
- [ ] 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.
- [ ] Bypass Dear PyGui initialization if headless mode is active.
- [ ] Task: Create foundational API application
- [ ] Set up the core FastAPI application instance.
- [ ] Implement `/health` and `/status` endpoints for Docker lifecycle checks.
- [ ] Task: Conductor - User Manual Verification 'Project Setup & Headless Scaffold' (Protocol in workflow.md)
- [x] Add `fastapi` and `uvicorn` to `pyproject.toml` (and sync `requirements.txt` via `uv`).
- [x] Task: Implement headless startup
- [x] Modify `gui_2.py` (or create `headless.py`) to parse a `--headless` CLI flag.
- [x] Update config parsing in `config.toml` to support headless configuration sections.
- [x] Bypass Dear PyGui initialization if headless mode is active.
- [x] Task: Create foundational API application
- [x] Set up the core FastAPI application instance.
- [x] Implement `/health` and `/status` endpoints for Docker lifecycle checks.
- [x] Task: Conductor - User Manual Verification 'Project Setup & Headless Scaffold' (Protocol in workflow.md) d5f056c
## Phase 2: Core API Routes & Authentication
- [ ] Task: Implement API Key Security
- [ ] Create a dependency/middleware in FastAPI to validate `X-API-KEY`.
- [ ] Configure the API key validator to read from environment variables or `manual_slop.toml` (supporting Unraid template secrets).
- [ ] Add tests for authorized and unauthorized API access.
- [ ] Task: Implement AI Generation Endpoint
- [ ] Create a `/api/v1/generate` POST endpoint.
- [ ] Map request payloads to `ai_client.py` unified wrappers.
- [ ] Return standard JSON responses with the generated text and token metrics.
- [ ] Task: Conductor - User Manual Verification 'Core API Routes & Authentication' (Protocol in workflow.md)
## Phase 2: Core API Routes & Authentication [checkpoint: 4e0bcd5]
- [x] Task: Implement API Key Security
- [x] Create a dependency/middleware in FastAPI to validate `X-API-KEY`.
- [x] Configure the API key validator to read from environment variables or `manual_slop.toml` (supporting Unraid template secrets).
- [x] Add tests for authorized and unauthorized API access.
- [x] Task: Implement AI Generation Endpoint
- [x] Create a `/api/v1/generate` POST endpoint.
- [x] Map request payloads to `ai_client.py` unified wrappers.
- [x] Return standard JSON responses with the generated text and token metrics.
- [x] Task: Conductor - User Manual Verification 'Core API Routes & Authentication' (Protocol in workflow.md) 4e0bcd5
## Phase 3: Remote Tool Confirmation Mechanism
- [ ] Task: Refactor Execution Engine for Async Wait
- [ ] Modify `shell_runner.py` and tool-call loops to support a non-blocking "Pending Confirmation" state instead of launching a GUI modal.
- [ ] Task: Implement Pending Action Queue
- [ ] Create an in-memory (or file-backed) queue for tracking unconfirmed PowerShell scripts.
- [ ] Task: Expose Confirmation API
- [ ] Create `/api/v1/pending_actions` endpoint (GET) to list pending scripts.
- [ ] Create `/api/v1/confirm/{action_id}` endpoint (POST) to approve or deny a script execution.
- [ ] Ensure the AI generation loop correctly resumes upon receiving approval.
- [ ] Task: Conductor - User Manual Verification 'Remote Tool Confirmation Mechanism' (Protocol in workflow.md)
## Phase 3: Remote Tool Confirmation Mechanism [checkpoint: a6e184e]
- [x] Task: Refactor Execution Engine for Async Wait
- [x] Modify `shell_runner.py` and tool-call loops to support a non-blocking "Pending Confirmation" state instead of launching a GUI modal.
- [x] Task: Implement Pending Action Queue
- [x] Create an in-memory (or file-backed) queue for tracking unconfirmed PowerShell scripts.
- [x] Task: Expose Confirmation API
- [x] Create `/api/v1/pending_actions` endpoint (GET) to list pending scripts.
- [x] Create `/api/v1/confirm/{action_id}` endpoint (POST) to approve or deny a script execution.
- [x] Ensure the AI generation loop correctly resumes upon receiving approval.
- [x] Task: Conductor - User Manual Verification 'Remote Tool Confirmation Mechanism' (Protocol in workflow.md) a6e184e
## Phase 4: Session & Context Management via API
- [ ] Task: Expose Session History
- [ ] Create endpoints to list, retrieve, and delete session logs from the `project_history.toml`.
- [ ] Task: Expose Context Configuration
- [ ] Create endpoints to list currently tracked files/folders in the project scope.
- [ ] Task: Conductor - User Manual Verification 'Session & Context Management via API' (Protocol in workflow.md)
## Phase 4: Session & Context Management via API [checkpoint: 7f3a1e2]
- [x] Task: Expose Session History
- [x] Create endpoints to list, retrieve, and delete session logs from the `project_history.toml`.
- [x] Task: Expose Context Configuration
- [x] Create endpoints to list currently tracked files/folders in the project scope.
- [x] Task: Conductor - User Manual Verification 'Session & Context Management via API' (Protocol in workflow.md) 7f3a1e2
## Phase 5: Dockerization
- [ ] Task: Create Dockerfile
- [ ] Write a `Dockerfile` using `python:3.11-slim` as a base.
- [ ] Configure `uv` inside the container for fast dependency installation.
- [ ] Expose the API port (e.g., 8000) and set the container entrypoint.
- [ ] Task: Conductor - User Manual Verification 'Dockerization' (Protocol in workflow.md)
## Phase 5: Dockerization [checkpoint: 5176b8d]
- [x] Task: Create Dockerfile
- [x] Write a `Dockerfile` using `python:3.11-slim` as a base.
- [x] Configure `uv` inside the container for fast dependency installation.
- [x] Expose the API port (e.g., 8000) and set the container entrypoint.
- [x] Task: Conductor - User Manual Verification 'Dockerization' (Protocol in workflow.md) 5176b8d

View File

@@ -8,6 +8,7 @@
4. **High Code Coverage:** Aim for >80% code coverage for all modules
5. **User Experience First:** Every decision should prioritize user experience
6. **Non-Interactive & CI-Aware:** Prefer non-interactive commands. Use `CI=true` for watch-mode tools (tests, linters) to ensure single execution.
7. **MMA Tiered Delegation is Mandatory:** The Conductor acts as a Tier 1/2 Orchestrator. You MUST delegate all non-trivial coding to Tier 3 Workers and all error analysis to Tier 4 QA Agents. Do NOT perform large file writes directly.
## Task Workflow
@@ -15,17 +16,20 @@ All tasks follow a strict lifecycle:
### Standard Task Workflow
0. **Initialize MMA Environment:** Before executing the first task of any track, you MUST activate the `mma-orchestrator` skill (`activate_skill mma-orchestrator`).
1. **Select Task:** Choose the next available task from `plan.md` in sequential order
2. **Mark In Progress:** Before beginning work, edit `plan.md` and change the task from `[ ]` to `[~]`
3. **Write Failing Tests (Red Phase):**
- Create a new test file for the feature or bug fix.
- Write one or more unit tests that clearly define the expected behavior and acceptance criteria for the task.
- **Delegate Test Creation:** Do NOT write test code directly. Spawn a Tier 3 Worker (`run_subagent.ps1 -Role Worker`) with a prompt to create the necessary test files and unit tests based on the task criteria.
- Take the code generated by the Worker and apply it.
- **CRITICAL:** Run the tests and confirm that they fail as expected. This is the "Red" phase of TDD. Do not proceed until you have failing tests.
4. **Implement to Pass Tests (Green Phase):**
- Write the minimum amount of application code necessary to make the failing tests pass.
- **Delegate Implementation:** Do NOT write the implementation code directly. Spawn a Tier 3 Worker (`run_subagent.ps1 -Role Worker`) with a highly specific prompt to write the minimum amount of application code necessary to make the failing tests pass.
- Take the code generated by the Worker and apply it.
- Run the test suite again and confirm that all tests now pass. This is the "Green" phase.
5. **Refactor (Optional but Recommended):**
@@ -84,7 +88,8 @@ All tasks follow a strict lifecycle:
- Before execution, you **must** announce the exact shell command you will use to run the tests.
- **Example Announcement:** "I will now run the automated test suite to verify the phase. **Command:** `CI=true npm test`"
- Execute the announced command.
- If tests fail, you **must** inform the user and begin debugging. You may attempt to propose a fix a **maximum of two times**. If the tests still fail after your second proposed fix, you **must stop**, report the persistent failure, and ask the user for guidance.
- If tests fail with significant output (e.g., a large traceback), **DO NOT** attempt to read the raw `stderr` directly into your context. Instead, pipe the output to a log file and **spawn a Tier 4 QA Agent (`run_subagent.ps1 -Role QA`)** to summarize the failure.
- You **must** inform the user and begin debugging using the QA Agent's summary. You may attempt to propose a fix a **maximum of two times**. If the tests still fail after your second proposed fix, you **must stop**, report the persistent failure, and ask the user for guidance.
4. **Execute Automated API Hook Verification:**
- **CRITICAL:** The Conductor agent will now automatically execute verification tasks using the application's API hooks.

View File

@@ -7,9 +7,9 @@ history_trunc_limit = 8000
system_prompt = ""
[theme]
palette = "Gold"
font_size = 14.0
scale = 1.2000000476837158
palette = "ImGui Dark"
font_size = 16.0
scale = 1.0
font_path = ""
[projects]
@@ -32,3 +32,7 @@ active = "C:\\projects\\manual_slop\\tests\\temp_project.toml"
"Operations Hub" = true
Theme = true
Diagnostics = true
[headless]
port = 8000
api_key = ""

240
gui_2.py
View File

@@ -6,6 +6,7 @@ import math
import json
import sys
import os
import uuid
from pathlib import Path
from tkinter import filedialog, Tk
import aggregate
@@ -22,6 +23,9 @@ import api_hooks
import mcp_client
from performance_monitor import PerformanceMonitor
from fastapi import FastAPI, Depends, HTTPException, Security
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel
from imgui_bundle import imgui, hello_imgui, immapp
CONFIG_PATH = Path("config.toml")
@@ -87,10 +91,8 @@ def _parse_history_entries(history: list[str], roles: list[str] | None = None) -
class ConfirmDialog:
_next_id = 0
def __init__(self, script: str, base_dir: str):
ConfirmDialog._next_id += 1
self._uid = ConfirmDialog._next_id
self._uid = str(uuid.uuid4())
self._script = str(script) if script is not None else ""
self._base_dir = str(base_dir) if base_dir is not None else ""
self._condition = threading.Condition()
@@ -188,6 +190,7 @@ class App:
self._pending_dialog: ConfirmDialog | None = None
self._pending_dialog_open = False
self._pending_dialog_lock = threading.Lock()
self._pending_actions: dict[str, ConfirmDialog] = {}
self._tool_log: list[tuple[str, str]] = []
self._comms_log: list[dict] = []
@@ -303,6 +306,192 @@ class App:
self._discussion_names_cache = []
self._discussion_names_dirty = True
def create_api(self) -> FastAPI:
api = FastAPI(title="Manual Slop Headless API")
class GenerateRequest(BaseModel):
prompt: str
auto_add_history: bool = True
temperature: float | None = None
max_tokens: int | None = None
class ConfirmRequest(BaseModel):
approved: bool
API_KEY_NAME = "X-API-KEY"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
async def get_api_key(header_key: str = Depends(api_key_header)):
headless_cfg = self.config.get("headless", {})
config_key = headless_cfg.get("api_key", "").strip()
env_key = os.environ.get("SLOP_API_KEY", "").strip()
target_key = env_key or config_key
if not target_key:
return None
if header_key == target_key:
return header_key
raise HTTPException(status_code=403, detail="Could not validate API Key")
@api.get("/health")
def health():
return {"status": "ok"}
@api.get("/status", dependencies=[Depends(get_api_key)])
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
}
@api.get("/api/v1/pending_actions", dependencies=[Depends(get_api_key)])
def pending_actions():
actions = []
with self._pending_dialog_lock:
# Include multi-actions from headless mode
for uid, dialog in self._pending_actions.items():
actions.append({
"action_id": uid,
"script": dialog._script,
"base_dir": dialog._base_dir
})
# Include single active dialog from GUI mode
if self._pending_dialog:
actions.append({
"action_id": self._pending_dialog._uid,
"script": self._pending_dialog._script,
"base_dir": self._pending_dialog._base_dir
})
return actions
@api.post("/api/v1/confirm/{action_id}", dependencies=[Depends(get_api_key)])
def confirm_action(action_id: str, req: ConfirmRequest):
success = self.resolve_pending_action(action_id, req.approved)
if not success:
raise HTTPException(status_code=404, detail=f"Action ID {action_id} not found")
return {"status": "success", "action_id": action_id, "approved": req.approved}
@api.get("/api/v1/sessions", dependencies=[Depends(get_api_key)])
def list_sessions():
log_dir = Path("logs")
if not log_dir.exists():
return []
return sorted([f.name for f in log_dir.glob("*.log")], reverse=True)
@api.get("/api/v1/sessions/{filename}", dependencies=[Depends(get_api_key)])
def get_session(filename: str):
if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
log_path = Path("logs") / filename
if not log_path.exists() or not log_path.is_file():
raise HTTPException(status_code=404, detail="Session log not found")
try:
content = log_path.read_text(encoding="utf-8")
return {"filename": filename, "content": content}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@api.delete("/api/v1/sessions/{filename}", dependencies=[Depends(get_api_key)])
def delete_session(filename: str):
if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
log_path = Path("logs") / filename
if not log_path.exists() or not log_path.is_file():
raise HTTPException(status_code=404, detail="Session log not found")
try:
log_path.unlink()
return {"status": "success", "message": f"Deleted {filename}"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@api.get("/api/v1/context", dependencies=[Depends(get_api_key)])
def get_context():
return {
"files": self.files,
"screenshots": self.screenshots,
"files_base_dir": self.ui_files_base_dir,
"screenshots_base_dir": self.ui_shots_base_dir
}
@api.post("/api/v1/generate", dependencies=[Depends(get_api_key)])
def generate(req: GenerateRequest):
if not req.prompt.strip():
raise HTTPException(status_code=400, detail="Prompt cannot be empty")
with self._send_thread_lock:
start_time = time.time()
try:
# Refresh context before sending
md, path, file_items, stable_md, disc_text = self._do_generate()
self.last_md = md
self.last_md_path = path
self.last_file_items = file_items
except Exception as e:
raise HTTPException(status_code=500, detail=f"Context aggregation failure: {e}")
user_msg = req.prompt
base_dir = self.ui_files_base_dir
csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()])
ai_client.set_custom_system_prompt("\n\n".join(csp))
# Override parameters if provided in request, otherwise use GUI defaults
temp = req.temperature if req.temperature is not None else self.temperature
tokens = req.max_tokens if req.max_tokens is not None else self.max_tokens
ai_client.set_model_params(temp, tokens, self.history_trunc_limit)
ai_client.set_agent_tools(self.ui_agent_tools)
if req.auto_add_history:
with self._pending_history_adds_lock:
self._pending_history_adds.append({
"role": "User",
"content": user_msg,
"collapsed": False,
"ts": project_manager.now_ts()
})
try:
resp = ai_client.send(stable_md, user_msg, base_dir, self.last_file_items, disc_text)
if req.auto_add_history:
with self._pending_history_adds_lock:
self._pending_history_adds.append({
"role": "AI",
"content": resp,
"collapsed": False,
"ts": project_manager.now_ts()
})
# Ensure metrics are updated for the response
self._recalculate_session_usage()
duration = time.time() - start_time
return {
"text": resp,
"metadata": {
"provider": self.current_provider,
"model": self.current_model,
"duration_sec": round(duration, 3),
"timestamp": project_manager.now_ts()
},
"usage": self.session_usage
}
except ProviderError as e:
# Specific error handling for vendor issues (4xx/5xx from Gemini/Anthropic)
raise HTTPException(status_code=502, detail=f"AI Provider Error: {e.ui_message()}")
except Exception as e:
# Generic internal error
raise HTTPException(status_code=500, detail=f"In-flight AI request failure: {e}")
@api.post("/api/v1/stream", dependencies=[Depends(get_api_key)])
async def stream(req: GenerateRequest):
# Streaming implementation would require ai_client to support yield-based responses.
# Currently added as a placeholder to satisfy spec requirements.
raise HTTPException(status_code=501, detail="Streaming endpoint (/api/v1/stream) is not yet supported in this version.")
return api
# ---------------------------------------------------------------- project loading
def _cb_new_project_automated(self, user_data):
@@ -739,6 +928,14 @@ class App:
def _confirm_and_run(self, script: str, base_dir: str) -> str | None:
print(f"[DEBUG] _confirm_and_run triggered for script length: {len(script)}")
dialog = ConfirmDialog(script, base_dir)
is_headless = "--headless" in sys.argv
if is_headless:
with self._pending_dialog_lock:
self._pending_actions[dialog._uid] = dialog
print(f"[PENDING_ACTION] Created action {dialog._uid}")
else:
with self._pending_dialog_lock:
self._pending_dialog = dialog
@@ -748,12 +945,19 @@ class App:
with self._api_event_queue_lock:
self._api_event_queue.append({
"type": "script_confirmation_required",
"action_id": dialog._uid,
"script": str(script),
"base_dir": str(base_dir),
"ts": time.time()
})
approved, final_script = dialog.wait()
if is_headless:
with self._pending_dialog_lock:
if dialog._uid in self._pending_actions:
del self._pending_actions[dialog._uid]
print(f"[DEBUG] _confirm_and_run result: approved={approved}")
if not approved:
self._append_tool_log(final_script, "REJECTED by user")
@@ -766,6 +970,24 @@ class App:
self.ai_status = "powershell done, awaiting AI..."
return output
def resolve_pending_action(self, action_id: str, approved: bool):
with self._pending_dialog_lock:
if action_id in self._pending_actions:
dialog = self._pending_actions[action_id]
with dialog._condition:
dialog._approved = approved
dialog._done = True
dialog._condition.notify_all()
return True
elif self._pending_dialog and self._pending_dialog._uid == action_id:
dialog = self._pending_dialog
with dialog._condition:
dialog._approved = approved
dialog._done = True
dialog._condition.notify_all()
return True
return False
def _append_tool_log(self, script: str, result: str):
self._tool_log.append((script, result, time.time()))
self.ui_last_script_text = script
@@ -2001,6 +2223,17 @@ class App:
def run(self):
"""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)
self.runner_params = hello_imgui.RunnerParams()
@@ -2028,6 +2261,7 @@ class App:
# On exit
self.hook_server.stop()
self.perf_monitor.stop()
ai_client.cleanup() # Destroy active API caches to stop billing
self._flush_to_project()

View File

@@ -8,5 +8,5 @@ active = "main"
[discussions.main]
git_commit = ""
last_updated = "2026-02-25T11:19:43"
last_updated = "2026-02-25T13:21:57"
history = []

BIN
test_output.log Normal file

Binary file not shown.

BIN
test_output_final.log Normal file

Binary file not shown.

BIN
test_output_phase4.log Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -9,7 +9,56 @@ auto_add = true
[discussions.main]
git_commit = ""
last_updated = "2026-02-25T11:21:27"
last_updated = "2026-02-25T13:21:57"
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",
]

110
tests/test_headless_api.py Normal file
View File

@@ -0,0 +1,110 @@
import unittest
from fastapi.testclient import TestClient
import gui_2
from unittest.mock import patch, MagicMock
from pathlib import Path
class TestHeadlessAPI(unittest.TestCase):
def setUp(self):
# 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'):
self.app_instance = gui_2.App()
# Clear any leftover state
self.app_instance._pending_actions = {}
self.app_instance._pending_dialog = None
self.api = self.app_instance.create_api()
self.client = TestClient(self.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_unauthorized(self):
# Ensure a key is required
with patch.dict(self.app_instance.config, {"headless": {"api_key": "some-required-key"}}):
response = self.client.get("/status")
self.assertEqual(response.status_code, 403)
def test_status_endpoint_authorized(self):
# We'll use a test key
headers = {"X-API-KEY": "test-secret-key"}
with patch.dict(self.app_instance.config, {"headless": {"api_key": "test-secret-key"}}):
response = self.client.get("/status", headers=headers)
self.assertEqual(response.status_code, 200)
def test_generate_endpoint(self):
payload = {
"prompt": "Hello AI"
}
# Mock ai_client.send and get_comms_log
with patch('gui_2.ai_client.send') as mock_send, \
patch('gui_2.ai_client.get_comms_log') as mock_log:
mock_send.return_value = "Hello from Mock AI"
mock_log.return_value = [{
"kind": "response",
"payload": {
"usage": {"input_tokens": 10, "output_tokens": 5}
}
}]
response = self.client.post("/api/v1/generate", json=payload)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["text"], "Hello from Mock AI")
self.assertIn("metadata", data)
self.assertEqual(data["usage"]["input_tokens"], 10)
def test_pending_actions_endpoint(self):
# Manually add a pending action
with patch('gui_2.uuid.uuid4', return_value="test-action-id"):
dialog = gui_2.ConfirmDialog("dir", ".")
self.app_instance._pending_actions[dialog._uid] = dialog
response = self.client.get("/api/v1/pending_actions")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["action_id"], "test-action-id")
def test_confirm_action_endpoint(self):
# Manually add a pending action
with patch('gui_2.uuid.uuid4', return_value="test-confirm-id"):
dialog = gui_2.ConfirmDialog("dir", ".")
self.app_instance._pending_actions[dialog._uid] = dialog
payload = {"approved": True}
response = self.client.post("/api/v1/confirm/test-confirm-id", json=payload)
self.assertEqual(response.status_code, 200)
self.assertTrue(dialog._done)
self.assertTrue(dialog._approved)
def test_list_sessions_endpoint(self):
# Ensure logs directory exists
Path("logs").mkdir(exist_ok=True)
# Create a dummy log
dummy_log = Path("logs/test_session_api.log")
dummy_log.write_text("dummy content")
try:
response = self.client.get("/api/v1/sessions")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn("test_session_api.log", data)
finally:
if dummy_log.exists():
dummy_log.unlink()
def test_get_context_endpoint(self):
response = self.client.get("/api/v1/context")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertIn("files", data)
self.assertIn("screenshots", data)
self.assertIn("files_base_dir", data)
if __name__ == "__main__":
unittest.main()

View 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()