930 lines
36 KiB
Python
930 lines
36 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from copy import deepcopy
|
|
import json
|
|
import os
|
|
import socket
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
from cognitive_agent.application.self_extension import (
|
|
ExtensionIntegrationError,
|
|
GatewayError,
|
|
SelfExtensionService,
|
|
SimulationGateway,
|
|
TcpGateway,
|
|
build_gateway_command,
|
|
validate_slot,
|
|
)
|
|
from cognitive_agent.application.zcan_seat_executor import ZcanBridgeSeatExecutor
|
|
|
|
|
|
class SelfExtensionTests(unittest.TestCase):
|
|
@staticmethod
|
|
def _wait_until(predicate: Callable[[], bool], timeout: float = 3.0) -> bool:
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
if predicate():
|
|
return True
|
|
time.sleep(0.02)
|
|
return predicate()
|
|
|
|
def test_gateway_command_contracts_and_slot_validation(self) -> None:
|
|
command = build_gateway_command("learn", {"slot": 17})
|
|
self.assertEqual(command["type"], "infrared")
|
|
self.assertEqual(command["action"], "learn")
|
|
self.assertEqual(command["data"], {"no": 17})
|
|
self.assertTrue(str(command["messageId"]).startswith("bridge-"))
|
|
|
|
self.assertEqual(validate_slot("248"), 248)
|
|
with self.assertRaises(GatewayError):
|
|
validate_slot(0)
|
|
with self.assertRaises(GatewayError):
|
|
build_gateway_command("erase", {"confirm": "wrong"})
|
|
|
|
def test_simulation_gateway_executes_without_physical_confirmation(self) -> None:
|
|
service = SelfExtensionService(SimulationGateway())
|
|
result = service.execute_gateway("emit", {"slot": 3, "repeat": 2})
|
|
|
|
self.assertTrue(result["success"])
|
|
self.assertEqual(result["transport"], "simulation")
|
|
self.assertFalse(result["physical_state_confirmed"])
|
|
self.assertEqual(len(result["device_response"]), 2)
|
|
|
|
def test_plan_only_accepts_learned_catalog_capabilities(self) -> None:
|
|
service = SelfExtensionService(SimulationGateway())
|
|
proposed = {
|
|
"summary": "先开玩具灯,再打开阅读灯",
|
|
"needs_confirmation": False,
|
|
"question": "",
|
|
"steps": [
|
|
{
|
|
"kind": "peripheral",
|
|
"capability_id": "cap-light-on",
|
|
"command": "",
|
|
"label": "ignored",
|
|
},
|
|
{
|
|
"kind": "peripheral",
|
|
"capability_id": "invented-capability",
|
|
"command": "",
|
|
"label": "ignored",
|
|
},
|
|
{
|
|
"kind": "vehicle",
|
|
"capability_id": "",
|
|
"command": "light reading all on",
|
|
"label": "车内灯光 · 阅读灯",
|
|
},
|
|
],
|
|
}
|
|
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
|
with patch.object(
|
|
service,
|
|
"_request_gpt_plan",
|
|
return_value=json.dumps(proposed, ensure_ascii=False),
|
|
):
|
|
result = service.plan(
|
|
{
|
|
"utterance": "打开玩具灯和阅读灯",
|
|
"peripherals": [
|
|
{
|
|
"id": "cap-light-on",
|
|
"status": "learned",
|
|
"device_name": "灯光测试玩具",
|
|
"action": "开启",
|
|
"risk": "low",
|
|
}
|
|
],
|
|
"vehicle_devices": [
|
|
{
|
|
"name": "车内灯光",
|
|
"actions": [
|
|
{
|
|
"name": "阅读灯",
|
|
"command": "light reading all on",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
self.assertEqual(len(result["steps"]), 2)
|
|
self.assertEqual(result["steps"][0]["capability_id"], "cap-light-on")
|
|
self.assertEqual(result["steps"][1]["command"], "light reading all on")
|
|
|
|
def test_plan_rejects_vehicle_command_outside_current_catalog(self) -> None:
|
|
service = SelfExtensionService(SimulationGateway())
|
|
proposed = {
|
|
"summary": "unsafe target",
|
|
"needs_confirmation": False,
|
|
"question": "",
|
|
"steps": [
|
|
{
|
|
"kind": "vehicle",
|
|
"capability_id": "",
|
|
"command": "seat passenger vent high",
|
|
"label": "wrong seat",
|
|
"delay_ms": 0,
|
|
}
|
|
],
|
|
}
|
|
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
|
with patch.object(
|
|
service,
|
|
"_request_gpt_plan",
|
|
return_value=json.dumps(proposed),
|
|
):
|
|
with self.assertRaisesRegex(ExtensionIntegrationError, "双通道"):
|
|
service.plan(
|
|
{
|
|
"utterance": "打开座椅通风",
|
|
"peripherals": [],
|
|
"vehicle_devices": [
|
|
{
|
|
"name": "主驾座椅",
|
|
"actions": [
|
|
{
|
|
"name": "通风高档",
|
|
"command": "seat driver vent high",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
)
|
|
|
|
def test_gpt_plan_honors_streaming_responses_configuration(self) -> None:
|
|
proposed = json.dumps(
|
|
{
|
|
"summary": "关闭空调",
|
|
"needs_confirmation": False,
|
|
"question": "",
|
|
"steps": [
|
|
{
|
|
"kind": "vehicle",
|
|
"capability_id": "",
|
|
"command": "ac off",
|
|
"label": "关闭空调",
|
|
}
|
|
],
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
stream_body = "\n".join(
|
|
(
|
|
"event: response.output_text.delta",
|
|
"data: "
|
|
+ json.dumps(
|
|
{
|
|
"type": "response.output_text.delta",
|
|
"delta": proposed[:20],
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
"event: response.output_text.done",
|
|
"data: "
|
|
+ json.dumps(
|
|
{"type": "response.output_text.done", "text": proposed},
|
|
ensure_ascii=False,
|
|
),
|
|
"data: [DONE]",
|
|
)
|
|
)
|
|
response = MagicMock()
|
|
response.__enter__.return_value = response
|
|
response.read.return_value = stream_body.encode("utf-8")
|
|
|
|
with patch.dict(os.environ, {"OPENAI_STREAM": "true"}, clear=False):
|
|
service = SelfExtensionService(SimulationGateway())
|
|
with patch(
|
|
"cognitive_agent.application.self_extension.urlopen",
|
|
return_value=response,
|
|
) as open_request:
|
|
output = service._request_gpt_plan(
|
|
"关闭空调",
|
|
[],
|
|
[
|
|
{
|
|
"name": "空调",
|
|
"actions": [{"name": "关闭空调", "command": "ac off"}],
|
|
}
|
|
],
|
|
"test-key",
|
|
)
|
|
|
|
self.assertEqual(json.loads(output)["steps"][0]["command"], "ac off")
|
|
request_payload = json.loads(
|
|
open_request.call_args.args[0].data.decode("utf-8")
|
|
)
|
|
self.assertIs(request_payload["stream"], True)
|
|
self.assertEqual(request_payload["model"], "gpt-5.6-sol")
|
|
command_schema = request_payload["text"]["format"]["schema"]["properties"][
|
|
"steps"
|
|
]["items"]["properties"]["command"]
|
|
self.assertEqual(command_schema["type"], "string")
|
|
self.assertEqual(command_schema["maxLength"], 300)
|
|
|
|
def test_plan_accepts_bounded_passenger_position_for_declared_ch0_executor(
|
|
self,
|
|
) -> None:
|
|
service = SelfExtensionService(SimulationGateway())
|
|
proposed = {
|
|
"summary": "调整副驾",
|
|
"needs_confirmation": False,
|
|
"question": "",
|
|
"steps": [
|
|
{
|
|
"kind": "vehicle",
|
|
"capability_id": "",
|
|
"command": "副驾水平调到63%",
|
|
"label": "副驾水平位置 63%",
|
|
"delay_ms": 0,
|
|
}
|
|
],
|
|
}
|
|
payload = {
|
|
"utterance": "副驾水平位置调到63%",
|
|
"peripherals": [],
|
|
"vehicle_devices": [
|
|
{
|
|
"id": "passenger-seat",
|
|
"name": "副驾座椅",
|
|
"channel": "CH0",
|
|
"actions": [
|
|
{"name": "副驾水平 50%", "command": "副驾水平调到50%"}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
|
with patch.object(
|
|
service,
|
|
"_request_gpt_plan",
|
|
return_value=json.dumps(proposed, ensure_ascii=False),
|
|
):
|
|
result = service.plan(payload)
|
|
self.assertEqual(result["steps"][0]["command"], "副驾水平调到63%")
|
|
|
|
proposed["steps"][0]["command"] = "副驾水平调到101%"
|
|
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}, clear=False):
|
|
with patch.object(
|
|
service,
|
|
"_request_gpt_plan",
|
|
return_value=json.dumps(proposed, ensure_ascii=False),
|
|
):
|
|
with self.assertRaisesRegex(ExtensionIntegrationError, "双通道"):
|
|
service.plan(payload)
|
|
|
|
def test_vehicle_execution_reuses_the_same_orchestration_service(self) -> None:
|
|
with patch.dict(
|
|
os.environ,
|
|
{"COGNITIVE_AGENT_EXECUTION_MODE": "dry_run"},
|
|
clear=False,
|
|
):
|
|
service = SelfExtensionService(SimulationGateway())
|
|
vehicle_service = Mock()
|
|
vehicle_service.process.return_value = {
|
|
"status": "succeeded",
|
|
"provider_used": "local_rules",
|
|
"request_id": "req-extension-1",
|
|
"conversation_id": "conv-extension-shared",
|
|
"conversation_turn": 3,
|
|
"decision_summary": ["打开阅读灯"],
|
|
"validation": [],
|
|
"execution_report": {"spec": {"results": [{"status": "succeeded"}]}},
|
|
"deferred_requests": [],
|
|
}
|
|
|
|
result = service.execute_vehicle(
|
|
{
|
|
"command": "打开全部阅读灯",
|
|
"conversation_id": "conv-extension-shared",
|
|
"vehicle_id": "denza_n7",
|
|
},
|
|
vehicle_service,
|
|
hardware_access_bypass=True,
|
|
)
|
|
|
|
self.assertTrue(result["success"])
|
|
self.assertEqual(result["request_id"], "req-extension-1")
|
|
self.assertEqual(result["conversation_id"], "conv-extension-shared")
|
|
self.assertEqual(result["conversation_turn"], 3)
|
|
self.assertEqual(result["status"], "succeeded")
|
|
self.assertEqual(
|
|
result["execution_report"], vehicle_service.process.return_value["execution_report"]
|
|
)
|
|
self.assertEqual(result["operation_results"], [{"status": "succeeded"}])
|
|
self.assertFalse(result["physical_feedback_required"])
|
|
self.assertFalse(result["physical_feedback_confirmed"])
|
|
vehicle_service.process.assert_called_once()
|
|
self.assertEqual(vehicle_service.process.call_args.args[0], "打开全部阅读灯")
|
|
self.assertEqual(
|
|
vehicle_service.process.call_args.args[2], "conv-extension-shared"
|
|
)
|
|
self.assertTrue(vehicle_service.process.call_args.kwargs["hardware_access_bypass"])
|
|
|
|
def test_vehicle_execution_only_marks_exact_complete_status_as_success(self) -> None:
|
|
service = SelfExtensionService(SimulationGateway())
|
|
for status in (
|
|
"validation_failed",
|
|
"needs_confirmation",
|
|
"capability_unavailable",
|
|
"partially_succeeded",
|
|
):
|
|
with self.subTest(status=status):
|
|
vehicle_service = Mock()
|
|
vehicle_service.process.return_value = {
|
|
"status": status,
|
|
"provider_used": "local_rules",
|
|
"request_id": f"req-{status}",
|
|
"decision_summary": [],
|
|
"validation": [],
|
|
"execution_report": {
|
|
"spec": {"results": [{"status": "succeeded"}]}
|
|
},
|
|
"deferred_requests": [],
|
|
}
|
|
|
|
result = service.execute_vehicle(
|
|
{"command": "打开全部阅读灯"},
|
|
vehicle_service,
|
|
hardware_access_bypass=True,
|
|
)
|
|
|
|
self.assertFalse(result["success"])
|
|
self.assertEqual(result["status"], status)
|
|
|
|
def test_vehicle_execution_rejects_failed_or_missing_operation_results(self) -> None:
|
|
service = SelfExtensionService(SimulationGateway())
|
|
for operation_results in (
|
|
[],
|
|
[{"status": "succeeded"}, {"status": "failed", "message": "timeout"}],
|
|
):
|
|
with self.subTest(operation_results=operation_results):
|
|
vehicle_service = Mock()
|
|
vehicle_service.process.return_value = {
|
|
"status": "succeeded",
|
|
"execution_report": {"spec": {"results": operation_results}},
|
|
"domain_state": {"roof": {"shade_position_pct": 31}},
|
|
"actuator_state": {"driver": {"heating": {"state": "off"}}},
|
|
}
|
|
|
|
result = service.execute_vehicle(
|
|
{"command": "打开遮阳帘"},
|
|
vehicle_service,
|
|
hardware_access_bypass=True,
|
|
)
|
|
|
|
self.assertFalse(result["success"])
|
|
self.assertEqual(
|
|
result["domain_state"], {"roof": {"shade_position_pct": 31}}
|
|
)
|
|
self.assertEqual(
|
|
result["actuator_state"],
|
|
{"driver": {"heating": {"state": "off"}}},
|
|
)
|
|
|
|
def test_hardware_vehicle_execution_requires_authoritative_feedback_per_item(
|
|
self,
|
|
) -> None:
|
|
with patch.dict(
|
|
os.environ,
|
|
{"COGNITIVE_AGENT_EXECUTION_MODE": "hardware"},
|
|
clear=False,
|
|
):
|
|
service = SelfExtensionService(SimulationGateway())
|
|
vehicle_service = Mock()
|
|
vehicle_service.process.return_value = {
|
|
"status": "succeeded",
|
|
"provider_used": "local_rules",
|
|
"request_id": "req-extension-hardware",
|
|
"execution_report": {
|
|
"spec": {
|
|
"status": "succeeded",
|
|
"physical_state": {
|
|
"source": "physical_can_ch1",
|
|
"complete": True,
|
|
},
|
|
"results": [
|
|
{
|
|
"ref": "operation-1",
|
|
"domain": "climate",
|
|
"status": "succeeded",
|
|
"telemetry": {
|
|
"channel": 1,
|
|
"simulated": False,
|
|
"feedback_required": True,
|
|
"feedback_observed": True,
|
|
"feedback_match": True,
|
|
"feedback_frame": {
|
|
"frame_id": "0x404",
|
|
"channel": 1,
|
|
"payload_size": 8,
|
|
"data_hex": "00 02 00 00 18 18 00 00",
|
|
},
|
|
"feedback_state": {
|
|
"source": "physical_can_ch1",
|
|
"channel": 1,
|
|
"frame_id": "0x404",
|
|
"feedback_frame_id": "0x404",
|
|
"valid": True,
|
|
"domain_state": {"climate": {"power": "on"}},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"ref": "operation-2",
|
|
"domain": "seat",
|
|
"status": "succeeded",
|
|
"telemetry": {
|
|
"channel": 1,
|
|
"simulated": False,
|
|
"feedback_required": True,
|
|
"feedback_observed": True,
|
|
"feedback_match": True,
|
|
"feedback_frame": {
|
|
"frame_id": "0x4FA",
|
|
"channel": 1,
|
|
"payload_size": 8,
|
|
"data_hex": "02 00 00 00 00 00 00 00",
|
|
},
|
|
"feedback_state": {
|
|
"source": "physical_can_ch1",
|
|
"channel": 1,
|
|
"frame_id": "0x4FA",
|
|
"feedback_frame_id": "0x4FA",
|
|
"valid": True,
|
|
"actuator_state": {
|
|
"driver": {
|
|
"ventilation": {
|
|
"state": "on",
|
|
"level": 1,
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}
|
|
},
|
|
"domain_state": {"climate": {"power": "on"}},
|
|
"actuator_state": {"driver": {"ventilation": {"state": "on"}}},
|
|
}
|
|
|
|
confirmed = service.execute_vehicle(
|
|
{"command": "打开空调"},
|
|
vehicle_service,
|
|
hardware_access_bypass=True,
|
|
)
|
|
|
|
self.assertTrue(confirmed["success"])
|
|
self.assertTrue(confirmed["physical_feedback_required"])
|
|
self.assertTrue(confirmed["physical_feedback_confirmed"])
|
|
self.assertEqual(confirmed["physical_state"]["source"], "physical_can_ch1")
|
|
self.assertEqual(len(confirmed["physical_feedback"]), 2)
|
|
self.assertTrue(all(item["confirmed"] for item in confirmed["physical_feedback"]))
|
|
self.assertTrue(all(item["required"] for item in confirmed["physical_feedback"]))
|
|
self.assertTrue(all(item["observed"] for item in confirmed["physical_feedback"]))
|
|
self.assertTrue(all(item["match"] is True for item in confirmed["physical_feedback"]))
|
|
self.assertTrue(all(item["channel"] == 1 for item in confirmed["physical_feedback"]))
|
|
self.assertEqual(
|
|
confirmed["physical_feedback"][0]["state"]["source"],
|
|
"physical_can_ch1",
|
|
)
|
|
|
|
operation = vehicle_service.process.return_value["execution_report"]["spec"][
|
|
"results"
|
|
][1]
|
|
valid_telemetry = deepcopy(operation["telemetry"])
|
|
operation["telemetry"] = {**valid_telemetry, "feedback_match": None}
|
|
unconfirmed = service.execute_vehicle(
|
|
{"command": "打开空调"},
|
|
vehicle_service,
|
|
hardware_access_bypass=True,
|
|
)
|
|
|
|
self.assertFalse(unconfirmed["success"])
|
|
self.assertFalse(unconfirmed["physical_feedback_confirmed"])
|
|
self.assertIn("未逐项获得权威物理反馈", unconfirmed["message"])
|
|
|
|
invalid_cases: list[tuple[str, dict]] = []
|
|
for field, value in (
|
|
("channel", 0),
|
|
("simulated", True),
|
|
("feedback_required", False),
|
|
("feedback_observed", False),
|
|
("feedback_match", False),
|
|
("feedback_frame", None),
|
|
("feedback_state", None),
|
|
):
|
|
invalid_cases.append((field, {**valid_telemetry, field: value}))
|
|
|
|
wrong_frame_channel = deepcopy(valid_telemetry)
|
|
wrong_frame_channel["feedback_frame"]["channel"] = 0
|
|
invalid_cases.append(("feedback_frame.channel", wrong_frame_channel))
|
|
missing_frame_payload = deepcopy(valid_telemetry)
|
|
missing_frame_payload["feedback_frame"].pop("data_hex")
|
|
invalid_cases.append(("feedback_frame.data_hex", missing_frame_payload))
|
|
incomplete_frame_payload = deepcopy(valid_telemetry)
|
|
incomplete_frame_payload["feedback_frame"]["payload_size"] = 7
|
|
invalid_cases.append(
|
|
("feedback_frame.payload_size", incomplete_frame_payload)
|
|
)
|
|
wrong_source = deepcopy(valid_telemetry)
|
|
wrong_source["feedback_state"]["source"] = "physical_can_ch0"
|
|
invalid_cases.append(("feedback_state.source", wrong_source))
|
|
missing_decoded_state = deepcopy(valid_telemetry)
|
|
missing_decoded_state["feedback_state"].pop("actuator_state")
|
|
invalid_cases.append(("feedback_state.actuator_state", missing_decoded_state))
|
|
mismatched_frame = deepcopy(valid_telemetry)
|
|
mismatched_frame["feedback_state"]["frame_id"] = "0x404"
|
|
invalid_cases.append(("feedback_state.frame_id", mismatched_frame))
|
|
invalid_cases.append(
|
|
(
|
|
"legacy_ch0",
|
|
{
|
|
"channel": 0,
|
|
"simulated": False,
|
|
"hardware_feedback": True,
|
|
"feedback_frame": {"frame_id": "0x302"},
|
|
},
|
|
)
|
|
)
|
|
|
|
for label, insufficient_feedback in invalid_cases:
|
|
with self.subTest(insufficient_feedback=label):
|
|
operation["telemetry"] = insufficient_feedback
|
|
rejected = service.execute_vehicle(
|
|
{"command": "打开空调"},
|
|
vehicle_service,
|
|
hardware_access_bypass=True,
|
|
)
|
|
self.assertFalse(rejected["success"])
|
|
self.assertFalse(rejected["physical_feedback_confirmed"])
|
|
|
|
def test_ch0_passenger_feedback_requires_shared_runtime_physical_snapshot(
|
|
self,
|
|
) -> None:
|
|
operation = {
|
|
"ref": "passenger-longitudinal-63",
|
|
"domain": "seat",
|
|
"status": "succeeded",
|
|
"telemetry": {
|
|
"transport": "shared_can_runtime",
|
|
"channel": 0,
|
|
"simulated": False,
|
|
"hardware_feedback": True,
|
|
"feedback_required": True,
|
|
"feedback_observed": True,
|
|
"feedback_match": True,
|
|
"physical_state": {
|
|
"source": "physical_can",
|
|
"channel": 0,
|
|
"target": {
|
|
"device": "seat",
|
|
"zone": "front_right",
|
|
"instance": "passenger",
|
|
},
|
|
"position": {
|
|
"longitudinal": {
|
|
"position_pct": 63,
|
|
"position_valid": True,
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
verified = SelfExtensionService._authoritative_vehicle_feedback_state(
|
|
operation
|
|
)
|
|
self.assertIsNotNone(verified)
|
|
self.assertEqual(verified["verification"]["channel"], 0)
|
|
|
|
operation["telemetry"]["transport"] = "untrusted_bridge"
|
|
self.assertIsNone(
|
|
SelfExtensionService._authoritative_vehicle_feedback_state(operation)
|
|
)
|
|
|
|
def test_ch0_bridge_report_exposes_verified_passenger_snapshot(self) -> None:
|
|
function_config = {
|
|
"metadata": {"request_id": "req-ch0-extension"},
|
|
"spec": {
|
|
"operations": [
|
|
{
|
|
"id": "passenger-longitudinal-63",
|
|
"capability": "seat.set_longitudinal_position",
|
|
}
|
|
]
|
|
},
|
|
}
|
|
bridge_result = {
|
|
"result": "PASS",
|
|
"details": {
|
|
"summary": "物理反馈达到目标",
|
|
"motors": [
|
|
{
|
|
"position": 6300 if index == 0 else 5000,
|
|
"position_valid": True,
|
|
"active": False,
|
|
}
|
|
for index in range(4)
|
|
],
|
|
"thermal": [0x01],
|
|
},
|
|
}
|
|
report = ZcanBridgeSeatExecutor._report(
|
|
function_config,
|
|
"2026-08-11T00:00:00Z",
|
|
bridge_result,
|
|
None,
|
|
)
|
|
physical_state = report["spec"]["physical_state"]
|
|
self.assertEqual(physical_state["source"], "physical_can")
|
|
self.assertEqual(physical_state["channel"], 0)
|
|
self.assertEqual(
|
|
physical_state["position"]["longitudinal"]["position_pct"], 63
|
|
)
|
|
verified = SelfExtensionService._authoritative_vehicle_feedback_state(
|
|
report["spec"]["results"][0]
|
|
)
|
|
self.assertIsNotNone(verified)
|
|
|
|
def test_vehicle_execution_ignores_browser_routing_overrides(self) -> None:
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"COGNITIVE_AGENT_EXECUTION_MODE": "dry_run",
|
|
"COGNITIVE_AGENT_PROVIDER": "local_rules",
|
|
"COGNITIVE_AGENT_VEHICLE_ID": "denza_n7",
|
|
},
|
|
clear=False,
|
|
):
|
|
service = SelfExtensionService(SimulationGateway())
|
|
vehicle_service = Mock()
|
|
vehicle_service.process.return_value = {
|
|
"status": "succeeded",
|
|
"execution_report": {"spec": {"results": [{"status": "succeeded"}]}},
|
|
}
|
|
|
|
service.execute_vehicle(
|
|
{
|
|
"command": "seat driver vent low",
|
|
"execution_mode": "hardware",
|
|
"provider": "browser-selected-provider",
|
|
"vehicle_id": "t2g_primary_seat",
|
|
"simulation_fault": "adapter_disconnect",
|
|
"channel": "0",
|
|
},
|
|
vehicle_service,
|
|
hardware_access_bypass=True,
|
|
)
|
|
|
|
args = vehicle_service.process.call_args.args
|
|
kwargs = vehicle_service.process.call_args.kwargs
|
|
self.assertEqual(args[1], "local_rules")
|
|
self.assertEqual(args[3], "dry_run")
|
|
self.assertEqual(kwargs["vehicle_id"], "denza_n7")
|
|
self.assertEqual(kwargs["simulation_fault"], "normal")
|
|
|
|
def test_tcp_gateway_recovers_from_unavailable_address_and_tracks_peers(self) -> None:
|
|
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
probe.bind(("127.0.0.1", 0))
|
|
port = int(probe.getsockname()[1])
|
|
probe.close()
|
|
|
|
class UnavailableAddressSocket:
|
|
def setsockopt(self, *args: object) -> None:
|
|
del args
|
|
|
|
def bind(self, address: object) -> None:
|
|
del address
|
|
raise OSError(10049, "address is not available")
|
|
|
|
def close(self) -> None:
|
|
return
|
|
|
|
socket_attempts = 0
|
|
|
|
def new_server_socket() -> object:
|
|
nonlocal socket_attempts
|
|
socket_attempts += 1
|
|
if socket_attempts == 1:
|
|
return UnavailableAddressSocket()
|
|
return socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
clients: list[socket.socket] = []
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"GSCU_TCP_LISTEN_HOST": "127.0.0.1",
|
|
"GSCU_TCP_PORT": str(port),
|
|
"GSCU_TCP_BIND_RETRY_SECONDS": "0.05",
|
|
},
|
|
):
|
|
gateway = TcpGateway()
|
|
with patch.object(gateway, "_new_server_socket", side_effect=new_server_socket):
|
|
gateway.start()
|
|
try:
|
|
self.assertTrue(
|
|
self._wait_until(lambda: gateway.status()["listening"]),
|
|
gateway.status(),
|
|
)
|
|
self.assertGreaterEqual(socket_attempts, 2)
|
|
waiting = gateway.status()
|
|
self.assertFalse(waiting["connected"])
|
|
self.assertIn("等待设备接入", waiting["message"])
|
|
waiting_health = SelfExtensionService(gateway).health()
|
|
self.assertTrue(waiting_health["listening"])
|
|
self.assertFalse(waiting_health["peer_connected"])
|
|
|
|
first = socket.create_connection(("127.0.0.1", port), timeout=1)
|
|
clients.append(first)
|
|
self.assertTrue(
|
|
self._wait_until(lambda: gateway.status()["connected"]),
|
|
gateway.status(),
|
|
)
|
|
first_peer = gateway.status()["peer"]
|
|
self.assertTrue(str(first_peer).startswith("127.0.0.1:"))
|
|
connected_health = SelfExtensionService(gateway).health()
|
|
self.assertTrue(connected_health["connected"])
|
|
self.assertEqual(connected_health["peer"], first_peer)
|
|
|
|
# Replacing an old peer must not let its reader mark the new
|
|
# GSCU1B session offline when the old socket closes.
|
|
second = socket.create_connection(("127.0.0.1", port), timeout=1)
|
|
clients.append(second)
|
|
self.assertTrue(
|
|
self._wait_until(
|
|
lambda: gateway.status()["peer"] not in {None, first_peer}
|
|
),
|
|
gateway.status(),
|
|
)
|
|
time.sleep(0.1)
|
|
self.assertTrue(gateway.status()["connected"])
|
|
|
|
second.close()
|
|
clients.remove(second)
|
|
self.assertTrue(
|
|
self._wait_until(lambda: not gateway.status()["connected"]),
|
|
gateway.status(),
|
|
)
|
|
self.assertTrue(gateway.status()["listening"])
|
|
|
|
third = socket.create_connection(("127.0.0.1", port), timeout=1)
|
|
clients.append(third)
|
|
self.assertTrue(
|
|
self._wait_until(lambda: gateway.status()["connected"]),
|
|
gateway.status(),
|
|
)
|
|
finally:
|
|
for client in clients:
|
|
client.close()
|
|
gateway.close()
|
|
|
|
stopped = gateway.status()
|
|
self.assertFalse(stopped["listening"])
|
|
self.assertFalse(stopped["connected"])
|
|
|
|
def test_tcp_gateway_recovers_after_port_conflict_is_released(self) -> None:
|
|
blocker = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
blocker.bind(("127.0.0.1", 0))
|
|
blocker.listen(1)
|
|
port = int(blocker.getsockname()[1])
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"GSCU_TCP_LISTEN_HOST": "127.0.0.1",
|
|
"GSCU_TCP_PORT": str(port),
|
|
"GSCU_TCP_BIND_RETRY_SECONDS": "0.05",
|
|
},
|
|
):
|
|
gateway = TcpGateway()
|
|
gateway.start()
|
|
try:
|
|
self.assertTrue(
|
|
self._wait_until(lambda: "监听失败" in gateway.status().get("message", "")),
|
|
gateway.status(),
|
|
)
|
|
self.assertFalse(gateway.status()["listening"])
|
|
blocker.close()
|
|
self.assertTrue(
|
|
self._wait_until(lambda: gateway.status()["listening"]),
|
|
gateway.status(),
|
|
)
|
|
self.assertNotIn("失败", gateway.status()["message"])
|
|
finally:
|
|
blocker.close()
|
|
gateway.close()
|
|
|
|
def test_tcp_gateway_can_restart_immediately_after_close(self) -> None:
|
|
probe = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
probe.bind(("127.0.0.1", 0))
|
|
port = int(probe.getsockname()[1])
|
|
probe.close()
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"GSCU_TCP_LISTEN_HOST": "127.0.0.1",
|
|
"GSCU_TCP_PORT": str(port),
|
|
"GSCU_TCP_BIND_RETRY_SECONDS": "0.05",
|
|
},
|
|
):
|
|
gateway = TcpGateway()
|
|
|
|
try:
|
|
gateway.start()
|
|
self.assertTrue(self._wait_until(lambda: gateway.status()["listening"]))
|
|
gateway.close()
|
|
gateway.start()
|
|
self.assertTrue(
|
|
self._wait_until(lambda: gateway.status()["listening"]),
|
|
gateway.status(),
|
|
)
|
|
finally:
|
|
gateway.close()
|
|
|
|
def test_tcp_gateway_rejects_peer_accepted_during_close(self) -> None:
|
|
settimeout_entered = threading.Event()
|
|
allow_settimeout_to_finish = threading.Event()
|
|
accepted_socket_closed = threading.Event()
|
|
unexpected_reader_started = threading.Event()
|
|
allow_unexpected_reader_to_finish = threading.Event()
|
|
|
|
class AcceptedSocket:
|
|
def settimeout(self, timeout: float) -> None:
|
|
del timeout
|
|
settimeout_entered.set()
|
|
allow_settimeout_to_finish.wait(timeout=2)
|
|
|
|
def setsockopt(self, *args: object) -> None:
|
|
del args
|
|
|
|
def recv(self, size: int) -> bytes:
|
|
del size
|
|
unexpected_reader_started.set()
|
|
allow_unexpected_reader_to_finish.wait(timeout=2)
|
|
return b""
|
|
|
|
def close(self) -> None:
|
|
accepted_socket_closed.set()
|
|
|
|
accepted_socket = AcceptedSocket()
|
|
|
|
class ServerSocket:
|
|
def setsockopt(self, *args: object) -> None:
|
|
del args
|
|
|
|
def bind(self, address: object) -> None:
|
|
del address
|
|
|
|
def listen(self, backlog: int) -> None:
|
|
del backlog
|
|
|
|
def settimeout(self, timeout: float) -> None:
|
|
del timeout
|
|
|
|
def accept(self) -> tuple[AcceptedSocket, tuple[str, int]]:
|
|
return accepted_socket, ("127.0.0.1", 4444)
|
|
|
|
def close(self) -> None:
|
|
return
|
|
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"GSCU_TCP_LISTEN_HOST": "127.0.0.1",
|
|
"GSCU_TCP_PORT": "4444",
|
|
"GSCU_TCP_BIND_RETRY_SECONDS": "0.05",
|
|
},
|
|
):
|
|
gateway = TcpGateway()
|
|
with patch.object(gateway, "_new_server_socket", return_value=ServerSocket()):
|
|
gateway.start()
|
|
self.assertTrue(settimeout_entered.wait(timeout=1))
|
|
close_thread = threading.Thread(target=gateway.close)
|
|
close_thread.start()
|
|
try:
|
|
self.assertTrue(self._wait_until(gateway._stop.is_set))
|
|
finally:
|
|
allow_settimeout_to_finish.set()
|
|
close_thread.join(timeout=3)
|
|
|
|
try:
|
|
self.assertFalse(close_thread.is_alive())
|
|
self.assertTrue(accepted_socket_closed.is_set())
|
|
self.assertFalse(unexpected_reader_started.is_set())
|
|
self.assertFalse(gateway.status()["connected"])
|
|
self.assertIsNone(gateway.status()["peer"])
|
|
finally:
|
|
allow_unexpected_reader_to_finish.set()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|