petter2025 commited on
Commit
0fea413
·
1 Parent(s): 8aeaefb

Upload folder using huggingface_hub

Browse files
Dockerfile CHANGED
@@ -1,16 +1,52 @@
 
1
  # ---- deps stage: needs git + a credentialed clone of the private ARF repos
2
  # (agentic_reliability_framework, ARF-Bayesian-Pricing-Calculator).
3
  # This stage is discarded after build -- the credential never reaches
4
  # the final image's layers, env, or git config. ----
 
 
 
 
 
 
 
 
 
 
5
  FROM python:3.12-slim AS deps
6
- ARG GH_PAT
7
  RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
8
- RUN git config --global url."https://${GH_PAT}@github.com/".insteadOf "https://github.com/"
 
9
  RUN python -m venv /opt/venv
10
  ENV PATH="/opt/venv/bin:$PATH"
11
  WORKDIR /app
12
  COPY requirements.txt .
13
- RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  # ---- final stage: just the built venv + app code, no git, no credential ----
16
  FROM python:3.12-slim
@@ -18,4 +54,11 @@ COPY --from=deps /opt/venv /opt/venv
18
  ENV PATH="/opt/venv/bin:$PATH"
19
  WORKDIR /app
20
  COPY . .
21
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1.2
2
  # ---- deps stage: needs git + a credentialed clone of the private ARF repos
3
  # (agentic_reliability_framework, ARF-Bayesian-Pricing-Calculator).
4
  # This stage is discarded after build -- the credential never reaches
5
  # the final image's layers, env, or git config. ----
6
+ #
7
+ # GH_PAT is read via a BuildKit secret mount, not `ARG` -- an ARG's value is
8
+ # printed in plaintext as part of the logged RUN command that uses it (this
9
+ # is exactly how a real, live token ended up visible in a Render deploy log
10
+ # this session). A secret mount's value is never written to a log line or
11
+ # an image layer. REQUIRES a matching setup step in Render's dashboard
12
+ # before this will build: Render's Docker service settings -> Secret Files
13
+ # -> add a file named exactly `gh_pat` containing the token value (nothing
14
+ # else in the file). The old `GH_PAT` environment variable is no longer
15
+ # read by this Dockerfile and can be removed once this is confirmed working.
16
  FROM python:3.12-slim AS deps
 
17
  RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
18
+ RUN --mount=type=secret,id=gh_pat,dst=/etc/secrets/gh_pat \
19
+ git config --global url."https://$(cat /etc/secrets/gh_pat)@github.com/".insteadOf "https://github.com/"
20
  RUN python -m venv /opt/venv
21
  ENV PATH="/opt/venv/bin:$PATH"
22
  WORKDIR /app
23
  COPY requirements.txt .
24
+ # torch has no explicit pin anywhere in this dependency tree -- it's pulled in
25
+ # transitively by sentence-transformers (for agentic_reliability_framework's
26
+ # RAG/semantic-memory features) and, left to the default PyPI index, resolves
27
+ # to the CUDA-enabled build (nvidia-cusparselt, cuda-toolkit, nvidia-nccl, ...)
28
+ # even though this service runs on CPU-only Render instances. That variant's
29
+ # extra weight is a real contributor to out-of-memory deploy failures.
30
+ #
31
+ # A separate `pip install torch==... --index-url .../cpu` RUN before this one
32
+ # does NOT work: it's a distinct resolve that only knows about the CPU wheel;
33
+ # the very next `pip install -r requirements.txt`, seeing no --index-url, only
34
+ # has the default PyPI index in view and re-resolves torch from there,
35
+ # silently replacing the CPU build with the CUDA one at the same version
36
+ # number (confirmed happening in a real deploy -- final `pip install` log
37
+ # showed plain `torch-2.13.0` plus the full nvidia/cuda-toolkit/triton stack,
38
+ # not `torch-2.13.0+cpu`). Putting torch and -r requirements.txt in one
39
+ # `pip install` call, with the CPU wheelhouse as the primary --index-url and
40
+ # PyPI as --extra-index-url, makes it a single resolve: torch is satisfied
41
+ # from the CPU index and nothing later re-derives a different build for it.
42
+ # Version pinned to 2.13.0 to match exactly what pip's resolver already chose
43
+ # for this dependency tree (confirmed available on the CPU index for
44
+ # cp312/manylinux before pinning it here, not assumed).
45
+ RUN pip install --no-cache-dir \
46
+ --index-url https://download.pytorch.org/whl/cpu \
47
+ --extra-index-url https://pypi.org/simple \
48
+ torch==2.13.0 \
49
+ -r requirements.txt
50
 
