Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.
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
58 changes: 55 additions & 3 deletions kora_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, Final, List, Optional, Tuple

import yaml

Expand Down Expand Up @@ -3735,22 +3735,74 @@ def _boot_status_stub(*, error: str) -> dict:
}


# KR-FE-TENANT-PICKER-COCKPIT-CHROME — query-param name pinned by
# drift-guard. FE references the same literal via
# TENANT_ID_QUERY_PARAM in web/src/hooks/useActiveTenant.ts; the
# contract test in tests/test_tenants_endpoint.py asserts equality.
TENANT_ID_QUERY_PARAM_NAME: Final[str] = "tenant_id"


@app.get("/api/tenants/list")
async def list_tenants():
"""Return the tenant_ids the cost-state holder currently knows about.

Wraps :func:`agent.cost_state_holder.list_cost_holder_tenants`.
Always includes the canonical ``"default"`` tenant even when the
holder registry is empty, so the FE tenant-picker can render at
least the single-tenant option from boot. Sorted; "default" first
when present, then the rest alphabetically.
"""
try:
from agent.cost_state_holder import (
DEFAULT_TENANT_ID,
list_cost_holder_tenants,
)
except Exception as exc:
_log.warning(
"[kora.tenants] cost_state_holder import failed: %r — "
"returning default-only",
exc,
)
return {"tenants": ["default"]}

try:
registered = list(list_cost_holder_tenants())
except Exception as exc:
_log.warning(
"[kora.tenants] list_cost_holder_tenants raised %r — "
"returning default-only",
exc,
)
return {"tenants": [DEFAULT_TENANT_ID]}

if DEFAULT_TENANT_ID not in registered:
registered.append(DEFAULT_TENANT_ID)
# default-first, rest sorted — stable render order for the picker.
rest = sorted(t for t in registered if t != DEFAULT_TENANT_ID)
return {"tenants": [DEFAULT_TENANT_ID, *rest]}


@app.get("/api/cost-state")
async def get_cost_state():
async def get_cost_state(tenant_id: Optional[str] = None):
"""Return Kora's current cost-ladder state.

Live read via ``get_cost_state_summary`` (KR-P2-COST-FLIP). When
the CostStateHolder isn't initialised, the IsoKron provider isn't
registered, or the substrate read fails, returns the same shape
with ``stub: True`` + an ``error`` field so the FE keeps rendering
and the operator sees the cause.

``tenant_id`` (KR-FE-TENANT-PICKER-COCKPIT-CHROME): when present,
resolves a per-tenant holder via ``get_cost_holder(tenant_id)``.
Omitted → binds to ``DEFAULT_TENANT_ID`` — pre-#202 behavior
preserved exactly for legacy callers.
"""
try:
from agent.cost_state_holder import get_cost_holder
from agent.cost_state_summary import get_cost_state_summary
from plugins.memory.isokron import get_last_active_provider

