Files
Cognitive_Agent/tests/unit/test_self_extension.py
T

790 lines
31 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,
)
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, "CH1"):
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["enum"], ["", "ac off"])
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_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()