feat: add local hardware approval leases
This commit is contained in:
@@ -15,6 +15,7 @@ from unittest.mock import patch
|
||||
from cognitive_agent.auth import AuthConfig, AuthRuntime, hash_password
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError
|
||||
from cognitive_agent.infrastructure.can.access import HardwareAccessController
|
||||
from cognitive_agent.server import SeatDemoHandler
|
||||
|
||||
|
||||
@@ -71,6 +72,8 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
cls.temporary_directory.cleanup()
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.original_hardware_access = self.service.hardware_access
|
||||
self.service.hardware_access = HardwareAccessController()
|
||||
self.cookie_jar = CookieJar()
|
||||
self.opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(self.cookie_jar)
|
||||
@@ -87,6 +90,9 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
self.assertTrue(payload["authenticated"])
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.service.hardware_access = self.original_hardware_access
|
||||
|
||||
def test_authentication_protects_console_and_api(self) -> None:
|
||||
unauthenticated = urllib.request.build_opener()
|
||||
with unauthenticated.open(f"{self.base_url}/", timeout=3) as response:
|
||||
@@ -105,7 +111,7 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
def test_static_console_and_runtime_endpoint(self) -> None:
|
||||
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-8", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-9", timeout=3) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
with self.opener.open(f"{self.base_url}/api/runtime", timeout=3) as response:
|
||||
runtime = json.loads(response.read().decode("utf-8"))
|
||||
@@ -115,7 +121,9 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
self.assertIn('data-vehicle-model="byd-tang-2024-reference"', html)
|
||||
self.assertIn("整车孪生界面", html)
|
||||
self.assertIn("透明车体 · 实体设备", html)
|
||||
self.assertIn('src="/app.js?v=seat-hil-8"', html)
|
||||
self.assertIn('src="/app.js?v=seat-hil-9"', html)
|
||||
self.assertIn('id="hardwareAccessField"', html)
|
||||
self.assertIn("hardware_access_required", app_script)
|
||||
self.assertIn("HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000", app_script)
|
||||
self.assertIn(
|
||||
"requestTimeoutForExecutionMode(state.executionMode)", app_script
|
||||
@@ -151,6 +159,13 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
runtime["hardware"]["concurrency_policy"],
|
||||
"single_flight_fail_fast",
|
||||
)
|
||||
self.assertEqual(
|
||||
runtime["hardware"]["access_policy"],
|
||||
"local_approval_idle_lease",
|
||||
)
|
||||
self.assertEqual(
|
||||
runtime["hardware"]["access_inactivity_timeout_seconds"], 180
|
||||
)
|
||||
self.assertFalse(runtime["hardware"]["busy"])
|
||||
self.assertEqual(
|
||||
runtime["hardware"]["supported_seat_instances"], ["passenger"]
|
||||
@@ -370,6 +385,8 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
synchronize.assert_called_once_with(
|
||||
"conv-e2e-physical-state",
|
||||
"denza_n7",
|
||||
hardware_client_id=None,
|
||||
hardware_access_bypass=True,
|
||||
)
|
||||
|
||||
def test_gpt_http_failure_falls_back_without_returning_502(self) -> None:
|
||||
@@ -524,6 +541,109 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
)
|
||||
self.assertNotIn("must-not-be-persisted", artifact_text)
|
||||
|
||||
def test_local_decision_point_selects_one_public_terminal_and_can_stop_it(self) -> None:
|
||||
client_a = "hwc-e2e-client-a1"
|
||||
client_b = "hwc-e2e-client-b2"
|
||||
|
||||
def post(path: str, payload: dict, *, public: bool = False):
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if public:
|
||||
headers["X-Forwarded-For"] = "203.0.113.25"
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}{path}",
|
||||
method="POST",
|
||||
headers=headers,
|
||||
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
||||
)
|
||||
return self.opener.open(request, timeout=3)
|
||||
|
||||
with post(
|
||||
"/api/hardware/access/request",
|
||||
{"client_id": client_a, "label": "用户电脑 A"},
|
||||
public=True,
|
||||
) as response:
|
||||
first = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(response.status, 202)
|
||||
with post(
|
||||
"/api/hardware/access/request",
|
||||
{"client_id": client_b, "label": "用户电脑 B"},
|
||||
public=True,
|
||||
) as response:
|
||||
second = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(first["queue_position"], 1)
|
||||
self.assertEqual(second["queue_position"], 2)
|
||||
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/local/hardware-access", timeout=3
|
||||
) as response:
|
||||
local_snapshot = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(len(local_snapshot["pending"]), 2)
|
||||
|
||||
with post(
|
||||
"/api/local/hardware-access/approve", {"client_id": client_b}
|
||||
) as response:
|
||||
approved = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(approved["active"]["client_id"], client_b)
|
||||
|
||||
with self.assertRaises(urllib.error.HTTPError) as remote_admin:
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/local/hardware-access",
|
||||
headers={"X-Forwarded-For": "203.0.113.25"},
|
||||
)
|
||||
self.opener.open(request, timeout=3)
|
||||
self.assertEqual(remote_admin.exception.code, 403)
|
||||
|
||||
def hardware_request(client_id: str, conversation: str):
|
||||
return post(
|
||||
"/api/requests",
|
||||
{
|
||||
"utterance": "关闭副驾座椅加热",
|
||||
"provider": "local",
|
||||
"conversation_id": conversation,
|
||||
"execution_mode": "hardware",
|
||||
"hardware_client_id": client_id,
|
||||
},
|
||||
public=True,
|
||||
)
|
||||
|
||||
fake_result = {"status": "succeeded", "request_id": "req-approved-client"}
|
||||
with patch.object(
|
||||
self.service, "_process_once", return_value=fake_result
|
||||
) as process_once:
|
||||
with hardware_request(client_b, "conv-approved-client") as response:
|
||||
self.assertEqual(response.status, 201)
|
||||
with self.assertRaises(urllib.error.HTTPError) as unapproved:
|
||||
hardware_request(client_a, "conv-unapproved-client")
|
||||
denied_payload = json.loads(unapproved.exception.read().decode("utf-8"))
|
||||
self.assertEqual(unapproved.exception.code, 403)
|
||||
self.assertEqual(denied_payload["error"], "hardware_access_required")
|
||||
self.assertEqual(denied_payload["access_state"], "pending")
|
||||
self.assertEqual(process_once.call_count, 1)
|
||||
|
||||
with post(
|
||||
"/api/hardware/access/release",
|
||||
{"client_id": client_b},
|
||||
public=True,
|
||||
) as response:
|
||||
released = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(released["state"], "none")
|
||||
self.assertIsNone(self.service.hardware_access.admin_snapshot()["active"])
|
||||
|
||||
def test_local_hardware_decision_page_is_not_exposed_through_proxy(self) -> None:
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/hardware-control", timeout=3
|
||||
) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
self.assertIn("真实台架本地决策台", html)
|
||||
|
||||
proxied = urllib.request.Request(
|
||||
f"{self.base_url}/hardware-control",
|
||||
headers={"X-Forwarded-For": "203.0.113.25"},
|
||||
)
|
||||
with self.assertRaises(urllib.error.HTTPError) as caught:
|
||||
self.opener.open(proxied, timeout=3)
|
||||
self.assertEqual(caught.exception.code, 403)
|
||||
|
||||
def test_overlapping_hardware_request_returns_conflict_without_entering_can(self) -> None:
|
||||
entered = threading.Event()
|
||||
release = threading.Event()
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from cognitive_agent.infrastructure.can.access import (
|
||||
HardwareAccessController,
|
||||
HardwareAccessRequiredError,
|
||||
)
|
||||
|
||||
|
||||
CLIENT_A = "hwc-aaaaaaaa-1111"
|
||||
CLIENT_B = "hwc-bbbbbbbb-2222"
|
||||
|
||||
|
||||
class FakeClock:
|
||||
def __init__(self) -> None:
|
||||
self.value = 100.0
|
||||
|
||||
def __call__(self) -> float:
|
||||
return self.value
|
||||
|
||||
def advance(self, seconds: float) -> None:
|
||||
self.value += seconds
|
||||
|
||||
|
||||
class HardwareAccessControllerTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.clock = FakeClock()
|
||||
self.controller = HardwareAccessController(clock=self.clock)
|
||||
|
||||
def test_local_admin_can_choose_one_of_multiple_pending_terminals(self) -> None:
|
||||
first = self.controller.request_access(CLIENT_A, "电脑 A", "203.0.113.10")
|
||||
second = self.controller.request_access(CLIENT_B, "电脑 B", "203.0.113.11")
|
||||
|
||||
self.assertEqual(first["queue_position"], 1)
|
||||
self.assertEqual(second["queue_position"], 2)
|
||||
snapshot = self.controller.approve(CLIENT_B)
|
||||
|
||||
self.assertEqual(snapshot["active"]["client_id"], CLIENT_B)
|
||||
self.assertEqual([item["client_id"] for item in snapshot["pending"]], [CLIENT_A])
|
||||
self.assertEqual(self.controller.status(CLIENT_B)["state"], "approved")
|
||||
self.assertEqual(self.controller.status(CLIENT_A)["state"], "pending")
|
||||
|
||||
def test_execution_refreshes_lease_and_idle_180_seconds_expires_it(self) -> None:
|
||||
self.controller.request_access(CLIENT_A, "电脑 A", "203.0.113.10")
|
||||
self.controller.approve(CLIENT_A)
|
||||
self.clock.advance(179)
|
||||
self.assertEqual(self.controller.status(CLIENT_A)["state"], "approved")
|
||||
|
||||
with self.controller.use(CLIENT_A):
|
||||
self.clock.advance(5)
|
||||
|
||||
self.clock.advance(179)
|
||||
self.assertEqual(self.controller.status(CLIENT_A)["state"], "approved")
|
||||
self.clock.advance(1)
|
||||
expired = self.controller.status(CLIENT_A)
|
||||
self.assertEqual(expired["state"], "expired")
|
||||
self.assertIsNone(self.controller.admin_snapshot()["active"])
|
||||
with self.assertRaises(HardwareAccessRequiredError) as caught:
|
||||
with self.controller.use(CLIENT_A):
|
||||
pass
|
||||
self.assertEqual(caught.exception.state, "expired")
|
||||
|
||||
def test_long_running_execution_is_not_revoked_while_it_is_active(self) -> None:
|
||||
self.controller.request_access(CLIENT_A, "电脑 A", "203.0.113.10")
|
||||
self.controller.approve(CLIENT_A)
|
||||
|
||||
with self.controller.use(CLIENT_A):
|
||||
self.clock.advance(600)
|
||||
status = self.controller.status(CLIENT_A)
|
||||
self.assertEqual(status["state"], "approved")
|
||||
self.assertTrue(status["active_execution"])
|
||||
|
||||
self.assertEqual(
|
||||
self.controller.status(CLIENT_A)["idle_remaining_seconds"], 180
|
||||
)
|
||||
|
||||
def test_user_release_requires_a_new_request(self) -> None:
|
||||
self.controller.request_access(CLIENT_A, "电脑 A", "203.0.113.10")
|
||||
self.controller.approve(CLIENT_A)
|
||||
self.assertEqual(self.controller.release(CLIENT_A)["state"], "none")
|
||||
with self.assertRaises(HardwareAccessRequiredError):
|
||||
with self.controller.use(CLIENT_A):
|
||||
pass
|
||||
|
||||
def test_approving_another_terminal_revokes_previous_lease(self) -> None:
|
||||
self.controller.request_access(CLIENT_A, "电脑 A", "203.0.113.10")
|
||||
self.controller.approve(CLIENT_A)
|
||||
self.controller.request_access(CLIENT_B, "电脑 B", "203.0.113.11")
|
||||
snapshot = self.controller.approve(CLIENT_B)
|
||||
|
||||
self.assertEqual(snapshot["active"]["client_id"], CLIENT_B)
|
||||
self.assertEqual(self.controller.status(CLIENT_A)["state"], "none")
|
||||
with self.assertRaises(HardwareAccessRequiredError):
|
||||
with self.controller.use(CLIENT_A):
|
||||
pass
|
||||
|
||||
def test_local_admin_can_deny_pending_and_revoke_active(self) -> None:
|
||||
self.controller.request_access(CLIENT_A, "电脑 A", "203.0.113.10")
|
||||
self.controller.deny(CLIENT_A)
|
||||
self.assertEqual(self.controller.status(CLIENT_A)["state"], "none")
|
||||
|
||||
self.controller.request_access(CLIENT_A, "电脑 A", "203.0.113.10")
|
||||
self.controller.approve(CLIENT_A)
|
||||
snapshot = self.controller.revoke(CLIENT_A)
|
||||
self.assertIsNone(snapshot["active"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user