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
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Read this entire file before changing anything. Every rule is binding.
./scripts/verify
```
It runs the `mcp/` gates (`npm run typecheck`, `npm test`, `npm run build`)
and the Python check (throwaway-venv `pip install -e .`, `code-search-api
--help`, and a loopback serve smoke test against a throwaway
and the Python checks (throwaway-venv package install, `pytest`,
`code-search-api --help`, and a loopback serve smoke test against a throwaway
`CODE_SEARCH_DB`, never port 5204).

A change may be reported complete only when every applicable check below has
Expand All @@ -18,10 +18,10 @@ failure verbatim and do not claim success.
- `npm run typecheck`
- `npm test`
- `npm run build`
- Touched Python (`src/`, `pyproject.toml`)? There is no Python test suite.
Smallest real check: `pip install -e .` in a throwaway venv, then run
`code-search-api --help` and `code-search-api serve` against a throwaway
`CODE_SEARCH_DB`. Never point any check at the production database.
- Touched Python (`src/`, `pyproject.toml`)? Run the focused Python tests, then
`./scripts/verify`, which runs the complete Python suite in a throwaway venv
before the CLI and serve smoke checks. Never point any check at the
production database.
- Docs-only change? Verify every command and path named in the file exists.

## Project Shape
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ CODE_SEARCH_SUMMARY_MODEL=qwen3:32b # local, needs ~20GB VRAM
| `POST` | `/api/index` | Yes | Trigger background indexing run |
| `POST` | `/api/backfill-summaries` | Yes | Generate summaries for unsummarized chunks |
| `GET` | `/api/projects` | Yes | Per-project chunk and summary counts |
| `GET` | `/api/stats` | No | Chunk type breakdown and project coverage |
| `GET` | `/api/stats` | Yes | Chunk type breakdown and project coverage |
| `GET` | `/api/summary-stats` | Yes | Summary counts by model |

### Search request
Expand Down
8 changes: 5 additions & 3 deletions scripts/verify
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
#
# Gates, in order:
# 1. mcp/: npm run typecheck, npm test, npm run build
# 2. Python: pip install -e . into a throwaway venv, code-search-api --help,
# then a serve smoke test on 127.0.0.1 with a throwaway CODE_SEARCH_DB.
# 2. Python: install the package and pytest into a throwaway venv, run the
# Python tests, run code-search-api --help, then a serve smoke test on
# 127.0.0.1 with a throwaway CODE_SEARCH_DB.
# Never touches the live 5204 instance or any real database.
set -euo pipefail
cd "$(dirname "$0")/.."
Expand All @@ -31,7 +32,8 @@ cleanup() {
trap cleanup EXIT

python3 -m venv "$TMPDIR_VERIFY/venv"
"$TMPDIR_VERIFY/venv/bin/pip" install --quiet -e .
"$TMPDIR_VERIFY/venv/bin/pip" install --quiet pytest -e .
"$TMPDIR_VERIFY/venv/bin/python" -m pytest -q
"$TMPDIR_VERIFY/venv/bin/code-search-api" --help >/dev/null

# Serve smoke test: throwaway DB, loopback only, never port 5204.
Expand Down
2 changes: 1 addition & 1 deletion src/code_search_api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,7 @@ def list_projects() -> dict[str, Any]:
return {"projects": [dict(r) for r in rows]}


@app.get("/api/stats")
@protected_api.get("/api/stats")
def stats() -> dict[str, Any]:
"""Detailed stats about chunk types and coverage."""
with closing(get_conn()) as conn:
Expand Down
35 changes: 26 additions & 9 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
from fastapi.testclient import TestClient

API_KEY = "test-secret-key"
METADATA_ENDPOINTS = [
("/api/projects", "projects"),
("/api/stats", "by_project"),
("/api/summary-stats", "total_chunks"),
]


@pytest.fixture()
Expand Down Expand Up @@ -112,21 +117,33 @@ def test_query_param_key_is_not_accepted(server_module):
assert resp_named.status_code == 401


def test_readonly_route_open_when_key_unset(server_module):
"""Read-only endpoints stay open when no key is configured."""
@pytest.mark.parametrize(("path", "response_key"), METADATA_ENDPOINTS)
def test_metadata_routes_open_when_key_unset(server_module, path, response_key):
"""Metadata endpoints stay open when no API key is configured."""
server_module.CODE_SEARCH_API_KEY = None
with _client(server_module) as client:
resp = client.get("/api/projects")
resp = client.get(path)
# Auth must not block; the handler returns its normal payload.
assert resp.status_code == 200
assert "projects" in resp.json()
assert response_key in resp.json()


def test_readonly_route_requires_key_when_configured(server_module):
"""When a key is set, read-only protected routes still validate it."""
@pytest.mark.parametrize(("path", "response_key"), METADATA_ENDPOINTS)
def test_metadata_routes_require_valid_key_when_configured(
server_module, path, response_key,
):
"""Configured API keys protect every project-metadata route."""
server_module.CODE_SEARCH_API_KEY = API_KEY
with _client(server_module) as client:
no_key = client.get("/api/projects")
good_key = client.get("/api/projects", headers={"X-API-Key": API_KEY})
no_key = client.get(path)
wrong_key = client.get(path, headers={"X-API-Key": "wrong"})
x_api_key = client.get(path, headers={"X-API-Key": API_KEY})
bearer = client.get(
path, headers={"Authorization": f"Bearer {API_KEY}"},
)
assert no_key.status_code == 401
assert good_key.status_code == 200
assert wrong_key.status_code == 401
assert x_api_key.status_code == 200
assert bearer.status_code == 200
assert response_key in x_api_key.json()
assert response_key in bearer.json()
Loading