Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Note:** This document is automatically generated and verified against the live test suite by `scripts/generate_requirements.py` and `tests/backend/test_requirements_sync.py`.

**Test Verification Baseline:** **968 Automated Tests** (651 Pytest Backend + 271 Vitest Frontend + 46 Playwright E2E).
**Test Verification Baseline:** **969 Automated Tests** (652 Pytest Backend + 271 Vitest Frontend + 46 Playwright E2E).

---

Expand Down Expand Up @@ -980,14 +980,15 @@ persisting all records and vector points correctly across multiple flushes._
- `test_incremental_pipeline_clone_error_resilience` - _Verifies that a failure during shallow clone records an error in git_repositories
and leaves the prior indexed state intact without data loss._

#### `tests/test_litellm_service.py` (7 tests)
#### `tests/test_litellm_service.py` (8 tests)
- `test_discover_models_success`
- `test_discover_models_timeout`
- `test_discover_models_connect_error`
- `test_discover_models_http_401_unauthorized`
- `test_discover_models_http_500_error`
- `test_discover_models_url_normalization`
- `test_discover_models_default_resolution`
- `test_discover_models_ssrf_rejection`

#### `tests/test_local_storage_indexing.py` (4 tests)
- `test_incremental_indexing_on_save`
Expand Down
5 changes: 4 additions & 1 deletion app/api/routers/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ async def api_read_file(
@router.post("/admin/api/files/summarize")
async def api_summarize_file(payload: FileSummarizePayload):
try:
raw_path = payload.path.strip() if payload.path else ""
if not raw_path or "\x00" in raw_path or any(part == ".." for part in raw_path.replace("\\", "/").split("/")):
return JSONResponse(status_code=400, content={"error": "Path traversal or invalid path detected."})
summarizer = get_summarizer_service()
summary_text = summarizer.get_or_create_summary(
filepath=payload.path,
filepath=raw_path,
repo=payload.repo,
force_refresh=payload.force_refresh
)
Expand Down
4 changes: 2 additions & 2 deletions app/api/routers/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def api_get_graph_topology(
return data
except Exception as e:
logger.error(f"Error generating topology graph: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to generate topology graph."})

@router.get("/admin/api/graph/node-details")
async def api_get_graph_node_details(id: str):
Expand All @@ -56,4 +56,4 @@ async def api_get_graph_node_details(id: str):
return details
except Exception as e:
logger.error(f"Error getting node details for {id}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to retrieve node details."})
2 changes: 1 addition & 1 deletion app/api/routers/ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ async def api_get_ingestion_catalog(
}
except Exception as e:
logger.error(f"Error fetching ingestion catalog: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to fetch ingestion catalog."})
6 changes: 3 additions & 3 deletions app/api/routers/navigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async def api_get_navigator_tree(
return data
except Exception as e:
logger.error(f"Error generating navigator tree for {repo}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to generate navigator tree."})


@router.get("/admin/api/navigator/file-outline")
Expand All @@ -42,7 +42,7 @@ async def api_get_file_outline(
return data
except Exception as e:
logger.error(f"Error getting file outline for {filepath} in {repo}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to retrieve file outline."})


@router.get("/admin/api/navigator/symbol-impact")
Expand All @@ -60,4 +60,4 @@ async def api_get_symbol_impact(
return data
except Exception as e:
logger.error(f"Error getting symbol impact for {symbol_id} in {repo}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to retrieve symbol impact."})
30 changes: 18 additions & 12 deletions app/api/routers/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ async def api_get_repos():
return result
except Exception as e:
logger.error(f"Error fetching repos: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to fetch repositories."})

@router.post("/admin/api/repos")
async def api_add_repo(repo: RepoConfig):
Expand Down Expand Up @@ -71,7 +71,7 @@ async def api_add_repo(repo: RepoConfig):
return JSONResponse(status_code=400, content={"error": f"Repository with name '{repo.name}' already exists"})
except Exception as e:
logger.error(f"Error adding repo: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to add repository."})

@router.patch("/admin/api/repos/{repo_id}/auto-sync")
async def api_toggle_repo_auto_sync(repo_id: int, payload: AutoSyncToggleRequest):
Expand All @@ -82,7 +82,7 @@ async def api_toggle_repo_auto_sync(repo_id: int, payload: AutoSyncToggleRequest
return {"status": "success", "id": repo_id, "repo_id": repo_id, "auto_sync": payload.auto_sync}
except Exception as e:
logger.error(f"Error toggling auto-sync for repo {repo_id}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to toggle auto-sync for repository."})

@router.post("/admin/api/repos/{repo_id}/sync")
@router.post("/admin/api/repos/sync/{repo_id}")
Expand All @@ -102,7 +102,7 @@ async def api_sync_repo(repo_id: int):
return {"status": "success", "repo": r["name"], "message": f"Sync started for {r['name']}"}
except Exception as e:
logger.error(f"Error syncing repo {repo_id}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to sync repository."})