cost_holder = get_cost_holder()
cost_holder = get_cost_holder(tenant_id=tenant_id)
provider = get_last_active_provider()
if cost_holder is None:
return {
Expand Down
168 changes: 168 additions & 0 deletions tests/test_tenants_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""KR-FE-TENANT-PICKER-COCKPIT-CHROME — /api/tenants/list + the
cost-state ?tenant_id= passthrough.

Drift-guard pin: the BE-side ``TENANT_ID_QUERY_PARAM_NAME`` literal
must stay equal to the FE-side ``TENANT_ID_QUERY_PARAM`` literal in
``web/src/hooks/useActiveTenant.ts``. Renaming one without the other
silently breaks the cockpit's tenant-scoped reads. The asserts at
the bottom of this file fail loudly the moment they diverge.
"""

from __future__ import annotations

from pathlib import Path

import pytest

from agent.cost_state_holder import (
DEFAULT_TENANT_ID,
_reset_cost_holder_for_tests,
init_cost_holder,
)


@pytest.fixture(autouse=True)
def _reset_holders():
_reset_cost_holder_for_tests()
yield
_reset_cost_holder_for_tests()


@pytest.fixture
def client(monkeypatch, tmp_path):
try:
from starlette.testclient import TestClient
except ImportError:
pytest.skip("fastapi/starlette not installed")

monkeypatch.setenv("KORA_HOME", str(tmp_path))
monkeypatch.setenv("HERMES_HOME", str(tmp_path))

from kora_cli.web_server import app, _SESSION_HEADER_NAME, _SESSION_TOKEN

c = TestClient(app)
c.headers[_SESSION_HEADER_NAME] = _SESSION_TOKEN
return c


# ---------------------------------------------------------------------------
# /api/tenants/list
# ---------------------------------------------------------------------------


def test_tenants_list_default_only_when_registry_empty(client):
"""Empty holder registry → returns ["default"] anyway. Picker
needs at least the canonical option so the FE never crashes on
the single-tenant fresh-install path."""
resp = client.get("/api/tenants/list")
assert resp.status_code == 200
data = resp.json()
assert data == {"tenants": ["default"]}


def test_tenants_list_returns_default_first(client):
"""default-first, rest sorted — stable render order for the
sidebar dropdown so an alphabetically-earlier tenant (alpha)
doesn't bump 'default' down the list."""
init_cost_holder(tenant_id="zeta", credit_pool_usd=10.0)
init_cost_holder(tenant_id="alpha", credit_pool_usd=10.0)
init_cost_holder(tenant_id=DEFAULT_TENANT_ID, credit_pool_usd=10.0)

resp = client.get("/api/tenants/list")
assert resp.status_code == 200
assert resp.json() == {"tenants": ["default", "alpha", "zeta"]}


def test_tenants_list_synthesizes_default_when_only_named_tenants(client):
"""If operators init only named tenants (no explicit "default"),
the picker still needs the canonical default option — synthesize
it. The cost-holder registry doesn't auto-create "default" on
init."""
init_cost_holder(tenant_id="marvin", credit_pool_usd=10.0)

resp = client.get("/api/tenants/list")
assert resp.json() == {"tenants": ["default", "marvin"]}


# ---------------------------------------------------------------------------
# /api/cost-state passthrough
# ---------------------------------------------------------------------------


def test_cost_state_tenant_id_routes_to_per_tenant_holder(client):
"""``?tenant_id=marvin`` → resolves the marvin holder. With no
isokron provider registered the response is a stub, but the
stub-vs-real branch isn't what's under test here — what matters
is that the holder lookup happens against the named tenant and
not against ``default``."""
init_cost_holder(tenant_id="marvin", credit_pool_usd=42.0)

resp = client.get("/api/cost-state?tenant_id=marvin")
# The provider-not-registered branch returns stub:True; that's
# fine. The key assertion is that the call succeeded (200) and
# routed through the per-tenant holder accessor without raising.
assert resp.status_code == 200
body = resp.json()
assert "current" in body # full shape preserved


def test_cost_state_no_tenant_id_preserves_legacy_default_behavior(client):
"""Omitting the param ≡ pre-#202 single-tenant behavior."""
resp = client.get("/api/cost-state")
assert resp.status_code == 200
body = resp.json()
assert "current" in body


# ---------------------------------------------------------------------------
# Drift-guard pins
# ---------------------------------------------------------------------------


def test_tenant_id_query_param_name_pin():
"""BE-side literal ``TENANT_ID_QUERY_PARAM_NAME`` matches the
documented contract ``"tenant_id"``. Cross-stack test_tenant_id_*
asserts the FE literal matches; this side asserts the BE literal
matches the contract. Both sides must equal "tenant_id"."""
from kora_cli.web_server import TENANT_ID_QUERY_PARAM_NAME

assert TENANT_ID_QUERY_PARAM_NAME == "tenant_id"


def test_fe_useActiveTenant_pins_match_be_constants():
"""Grep the FE hook source to ensure the FE literal is equal to
the BE literal. Cross-stack pin so a rename on either side fails
the BE suite (FE has no vitest in this repo today).

Repo layout: this test lives at <repo>/tests/, the FE hook lives
at <repo>/web/src/hooks/useActiveTenant.ts.
"""
hook_path = (
Path(__file__).parent.parent
/ "web"
/ "src"
/ "hooks"
/ "useActiveTenant.ts"
)
src = hook_path.read_text(encoding="utf-8")

# Pin: the FE storage key + URL param literal + default tenant id.
assert 'TENANT_PICKER_STORAGE_KEY = "kora_active_tenant"' in src
assert 'TENANT_ID_QUERY_PARAM = "tenant"' in src
assert 'DEFAULT_TENANT_ID = "default"' in src
# Pin: the FE forwards the BE param under the BE literal name.
# Search across api.ts (the audit-query builder + per-endpoint
# calls) — anywhere is fine; the literal must appear at least
# once via the BE name "tenant_id".
api_path = (
Path(__file__).parent.parent
/ "web"
/ "src"
/ "lib"
/ "api.ts"
)
api_src = api_path.read_text(encoding="utf-8")
assert 'qs.set("tenant_id"' in api_src or '"tenant_id"' in api_src, (
"FE must forward the BE-pinned literal 'tenant_id' as the "
"query-param name; got no occurrence in web/src/lib/api.ts"
)
5 changes: 5 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { cn } from "@/lib/utils";
import { Backdrop } from "@/components/Backdrop";
import { SidebarFooter } from "@/components/SidebarFooter";
import { SidebarStatusStrip } from "@/components/SidebarStatusStrip";
import { TenantPicker } from "@/components/TenantPicker";
import { PageHeaderProvider } from "@/contexts/PageHeaderProvider";
import { useSystemActions } from "@/contexts/useSystemActions";
import type { SystemAction } from "@/contexts/system-actions-context";
Expand Down Expand Up @@ -837,6 +838,10 @@ export default function App() {
</Button>
</div>

{/* KR-FE-TENANT-PICKER-COCKPIT-CHROME — sidebar header
slot. Renders nothing unless ≥2 tenants observed. */}
<TenantPicker />

<nav
className="min-h-0 w-full flex-1 overflow-y-auto overflow-x-hidden border-t border-current/10 py-2"
aria-label={t.app.navigation}
Expand Down
Loading
Loading