51
  # ---- final stage: just the built venv + app code, no git, no credential ----
52
  FROM python:3.12-slim
 
54
  ENV PATH="/opt/venv/bin:$PATH"
55
  WORKDIR /app
56
  COPY . .
57
+ # Shell form (not exec/JSON-array form) deliberately -- ${PORT:-7860} only
58
+ # expands with a real shell interpreting the command; exec form passes
59
+ # arguments literally with no variable substitution at all. Render injects
60
+ # PORT and expects the app to bind to it (its deploy log explicitly failed
61
+ # port-scanning for it: "Bind your service to at least one port"); the
62
+ # Hugging Face Space mirror sets no such variable and expects the
63
+ # conventional default, 7860. One image, correct on both targets.
64
+ CMD uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}
README.md CHANGED
@@ -1,16 +1,31 @@
 
 
 
 
 
 
 
 
 
1
  # arf-api
2
 
3
  ARF API Control Plane (FastAPI)
4
 
5
  ## Live Demo
6
 
7
- The API is deployed and accessible at:
8
- - **Base URL**: [https://arf-ai-agentic-reliability-framework-api.hf.space](https://arf-ai-agentic-reliability-framework-api.hf.space)
 
 
 
 
 
9
  - **Interactive Documentation**: [https://arf-ai-agentic-reliability-framework-api.hf.space/docs](https://arf-ai-agentic-reliability-framework-api.hf.space/docs)
10
 
11
  ## Quick Start (Local Development)
12
 
13
  1. **Install dependencies**:
 
14
  ```bash
15
  pip install -r requirements.txt
16
  ```
@@ -91,9 +106,7 @@ curl -X POST "http://localhost:8000/api/v1/v1/incidents/evaluate" -H "Content-
91
  "effect": -90,
92
  "explanation_text": "If we apply restart_container instead of no_action, latency would change from 600.00 to 510.00 (Δ = -90.00). Based on heuristic causal model.",
93
  "is_model_based": false,
94
- "warnings": [
95
- "Using heuristic causal model (no fitted SCM)."
96
- ]
97
  },
98
  "utility_decision": {
99
  "best_action": "restart_container",
@@ -120,4 +133,3 @@ Notes
120
 
121
  - The governance endpoints use an in-process `RiskEngine` initialized at startup.
122
  - Outcomes are recorded via `POST /api/v1/intents/outcome` (`app/api/routes_governance.py`), tenant-scoped and auth-protected.
123
-
 
1
+ ---
2
+ title: ARF API
3
+ emoji: 🛡️
4
+ colorFrom: blue
5
+ colorTo: gray
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
  # arf-api
11
 
12
  ARF API Control Plane (FastAPI)
13
 
14
  ## Live Demo
15
 
16
+ **Render is the primary deployment target** (custom domain, real scaling, standard secrets
17
+ management -- the multi-stage Docker build in this repo was purpose-built for it). The Hugging
18
+ Face Space below is a secondary, publicly-browsable mirror of the same code, not the primary
19
+ integration target -- point real pilot/customer integrations at Render once its URL is
20
+ confirmed live, not at the Space URL.
21
+
22
+ - **HF Space (public mirror)**: [https://arf-ai-agentic-reliability-framework-api.hf.space](https://arf-ai-agentic-reliability-framework-api.hf.space)
23
  - **Interactive Documentation**: [https://arf-ai-agentic-reliability-framework-api.hf.space/docs](https://arf-ai-agentic-reliability-framework-api.hf.space/docs)
24
 
25
  ## Quick Start (Local Development)
26
 
27
  1. **Install dependencies**:
28
+
29
  ```bash
30
  pip install -r requirements.txt
31
  ```
 
106
  "effect": -90,
107
  "explanation_text": "If we apply restart_container instead of no_action, latency would change from 600.00 to 510.00 (Δ = -90.00). Based on heuristic causal model.",
108
  "is_model_based": false,
109
+ "warnings": ["Using heuristic causal model (no fitted SCM)."]
 
 
110
  },
111
  "utility_decision": {
112
  "best_action": "restart_container",
 
133
 
134
  - The governance endpoints use an in-process `RiskEngine` initialized at startup.
135
  - Outcomes are recorded via `POST /api/v1/intents/outcome` (`app/api/routes_governance.py`), tenant-scoped and auth-protected.
 
app/api/routes_history.py CHANGED
@@ -1,7 +1,8 @@
1
- from fastapi import APIRouter
 
2
  from app.core.storage import incident_history
3
 
4
- router = APIRouter()
5
 
6
 
7
  @router.get("/history")
 
1
+ from fastapi import APIRouter, Depends
2
+ from app.api.deps import verify_internal_key
3
  from app.core.storage import incident_history
4
 
5
+ router = APIRouter(dependencies=[Depends(verify_internal_key)])
6
 
7
 
8
  @router.get("/history")
app/api/routes_intents.py CHANGED
@@ -1,8 +1,9 @@
1
- from fastapi import APIRouter, HTTPException
 
2
  from app.models.intent_models import IntentSimulation, IntentSimulationResponse
3
  from app.services.intent_service import simulate_intent
4
 
5
- router = APIRouter()
6
 
7
 
8
  @router.post("/simulate_intent", response_model=IntentSimulationResponse)
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from app.api.deps import verify_internal_key
3
  from app.models.intent_models import IntentSimulation, IntentSimulationResponse
4
  from app.services.intent_service import simulate_intent
5
 
6
+ router = APIRouter(dependencies=[Depends(verify_internal_key)])
7
 
8
 
9
  @router.post("/simulate_intent", response_model=IntentSimulationResponse)
app/api/routes_memory.py CHANGED
@@ -1,6 +1,7 @@
1
- from fastapi import APIRouter, Request
 
2
 
3
- router = APIRouter()
4
 
5
 
6
  @router.get("/stats")
 
1
+ from fastapi import APIRouter, Depends, Request
2
+ from app.api.deps import verify_internal_key
3
 
4
+ router = APIRouter(dependencies=[Depends(verify_internal_key)])
5
 
6
 
7
  @router.get("/stats")
app/api/routes_pricing.py CHANGED
@@ -57,48 +57,26 @@ async def run_pricing(
57
  quota: dict = Depends(enforce_quota),
58
  ):
59
  """
60
- Multi‑run pricing with cooldown and buffer persistence.
61
- Each run’s simulated outcome is added to the buffer, so subsequent runs
62
- see an updated posterior.
 
 
 
 
 
 
 
 
 
 
 
63
  """
64
- # We need to reuse the same buffer across runs; we'll load it per request.
65
- # For simplicity, we'll load from the default location.
66
- from arf_pricing_calculator.storage.buffer import load_buffer, add_event
67
- from arf_pricing_calculator.orchestration.cooldown import enforce_cooldown, is_cooldown_active
68
-
69
- outputs = []
70
- buffer = load_buffer() # loads from calibration_buffer.json
71
-
72
- for i in range(req.runs):
73
- if not req.force and is_cooldown_active(
74
- req.customer_id, req.cooldown_hours):
75
- raise HTTPException(status_code=429,
76
- detail=f"Cooldown active after {i} runs")
77
-
78
- pricing_input = parse_input_dict(req.input)
79
- engine = PricingEngine(calibration_buffer=buffer)
80
- out = engine.estimate(pricing_input)
81
-
82
- # Simulate an outcome (in real use, this would come from the actual
83
- # deal)
84
- import random
85
- outcome = "success" if random.random() > out.risk_score else "failure" # nosec B311
86
-
87
- event = {
88
- "run_id": out.run_history_id,
89
- "customer_id": req.customer_id,
90
- "outcome": outcome,
91
- "price": out.recommended_price,
92
- "value": out.expected_value,
93
- "risk_score": out.risk_score,
94
- "run_number": i + 1,
95
- }
96
- add_event(event)
97
- buffer = load_buffer() # reload after update
98
-
99
- outputs.append(out)
100
-
101
- if i < req.runs - 1:
102
- enforce_cooldown(req.customer_id, req.cooldown_hours)
103
-
104
- return outputs
 
57
  quota: dict = Depends(enforce_quota),
58
  ):
59
  """
60
+ Multi‑run pricing with cooldown and buffer persistence. TEMPORARILY DISABLED.
61
+
62
+ This endpoint used to persist each run's "outcome" as
63
+ `random.random() > risk_score` -- a fabricated result, not a real deal
64
+ outcome -- into a calibration buffer with no customer_id scoping, so
65
+ every customer's calls read and wrote the same file. Net effect: every
66
+ customer's price was shaped by every other customer's randomly-generated
67
+ outcomes, not just their own. Disabled until both are fixed: (1) a real
68
+ outcome-ingestion path (this endpoint must not invent one), and (2) the
69
+ buffer scoped per customer. See AUDIT_arf-bayesian-pricing-calculator.md
70
+ and AUDIT_arf-api.md (workspace root) for the original findings and
71
+ recommended fix. `Depends(enforce_quota)` stays active so this still
72
+ requires the same auth it always did -- only authenticated callers reach
73
+ the disabled-notice below; everyone else still gets the normal 401/403.
74
  """
75
+ raise HTTPException(
76
+ status_code=503,
77
+ detail=(
78
+ "This endpoint is temporarily disabled while a data-integrity issue is "
79
+ "fixed. Use POST /api/v1/pricing/estimate for a single price estimate "
80
+ "with no persisted learning in the meantime."
81
+ ),
82
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/routes_risk.py CHANGED
@@ -1,8 +1,9 @@
1
- from fastapi import APIRouter, HTTPException
 
2
  from app.models.risk_models import RiskResponse
3
  from app.services.risk_service import get_system_risk
4
 
5
- router = APIRouter()
6
 
7
 
8
  @router.get("/get_risk", response_model=RiskResponse)
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from app.api.deps import verify_internal_key
3
  from app.models.risk_models import RiskResponse
4
  from app.services.risk_service import get_system_risk
5
 
6
+ router = APIRouter(dependencies=[Depends(verify_internal_key)])
7
 
8
 
9
  @router.get("/get_risk", response_model=RiskResponse)
docs/authentication.md CHANGED
@@ -2,20 +2,36 @@
2
 
3
  This page describes how to authenticate with the ARF API.
4
 
5
- Current status (mixed — not all routers are protected)
6
-
7
- - `routes_governance.py`: the entire router requires the `X-Internal-Key` header, verified against `ARF_INTERNAL_API_KEY` (`app/api/deps.py::verify_internal_key`). This fails closed — requests are rejected with 401 if the env var is unset. This is the header arf-gateway injects when proxying to this service.
8
- - `routes_admin.py`: individual `/admin/*` endpoints require an `admin_key` query parameter, verified against `ARF_ADMIN_API_KEY` (`app/api/deps.py`, or the local `verify_admin` dependency in that router). Also fails closed if unset.
9
- - `routes_risk.py`, `routes_intents.py`, `routes_history.py`, `routes_memory.py`: **no auth dependency at all**. If this service is reachable directly (e.g., its public HF Space URL) rather than only through arf-gateway, these routes are open to anyone.
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  What the code provides
12
 
13
- - `app/core/config.py` exposes an `api_key` setting read from `.env`, but no current route checks it — it is not the mechanism in use. The real mechanism is the two env vars above, checked in `app/api/deps.py`.
14
-
15
- If you need to lock down the remaining unauthenticated routers
16
-
17
- - Add `dependencies=[Depends(verify_internal_key)]` (or a purpose-built dependency) to the `APIRouter(...)` construction in the files listed above, following the pattern already used in `routes_governance.py`.
18
 
19
  Notes
20
 
21
  - Tests run against a real Postgres connection (`tests/conftest.py`), not SQLite; see the top-level README's Tests section.
 
 
 
 
 
2
 
3
  This page describes how to authenticate with the ARF API.
4
 
5
+ Current status
6
+
7
+ - `routes_governance.py`, `routes_risk.py`, `routes_intents.py`, `routes_history.py`,
8
+ `routes_memory.py`: the entire router requires the `X-Internal-Key` header, verified against
9
+ `ARF_INTERNAL_API_KEY` (`app/api/deps.py::verify_internal_key`). This fails closed requests
10
+ are rejected with 401 if the env var is unset, if the header is missing, or if it doesn't
11
+ match (constant-time comparison). This is the header arf-gateway injects when proxying to
12
+ this service. The last four were unauthenticated until this was fixed — see
13
+ `tests/test_deps.py` for the tests that verify the dependency itself actually rejects what
14
+ it should, not just that it's wired in.
15
+ - `routes_admin.py`: individual `/admin/*` endpoints require an `admin_key` query parameter,
16
+ verified against `ARF_ADMIN_API_KEY` (`app/api/deps.py`, or the local `verify_admin`
17
+ dependency in that router). Also fails closed if unset.
18
+ - `routes_pricing.py`: individual `/pricing/*` endpoints require a real per-customer API key
19
+ (`Authorization: Bearer <key>` or `?api_key=`), verified against the tracked/tenant-scoped
20
+ `enforce_quota` dependency (`app/core/usage_tracker.py`) — a different mechanism from
21
+ `X-Internal-Key`, since pricing estimates are meant to be reachable by a customer directly,
22
+ not only via the gateway.
23
 
24
  What the code provides
25
 
26
+ - `app/core/config.py` exposes an `api_key` setting read from `.env`, but no current route
27
+ checks it — it is not the mechanism in use. The real mechanisms are `X-Internal-Key`,
28
+ `ARF_ADMIN_API_KEY`, and per-customer API keys (`enforce_quota`), all checked in
29
+ `app/api/deps.py` / `app/core/usage_tracker.py`.
 
30
 
31
  Notes
32
 
33
  - Tests run against a real Postgres connection (`tests/conftest.py`), not SQLite; see the top-level README's Tests section.
34
+ - `tests/conftest.py` globally overrides `verify_internal_key` for the test suite (so routes
35
+ behind it can be exercised without the gateway-injected header) — this means the app-level
36
+ test suite alone can't confirm the dependency actually fails closed. `tests/test_deps.py`
37
+ calls it directly, bypassing that override, specifically to verify that.
tests/test_deps.py CHANGED
@@ -1,5 +1,10 @@
 
 
 
1
  import pytest
2
- from unittest.mock import patch, MagicMock
 
 
3
  from app.api.deps import get_db
4
 
5
 
@@ -13,3 +18,72 @@ def test_get_db_closes_session():
13
  with pytest.raises(Exception):
14
  db_gen.throw(Exception("test error"))
15
  mock_session.close.assert_called_once()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+ from unittest.mock import MagicMock, patch
3
+
4
  import pytest
5
+ from fastapi import HTTPException
6
+
7
+ import app.api.deps as deps
8
  from app.api.deps import get_db
9
 
10
 
 
18
  with pytest.raises(Exception):
19
  db_gen.throw(Exception("test error"))
20
  mock_session.close.assert_called_once()
21
+
22
+
23
+ # verify_internal_key tests below are called directly, not through
24
+ # TestClient: tests/conftest.py globally overrides verify_internal_key
25
+ # (`fastapi_app.dependency_overrides[verify_internal_key] = mock_verify_internal_key`)
26
+ # so that already-protected routes (routes_governance.py) can be exercised
27
+ # in tests without the gateway-injected X-Internal-Key header. That override
28
+ # makes the real fail-closed behavior untestable through the app for any
29
+ # router that uses it -- this is the only place it's actually verified to
30
+ # reject what it should reject, rather than just trusted to work because
31
+ # it's wired in.
32
+ #
33
+ # Newly relevant as of the auth fix to routes_risk.py, routes_intents.py,
34
+ # routes_history.py, routes_memory.py (see docs/authentication.md) -- those
35
+ # four routers now depend on this function passing correctly.
36
+
37
+ _NEWLY_PROTECTED_ROUTER_MODULES = [
38
+ "app.api.routes_risk",
39
+ "app.api.routes_intents",
40
+ "app.api.routes_history",
41
+ "app.api.routes_memory",
42
+ ]
43
+
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_verify_internal_key_rejects_missing_header(monkeypatch):
47
+ monkeypatch.setattr(deps, "INTERNAL_API_KEY", "real-secret")
48
+ with pytest.raises(HTTPException) as exc_info:
49
+ await deps.verify_internal_key(x_internal_key=None)
50
+ assert exc_info.value.status_code == 401
51
+
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_verify_internal_key_rejects_wrong_key(monkeypatch):
55
+ monkeypatch.setattr(deps, "INTERNAL_API_KEY", "real-secret")
56
+ with pytest.raises(HTTPException) as exc_info:
57
+ await deps.verify_internal_key(x_internal_key="wrong-key")
58
+ assert exc_info.value.status_code == 401
59
+
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_verify_internal_key_fails_closed_when_unset(monkeypatch):
63
+ """The env var being unset must reject every request, not let them
64
+ through -- this is the specific property that makes this safe to add
65
+ to a router without also needing to guarantee the env var is always
66
+ set."""
67
+ monkeypatch.setattr(deps, "INTERNAL_API_KEY", "")
68
+ with pytest.raises(HTTPException) as exc_info:
69
+ await deps.verify_internal_key(x_internal_key="anything")
70
+ assert exc_info.value.status_code == 401
71
+
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_verify_internal_key_accepts_correct_key(monkeypatch):
75
+ monkeypatch.setattr(deps, "INTERNAL_API_KEY", "real-secret")
76
+ result = await deps.verify_internal_key(x_internal_key="real-secret")
77
+ assert result is None
78
+
79
+
80
+ @pytest.mark.parametrize("router_module_name", _NEWLY_PROTECTED_ROUTER_MODULES)
81
+ def test_router_requires_verify_internal_key(router_module_name):
82
+ """Structural check, independent of the function-level tests above:
83
+ proves each router actually declares verify_internal_key as a
84
+ router-level dependency, not just that the function itself works in
85
+ isolation. Mirrors the pattern routes_governance.py already uses
86
+ (`APIRouter(dependencies=[Depends(verify_internal_key)])`)."""
87
+ module = importlib.import_module(router_module_name)
88
+ dependency_callables = [d.dependency for d in module.router.dependencies]
89
+ assert deps.verify_internal_key in dependency_callables
tests/test_routes_pricing.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.testclient import TestClient
2
+ from app.main import app
3
+
4
+ client = TestClient(app)
5
+
6
+
7
+ def test_run_pricing_is_temporarily_disabled():
8
+ """/pricing/run was disabled after AUDIT_arf-bayesian-pricing-calculator.md's
9
+ Critical finding: it persisted a fabricated random.random() outcome into a
10
+ calibration buffer with no customer_id scoping, so every customer's price
11
+ was shaped by every other customer's fabricated outcomes. This asserts the
12
+ disabled state itself, not the old (buggy) behavior -- update this test
13
+ when the endpoint is actually fixed and re-enabled, not before."""
14
+ response = client.post(
15
+ "/api/v1/pricing/run",
16
+ json={"input": {}, "customer_id": "test-customer", "runs": 1},
17
+ )
18
+ assert response.status_code == 503
19
+ assert "temporarily disabled" in response.json()["detail"].lower()
20
+
21
+
22
+ def test_run_pricing_still_requires_auth():
23
+ """Disabling the endpoint must not also disable its auth -- conftest.py's
24
+ mock_enforce_quota makes every request "authenticated" for this test
25
+ client, so this only confirms the Depends(enforce_quota) dependency is
26
+ still declared and still runs before the handler body (i.e. it wasn't
27
+ accidentally dropped along with the rest of the function body); it does
28
+ not exercise the real 401/403 paths, which are covered by test_deps.py
29
+ and usage_tracker's own tests."""
30
+ response = client.post(
31
+ "/api/v1/pricing/run",
32
+ json={"input": {}, "customer_id": "test-customer", "runs": 1},
33
+ )
34
+ # Reaching the 503 (not erroring before it) proves enforce_quota resolved
35
+ # successfully -- if the dependency were missing or broken, this would be
36
+ # a 401/422/500 instead.
37
+ assert response.status_code == 503
38
+
39
+
40
+ def test_estimate_pricing_route_still_registered():
41
+ """/pricing/estimate was not touched by the disable -- confirm it's still
42
+ a distinct, reachable route (a malformed request should 400, not 503/404),
43
+ so a caller following the "use /pricing/estimate instead" guidance in the
44
+ 503 detail message actually has somewhere to go."""
45
+ response = client.post("/api/v1/pricing/estimate", json={"input": {}})
46
+ assert response.status_code != 503
47
+ assert response.status_code != 404