STREAM_KEEPALIVE_TIMEOUT = 15.0

Expand Down Expand Up @@ -183,7 +183,7 @@ async def api_delete_repo(repo_id: int):
return {"status": "success", "name": name, "deleted": name}
except Exception as e:
logger.error(f"Error deleting repo {repo_id}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to delete repository."})

@router.get("/admin/api/paths")
async def api_get_paths():
Expand All @@ -193,12 +193,15 @@ async def api_get_paths():
return [dict(r) for r in rows]
except Exception as e:
logger.error(f"Error getting paths: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to get paths."})

@router.post("/admin/api/paths")
async def api_add_path(config: LocalPathConfig):
try:
resolved = os.path.abspath(config.path)
raw_path = config.path.strip() if config.path else ""
if not raw_path or "\x00" in raw_path or any(part == ".." for part in raw_path.replace("\\", "/").split("/")):
return JSONResponse(status_code=400, content={"error": "Path traversal or invalid path detected."})
resolved = os.path.normpath(os.path.abspath(raw_path))
if not os.path.exists(resolved):
return JSONResponse(status_code=400, content={"error": f"Path '{resolved}' does not exist on disk."})

Expand All @@ -217,7 +220,7 @@ async def api_add_path(config: LocalPathConfig):
return JSONResponse(status_code=400, content={"error": "Path already indexed"})
except Exception as e:
logger.error(f"Error adding path: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to add path."})

@router.delete("/admin/api/paths/{path_id}")
async def api_delete_path(path_id: int):
Expand All @@ -241,7 +244,7 @@ async def api_delete_path(path_id: int):
return {"status": "success", "path": path_val, "deleted": path_val}
except Exception as e:
logger.error(f"Error deleting path {path_id}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to delete path."})

@router.post("/admin/api/sync")
@router.post("/admin/api/reindex")
Expand Down Expand Up @@ -275,11 +278,14 @@ async def api_test_search(payload: SearchRequest):
return {"query": query, "type": payload.type, "results": results}
except Exception as e:
logger.error(f"Error testing search: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to execute search test."})

@router.get("/admin/api/browse")
async def api_browse_dir(path: str = "/"):
resolved = os.path.abspath(path)
cleaned = path.strip() if path else "/"
if "\x00" in cleaned or any(part in ("..", ".") for part in cleaned.replace("\\", "/").split("/") if part):
cleaned = "/"
resolved = os.path.normpath(os.path.abspath(cleaned))
if not os.path.exists(resolved):
resolved = "/"
try:
Expand All @@ -303,4 +309,4 @@ async def api_browse_dir(path: str = "/"):
}
except Exception as e:
logger.error(f"Error browsing dir {path}: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to browse directory."})
47 changes: 31 additions & 16 deletions app/api/routers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sqlite3
import logging
from typing import Optional
from urllib.parse import urlsplit
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse

Expand Down Expand Up @@ -73,7 +74,8 @@ def _count(table):
vector_db_healthy = bool(healthy)
vector_db_status = "Healthy" if vector_db_healthy else "Unhealthy"
except Exception as e:
vector_db_status = f"Error: {e}"
logger.error(f"Error checking vector store health/stats: {e}")
vector_db_status = "Error"

gh_token, _, gh_src = db_service.get_effective_git_token("https://github.com", provider="github")
gl_token, _, gl_src = db_service.get_effective_git_token("https://gitlab.com", provider="gitlab")
Expand Down Expand Up @@ -135,7 +137,7 @@ def _count(table):
}
except Exception as e:
logger.error(f"Error fetching stats: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to fetch stats."})

@router.get("/admin/api/settings/hosts")
async def api_get_host_credentials():
Expand All @@ -154,7 +156,7 @@ async def api_get_host_credentials():
return safe_hosts
except Exception as e:
logger.error(f"Error listing host credentials: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to list host credentials."})

@router.post("/admin/api/settings/hosts")
async def api_save_host_credential(payload: HostCredentialRequest):
Expand All @@ -170,7 +172,7 @@ async def api_save_host_credential(payload: HostCredentialRequest):
return JSONResponse(status_code=400, content={"error": f"Credentials for host '{payload.host}' already exist"})
except Exception as e:
logger.error(f"Error saving host credential: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to save host credential."})

@router.delete("/admin/api/settings/hosts/{host_id}")
async def api_delete_host_credential(host_id: int):
Expand All @@ -181,7 +183,7 @@ async def api_delete_host_credential(host_id: int):
return {"status": "success", "id": host_id}
except Exception as e:
logger.error(f"Error deleting host credential: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to delete host credential."})

