352 lines
14 KiB
Python
352 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import unittest
|
|
|
|
from cognitive_agent.application.conversation_state import ConversationStateStore
|
|
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
|
APP_SOURCE_PATH = REPOSITORY_ROOT / "apps" / "web-console" / "app.js"
|
|
EXTENSION_SOURCE_PATH = (
|
|
REPOSITORY_ROOT / "apps" / "web-console" / "self-extension.js"
|
|
)
|
|
|
|
|
|
def function_source(source: str, signature: str, next_signature: str) -> str:
|
|
start = source.index(signature)
|
|
end = source.index(next_signature, start)
|
|
return source[start:end]
|
|
|
|
|
|
class FrontendVehicleStateSyncContractTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.app_source = APP_SOURCE_PATH.read_text(encoding="utf-8")
|
|
cls.extension_source = EXTENSION_SOURCE_PATH.read_text(encoding="utf-8")
|
|
|
|
def test_both_modules_use_the_same_browser_conversation(self) -> None:
|
|
key = "cognitive-agent.conversation-id"
|
|
self.assertIn(
|
|
f'sessionStorage.getItem("{key}")',
|
|
self.app_source,
|
|
)
|
|
self.assertIn(
|
|
f'sessionStorage.setItem("{key}", conversationId)',
|
|
self.app_source,
|
|
)
|
|
self.assertIn(
|
|
f'const CONVERSATION_KEY = "{key}"',
|
|
self.extension_source,
|
|
)
|
|
self.assertIn(
|
|
"body: JSON.stringify({ command, conversation_id: sharedConversationId() })",
|
|
self.extension_source,
|
|
)
|
|
|
|
def test_shared_twin_theme_tracks_demo_role_and_extension_is_always_user(self) -> None:
|
|
self.assertIn("function applyTwinSceneTheme(", self.app_source)
|
|
self.assertIn('canvas.dataset.themeRole = themeRole', self.app_source)
|
|
self.assertIn(
|
|
'canvas.dataset.themeAccent = `#${palette.accent.toString(16).padStart(6, "0")}`',
|
|
self.app_source,
|
|
)
|
|
self.assertIn("accent: 0x0e7773", self.app_source)
|
|
self.assertIn("accent: 0x315eaa", self.app_source)
|
|
self.assertIn(
|
|
'if (document.body.dataset.module === "self-extension") return "user"',
|
|
self.app_source,
|
|
)
|
|
self.assertIn(
|
|
'const themeRole = document.body.dataset.module === "self-extension"',
|
|
self.app_source,
|
|
)
|
|
self.assertIn(
|
|
'window.addEventListener("cognitive-agent:demo-role-change"',
|
|
self.app_source,
|
|
)
|
|
|
|
def test_shared_twin_ground_uses_theme_radial_alpha_texture(self) -> None:
|
|
self.assertIn("function createTwinGroundTexture(", self.app_source)
|
|
self.assertIn("new THREE.PlaneGeometry(30, 30)", self.app_source)
|
|
self.assertIn(
|
|
'surfaceGradient.addColorStop(0, "rgba(255, 255, 255, 1)")',
|
|
self.app_source,
|
|
)
|
|
self.assertIn(
|
|
'surfaceGradient.addColorStop(1, "rgba(255, 255, 255, 0)")',
|
|
self.app_source,
|
|
)
|
|
self.assertIn("const outerRadius = size * 0.48", self.app_source)
|
|
self.assertIn(
|
|
"const gridColor = accent.clone().lerp(new THREE.Color(0xffffff), 0.52)",
|
|
self.app_source,
|
|
)
|
|
self.assertIn("gridContext.lineWidth = major ? 0.9 : 0.45", self.app_source)
|
|
self.assertIn('gridContext.globalCompositeOperation = "destination-in"', self.app_source)
|
|
self.assertIn("applyTwinGroundTheme(twin, palette)", self.app_source)
|
|
self.assertNotIn("new THREE.GridHelper(10, 20", self.app_source)
|
|
|
|
def test_lan_http_origin_does_not_require_crypto_random_uuid(self) -> None:
|
|
id_source = function_source(
|
|
self.app_source,
|
|
"function createOpaqueId(prefix) {",
|
|
"\nfunction createConversationId",
|
|
)
|
|
self.assertIn("globalThis.crypto?.randomUUID", id_source)
|
|
self.assertIn("globalThis.crypto?.getRandomValues", id_source)
|
|
self.assertIn("Math.random()", id_source)
|
|
self.assertIn('return createOpaqueId("conv")', self.app_source)
|
|
self.assertIn('createOpaqueId("hwc")', self.app_source)
|
|
|
|
def test_extension_accepts_only_explicit_physical_ch1_state(self) -> None:
|
|
authority_source = function_source(
|
|
self.extension_source,
|
|
" function isAuthoritativeVehicleStatePayload(payload) {",
|
|
"\n function renderAuthoritativeVehicleState",
|
|
)
|
|
self.assertIn(
|
|
'payload.conversation_id !== sharedConversationId()', authority_source
|
|
)
|
|
self.assertIn(
|
|
"containsAuthoritativePhysicalCh1State(payload.actuator_state)",
|
|
authority_source,
|
|
)
|
|
self.assertIn(
|
|
"containsAuthoritativePhysicalCh1State(payload.domain_state)",
|
|
authority_source,
|
|
)
|
|
self.assertNotIn("physical_feedback_confirmed", authority_source)
|
|
|
|
summary_source = function_source(
|
|
self.extension_source,
|
|
" function renderAuthoritativeVehicleState(payload) {",
|
|
"\n function setMode",
|
|
)
|
|
self.assertNotIn(
|
|
"items.length === 1 && payload.physical_feedback_confirmed",
|
|
summary_source,
|
|
)
|
|
for state_path in (
|
|
"ventilation?.feedback_source",
|
|
"heating?.feedback_source",
|
|
"climate?.feedback_source",
|
|
"roof?.feedback_source",
|
|
"frontLeftWindow?.feedback_source",
|
|
):
|
|
self.assertIn(f'{state_path} === "physical_can_ch1"', summary_source)
|
|
|
|
def test_orchestration_renders_only_the_physical_ch1_patch(self) -> None:
|
|
apply_source = function_source(
|
|
self.app_source,
|
|
"function applyAuthoritativeVehicleState(payload) {",
|
|
'\nwindow.addEventListener("cognitive-agent:extension-vehicle-result"',
|
|
)
|
|
self.assertIn(
|
|
"const actuatorPatch = physicalCh1StatePatch(payload.actuator_state)",
|
|
apply_source,
|
|
)
|
|
self.assertIn(
|
|
"const domainPatch = physicalCh1StatePatch(payload.domain_state)",
|
|
apply_source,
|
|
)
|
|
self.assertIn("renderSeatState(actuatorPatch)", apply_source)
|
|
self.assertIn("renderDomainState(domainPatch)", apply_source)
|
|
self.assertIn("if (actuatorPatch?.driver)", apply_source)
|
|
self.assertNotIn("renderSeatState(payload.actuator_state)", apply_source)
|
|
self.assertNotIn("renderDomainState(payload.domain_state)", apply_source)
|
|
|
|
render_source = function_source(
|
|
self.app_source,
|
|
"function renderResult(result) {",
|
|
"\nasync function submitRequest",
|
|
)
|
|
ch1_branch = function_source(
|
|
render_source,
|
|
" if (ch1HardwareResult) {",
|
|
" updateTwinExplicitAngles",
|
|
)
|
|
self.assertIn("applyAuthoritativeVehicleState(result)", ch1_branch)
|
|
self.assertIn("} else {", ch1_branch)
|
|
self.assertIn("renderSeatState(result.actuator_state)", ch1_branch)
|
|
self.assertIn("renderDomainState(result.domain_state)", ch1_branch)
|
|
|
|
def test_physical_patch_does_not_promote_unmarked_sibling_domains(self) -> None:
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
self.skipTest("Node.js is required to execute the frontend patch helper")
|
|
patch_source = function_source(
|
|
self.app_source,
|
|
"function physicalCh1StatePatch(value) {",
|
|
"\nfunction isAuthoritativeVehicleStatePayload",
|
|
)
|
|
state = {
|
|
"climate": {
|
|
"feedback_source": "physical_can_ch1",
|
|
"power": "on",
|
|
},
|
|
"roof": {"shade_position_pct": 100},
|
|
"window": {
|
|
"positions": {"front_left": {"position_pct": 50}}
|
|
},
|
|
}
|
|
script = (
|
|
f"{patch_source}\n"
|
|
f"const input = {json.dumps(state)};\n"
|
|
"process.stdout.write(JSON.stringify(physicalCh1StatePatch(input)));"
|
|
)
|
|
completed = subprocess.run(
|
|
[node, "-e", script],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
self.assertEqual(
|
|
json.loads(completed.stdout),
|
|
{"climate": state["climate"]},
|
|
)
|
|
|
|
def test_dual_channel_runtime_polls_each_seat_without_aliasing(self) -> None:
|
|
passenger_source = function_source(
|
|
self.app_source,
|
|
"async function synchronizePassengerSeatState() {",
|
|
"\nasync function synchronizeDriverSeatState()",
|
|
)
|
|
driver_source = function_source(
|
|
self.app_source,
|
|
"async function synchronizeDriverSeatState() {",
|
|
"\nasync function synchronizePhysicalSeatState()",
|
|
)
|
|
combined_source = function_source(
|
|
self.app_source,
|
|
"async function synchronizePhysicalSeatState() {",
|
|
"\nasync function loadAuthStatus()",
|
|
)
|
|
self.assertIn("state.runtime?.hardware?.configured", passenger_source)
|
|
self.assertIn("/api/hardware/seat-state", passenger_source)
|
|
self.assertIn("/api/hardware/vehicle-state", driver_source)
|
|
self.assertLess(
|
|
combined_source.index("synchronizePassengerSeatState()"),
|
|
combined_source.index("synchronizeDriverSeatState()"),
|
|
)
|
|
render_source = function_source(
|
|
self.app_source,
|
|
"function renderSeatState(actuatorState) {",
|
|
"\nfunction renderDomainState",
|
|
)
|
|
self.assertNotIn("passenger: mergedActuatorState.driver", render_source)
|
|
|
|
def test_vehicle_selection_cannot_replay_unfiltered_state_in_ch1_mode(self) -> None:
|
|
select_source = function_source(
|
|
self.app_source,
|
|
"function selectVehicle(vehicleId, { silent = false } = {}) {",
|
|
"\nfunction populateVehicleSelector",
|
|
)
|
|
driver_branch = select_source.index("if (isDriverOnlyCh1Runtime())")
|
|
filtered_snapshot = select_source.index(
|
|
"state.vehicleStateSnapshot?.actuatorState",
|
|
driver_branch,
|
|
)
|
|
unfiltered_result = select_source.index("state.result?.actuator_state")
|
|
self.assertLess(driver_branch, filtered_snapshot)
|
|
self.assertLess(filtered_snapshot, unfiltered_result)
|
|
self.assertIn("} else if (state.result?.actuator_state)", select_source)
|
|
|
|
def test_driver_only_ch1_has_no_hidden_browser_lease_gate(self) -> None:
|
|
submit_source = function_source(
|
|
self.app_source,
|
|
"async function submitRequest(event) {",
|
|
"\nasync function copyArtifact",
|
|
)
|
|
self.assertIn("const driverOnlyCh1 = isDriverOnlyCh1Runtime()", submit_source)
|
|
self.assertIn("&& !driverOnlyCh1", submit_source)
|
|
self.assertIn(
|
|
'state.executionMode === "hardware" && !driverOnlyCh1',
|
|
submit_source,
|
|
)
|
|
startup_source = self.app_source[
|
|
self.app_source.index('document.addEventListener("DOMContentLoaded"') :
|
|
]
|
|
self.assertIn("if (!isDriverOnlyCh1Runtime())", startup_source)
|
|
self.assertIn("loadHardwareAccessStatus", startup_source)
|
|
|
|
def test_state_results_flow_in_both_directions(self) -> None:
|
|
self.assertIn(
|
|
'window.addEventListener("cognitive-agent:extension-vehicle-result"',
|
|
self.app_source,
|
|
)
|
|
self.assertIn(
|
|
'window.addEventListener("cognitive-agent:result"',
|
|
self.extension_source,
|
|
)
|
|
|
|
|
|
class PhysicalMarkerInvalidationContractTests(unittest.TestCase):
|
|
def test_logical_door_and_window_actions_replace_stale_physical_roots(self) -> None:
|
|
store = ConversationStateStore()
|
|
conversation_id = "conv-frontend-marker-invalidation"
|
|
store.synchronize_physical_ch1_feedback(
|
|
conversation_id,
|
|
[
|
|
{
|
|
"source": "physical_can_ch1",
|
|
"channel": 1,
|
|
"frame_id": "0x3B6",
|
|
"feedback_frame_id": "0x3B6",
|
|
"valid": True,
|
|
"verification": {
|
|
"authoritative": True,
|
|
"channel": 1,
|
|
"simulated": False,
|
|
"feedback_required": True,
|
|
"feedback_observed": True,
|
|
"feedback_match": True,
|
|
"frame_id": "0x3B6",
|
|
},
|
|
"domain_state": {
|
|
"door": {"locks": {"rear_left": {"state": "lock"}}},
|
|
"window": {
|
|
"positions": {
|
|
"front_left": {
|
|
"position_pct": 20,
|
|
"closed": False,
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
],
|
|
)
|
|
physical = store.snapshot(conversation_id)["domain_devices"]
|
|
self.assertEqual(physical["door"]["feedback_source"], "physical_can_ch1")
|
|
self.assertEqual(physical["window"]["feedback_source"], "physical_can_ch1")
|
|
|
|
updated = store.apply_successful_actions(
|
|
conversation_id,
|
|
"logical projection must not look physical",
|
|
[
|
|
{
|
|
"domain": "door",
|
|
"capability": "door.set_rear_lock",
|
|
"target": "rear",
|
|
"parameters": {"state": "unlock"},
|
|
},
|
|
{
|
|
"domain": "window",
|
|
"capability": "window.set_position",
|
|
"target": "front_left",
|
|
"parameters": {"position_pct": 60},
|
|
},
|
|
],
|
|
[],
|
|
)["domain_devices"]
|
|
self.assertEqual(updated["door"]["feedback_source"], "virtual")
|
|
self.assertEqual(updated["window"]["feedback_source"], "virtual")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|