Compare commits
7 Commits
33a603c0c5
...
63fd391dff
| Author | SHA1 | Date | |
|---|---|---|---|
| 63fd391dff | |||
| 6eb88a4041 | |||
| 28fcaa7eae | |||
| 386e36a92b | |||
| 1491619310 | |||
| 4e0bcd5188 | |||
| d5f056c3d1 |
21
.dockerignore
Normal file
21
.dockerignore
Normal 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
34
Dockerfile
Normal 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"]
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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/)*
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
10
config.toml
10
config.toml
@@ -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 = ""
|
||||
|
||||
286
gui_2.py
286
gui_2.py
@@ -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,8 +928,16 @@ 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)
|
||||
with self._pending_dialog_lock:
|
||||
self._pending_dialog = dialog
|
||||
|
||||
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
|
||||
|
||||
# Notify API hook subscribers
|
||||
if self.test_hooks_enabled and hasattr(self, '_api_event_queue'):
|
||||
@@ -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,33 +2223,45 @@ class App:
|
||||
|
||||
def run(self):
|
||||
"""Initializes the ImGui runner and starts the main application loop."""
|
||||
theme.load_from_config(self.config)
|
||||
if "--headless" in sys.argv:
|
||||
print("Headless mode active")
|
||||
self._fetch_models(self.current_provider)
|
||||
|
||||
self.runner_params = hello_imgui.RunnerParams()
|
||||
self.runner_params.app_window_params.window_title = "manual slop"
|
||||
self.runner_params.app_window_params.window_geometry.size = (1680, 1200)
|
||||
self.runner_params.imgui_window_params.enable_viewports = False
|
||||
self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
|
||||
self.runner_params.fps_idling.enable_idling = False
|
||||
self.runner_params.imgui_window_params.show_menu_bar = True
|
||||
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
|
||||
self.runner_params.ini_filename = "manualslop_layout.ini"
|
||||
import uvicorn
|
||||
headless_cfg = self.config.get("headless", {})
|
||||
port = headless_cfg.get("port", 8000)
|
||||
|
||||
self.runner_params.callbacks.show_gui = self._gui_func
|
||||
self.runner_params.callbacks.show_menus = self._show_menus
|
||||
self.runner_params.callbacks.load_additional_fonts = self._load_fonts
|
||||
self.runner_params.callbacks.post_init = self._post_init
|
||||
api = self.create_api()
|
||||
uvicorn.run(api, host="0.0.0.0", port=port)
|
||||
else:
|
||||
theme.load_from_config(self.config)
|
||||
|
||||
self._fetch_models(self.current_provider)
|
||||
self.runner_params = hello_imgui.RunnerParams()
|
||||
self.runner_params.app_window_params.window_title = "manual slop"
|
||||
self.runner_params.app_window_params.window_geometry.size = (1680, 1200)
|
||||
self.runner_params.imgui_window_params.enable_viewports = False
|
||||
self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space
|
||||
self.runner_params.fps_idling.enable_idling = False
|
||||
self.runner_params.imgui_window_params.show_menu_bar = True
|
||||
self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder
|
||||
self.runner_params.ini_filename = "manualslop_layout.ini"
|
||||
|
||||
# Start API hooks server (if enabled)
|
||||
self.hook_server = api_hooks.HookServer(self)
|
||||
self.hook_server.start()
|
||||
self.runner_params.callbacks.show_gui = self._gui_func
|
||||
self.runner_params.callbacks.show_menus = self._show_menus
|
||||
self.runner_params.callbacks.load_additional_fonts = self._load_fonts
|
||||
self.runner_params.callbacks.post_init = self._post_init
|
||||
|
||||
immapp.run(self.runner_params)
|
||||
self._fetch_models(self.current_provider)
|
||||
|
||||
# Start API hooks server (if enabled)
|
||||
self.hook_server = api_hooks.HookServer(self)
|
||||
self.hook_server.start()
|
||||
|
||||
immapp.run(self.runner_params)
|
||||
|
||||
# On exit
|
||||
self.hook_server.stop()
|
||||
|
||||
# On exit
|
||||
self.hook_server.stop()
|
||||
self.perf_monitor.stop()
|
||||
ai_client.cleanup() # Destroy active API caches to stop billing
|
||||
self._flush_to_project()
|
||||
|
||||
@@ -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
BIN
test_output.log
Normal file
Binary file not shown.
BIN
test_output_final.log
Normal file
BIN
test_output_final.log
Normal file
Binary file not shown.
BIN
test_output_phase4.log
Normal file
BIN
test_output_phase4.log
Normal file
Binary file not shown.
BIN
test_output_phase4_fixed.log
Normal file
BIN
test_output_phase4_fixed.log
Normal file
Binary file not shown.
@@ -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
110
tests/test_headless_api.py
Normal 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()
|
||||
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