@router.post("/admin/api/settings/token")
async def api_save_token(payload: TokenRequest):
Expand Down Expand Up @@ -212,7 +214,7 @@ async def api_save_token(payload: TokenRequest):
}
except Exception as e:
logger.error(f"Error saving tokens: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to save tokens."})

@router.get("/admin/api/settings/auto-sync")
async def api_get_auto_sync_settings():
Expand All @@ -226,7 +228,7 @@ async def api_get_auto_sync_settings():
}
except Exception as e:
logger.error(f"Error loading auto-sync settings: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to load auto-sync settings."})

@router.post("/admin/api/settings/auto-sync")
async def api_save_auto_sync_settings(payload: AutoSyncSettingsRequest):
Expand All @@ -246,15 +248,15 @@ async def api_save_auto_sync_settings(payload: AutoSyncSettingsRequest):
}
except Exception as e:
logger.error(f"Error saving auto-sync settings: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to save auto-sync settings."})

@router.get("/admin/api/logs")
async def api_get_logs(limit: int = 200, level: Optional[str] = None, search: Optional[str] = None):
try:
return log_service.get_diagnostic_logs(limit=limit, level=level, search=search)
except Exception as e:
logger.error(f"Error fetching diagnostic logs: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to fetch diagnostic logs."})

@router.delete("/admin/api/logs")
async def api_clear_logs():
Expand All @@ -263,7 +265,7 @@ async def api_clear_logs():
return {"status": "success", "message": "Diagnostic logs successfully cleared."}
except Exception as e:
logger.error(f"Error clearing diagnostic logs: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to clear diagnostic logs."})

@router.get("/admin/api/vector-store")
async def api_get_vector_store():
Expand All @@ -273,7 +275,7 @@ async def api_get_vector_store():
return cfg
except Exception as e:
logger.error(f"Error reading vector store config: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to read vector store config."})

@router.post("/admin/api/vector-store/test")
async def api_test_vector_store(payload: VectorStoreTestRequest):
Expand All @@ -293,7 +295,7 @@ async def api_test_vector_store(payload: VectorStoreTestRequest):
}
except Exception as e:
logger.error(f"Error testing vector store: {e}")
return JSONResponse(status_code=500, content={"success": False, "error": str(e), "message": str(e)})
return JSONResponse(status_code=500, content={"success": False, "error": "Failed to test vector store.", "message": "Failed to test vector store."})

@router.post("/admin/api/vector-store/switch")
async def api_switch_vector_store(payload: VectorStoreSwitchRequest):
Expand Down Expand Up @@ -321,16 +323,29 @@ def _reindex():
}
except Exception as e:
logger.error(f"Error switching vector store backend: {e}")
return JSONResponse(status_code=500, content={"status": "error", "error": str(e), "message": str(e)})
return JSONResponse(status_code=500, content={"status": "error", "error": "Failed to switch vector store backend.", "message": "Failed to switch vector store backend."})

@router.get("/admin/api/models/discover")
async def api_discover_models(url: Optional[str] = None, api_key: Optional[str] = None):
try:
if url:
parsed = urlsplit(url.strip())
if parsed.scheme not in ("http", "https"):
return JSONResponse(
status_code=400,
content={"status": "error", "error": "Invalid URL scheme: only http and https are allowed."}
)
host = (parsed.hostname or "").lower()
if not host or host in ("169.254.169.254", "metadata.google.internal") or host.startswith("169.254."):
return JSONResponse(
status_code=400,
content={"status": "error", "error": "Invalid or restricted target host."}
)
res = await litellm_service.discover_models(url=url, api_key=api_key)
return res
except Exception as e:
logger.error(f"Error discovering models: {e}")
return JSONResponse(status_code=500, content={"status": "error", "error": str(e), "message": str(e)})
return JSONResponse(status_code=500, content={"status": "error", "error": "Failed to discover models."})

@router.get("/admin/api/settings/embedding")
async def api_get_embedding_settings():
Expand All @@ -339,7 +354,7 @@ async def api_get_embedding_settings():
return cfg
except Exception as e:
logger.error(f"Error reading embedding settings: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
return JSONResponse(status_code=500, content={"error": "Failed to read embedding settings."})

@router.post("/admin/api/settings/embedding")
async def api_save_embedding_settings(payload: EmbeddingSettingsRequest):
Expand All @@ -362,7 +377,7 @@ async def api_save_embedding_settings(payload: EmbeddingSettingsRequest):
}
except Exception as e:
logger.error(f"Error updating embedding settings: {e}")
return JSONResponse(status_code=500, content={"status": "error", "error": str(e), "message": str(e)})
return JSONResponse(status_code=500, content={"status": "error", "error": "Failed to update embedding settings.", "message": "Failed to update embedding settings."})

@router.get("/admin/api/settings/files")
async def api_get_file_settings():
Expand Down
Loading
Loading