添加红外扩展
This commit is contained in:
@@ -349,11 +349,11 @@ operations:
|
||||
) as response:
|
||||
console_styles = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/self-extension.js?v=nfc-toy-33", timeout=3
|
||||
f"{self.base_url}/self-extension.js?v=gscu-tcp-37", timeout=3
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/self-extension.css?v=nfc-toy-33", timeout=3
|
||||
f"{self.base_url}/self-extension.css?v=gscu-tcp-37", timeout=3
|
||||
) as response:
|
||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||
with self.opener.open(
|
||||
@@ -393,8 +393,8 @@ operations:
|
||||
self.assertIn('<body class="console-v2">', html)
|
||||
self.assertIn('id="v2BootScreen"', html)
|
||||
self.assertIn('正在加载车机控制与实时状态', html)
|
||||
self.assertIn('src="/self-extension.js?v=nfc-toy-33"', html)
|
||||
self.assertIn('href="/self-extension.css?v=nfc-toy-33"', html)
|
||||
self.assertIn('src="/self-extension.js?v=gscu-tcp-37"', html)
|
||||
self.assertIn('href="/self-extension.css?v=gscu-tcp-37"', html)
|
||||
self.assertNotIn("self-calibration", html)
|
||||
self.assertNotIn("self-repair", html)
|
||||
self.assertNotIn("self-evolution", html)
|
||||
@@ -639,7 +639,7 @@ operations:
|
||||
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v210"', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v214"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-107"', service_worker)
|
||||
self.assertIn('"/assets/pets/self-extension-cat.png?v=3"', service_worker)
|
||||
self.assertIn(".then(() => self.skipWaiting())", service_worker)
|
||||
@@ -1037,6 +1037,96 @@ operations:
|
||||
self.assertEqual(result["transport"], "simulation")
|
||||
self.assertFalse(result["physical_state_confirmed"])
|
||||
|
||||
def test_self_extension_tcp_configuration_endpoint_updates_runtime(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
extension = SelfExtensionService(SimulationGateway())
|
||||
SeatDemoHandler.extension = extension
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"GSCU_RUNTIME_CONFIG_PATH": f"{directory}/gateway.json"},
|
||||
clear=False,
|
||||
):
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/extension/gscu/config",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{"listen_host": "127.0.0.1", "port": 45444}
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
persisted = json.loads(
|
||||
Path(directory, "gateway.json").read_text(encoding="utf-8")
|
||||
)
|
||||
finally:
|
||||
extension.close()
|
||||
SeatDemoHandler.extension = original_extension
|
||||
|
||||
self.assertTrue(result["success"])
|
||||
self.assertEqual(result["transport"], "tcp")
|
||||
self.assertEqual(result["listen_host"], "127.0.0.1")
|
||||
self.assertEqual(result["port"], 45444)
|
||||
self.assertEqual(
|
||||
persisted,
|
||||
{"transport": "tcp", "listen_host": "127.0.0.1", "port": 45444},
|
||||
)
|
||||
|
||||
def test_infrared_slot_catalog_api_and_confirmed_erase_all(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(directory, "infrared-slots.sqlite3"),
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
try:
|
||||
for slot, name in ((12, "电视开关"), (3, "空调制冷")):
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/extension/gscu/slots/{slot}",
|
||||
method="PUT",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps({"name": name, "source": "tcp"}).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
self.assertEqual(response.status, 200)
|
||||
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/extension/gscu/slots", timeout=3
|
||||
) as response:
|
||||
catalog = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual([item["slot"] for item in catalog["slots"]], [3, 12])
|
||||
|
||||
delete_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/extension/gscu/slots/3",
|
||||
method="DELETE",
|
||||
)
|
||||
with self.opener.open(delete_request, timeout=3) as response:
|
||||
deleted = json.loads(response.read().decode("utf-8"))
|
||||
self.assertTrue(deleted["removed"])
|
||||
self.assertFalse(deleted["device_signal_erased"])
|
||||
|
||||
erase_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/extension/gscu/erase-all",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps({"confirm": "ERASE_ALL"}).encode("utf-8"),
|
||||
)
|
||||
with self.opener.open(erase_request, timeout=3) as response:
|
||||
erased = json.loads(response.read().decode("utf-8"))
|
||||
self.assertTrue(erased["catalog_cleared"])
|
||||
self.assertEqual(erased["catalog_removed_count"], 1)
|
||||
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/api/extension/gscu/slots", timeout=3
|
||||
) as response:
|
||||
empty_catalog = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(empty_catalog["slots"], [])
|
||||
finally:
|
||||
SeatDemoHandler.extension = original_extension
|
||||
|
||||
def test_main_request_routes_live_nfc_peripheral_to_gscu(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
|
||||
@@ -35,7 +35,7 @@ def test_unified_console_exposes_nfc_discovery_and_learning_flow() -> None:
|
||||
assert 'sendGatewayCommand("read", { slot: learning.draft.slot })' in script
|
||||
assert 'function infraredSignalFromResponse(value)' in script
|
||||
assert 'function infraredFingerprint(signal)' in script
|
||||
assert '学习回执成功,但物理槽位回读为空' in script
|
||||
assert '学习指令已下发,但物理槽位回读为空' in script
|
||||
assert 'signalSource: "n7web"' in script
|
||||
assert 'if (capability.signalSource === "n7web") return' in script
|
||||
assert '建立档案并开始学习' in script
|
||||
@@ -49,8 +49,49 @@ def test_unified_console_exposes_nfc_discovery_and_learning_flow() -> None:
|
||||
assert '.v2-nfc-inline' in styles
|
||||
assert '.v2-nfc-dialog' in styles
|
||||
assert '.v2-nfc-capability-grid' in styles
|
||||
assert 'self-extension.js?v=nfc-toy-33' in html
|
||||
assert 'self-extension.css?v=nfc-toy-33' in html
|
||||
assert 'self-extension.js?v=gscu-tcp-37' in html
|
||||
assert 'self-extension.css?v=gscu-tcp-37' in html
|
||||
|
||||
|
||||
def test_unified_console_exposes_gscu_tcp_configuration_and_actions() -> None:
|
||||
script = source("self-extension.js")
|
||||
styles = source("self-extension.css")
|
||||
|
||||
assert 'id="extensionGscuTcpHost"' in script
|
||||
assert 'id="extensionGscuTcpPort"' in script
|
||||
assert 'id="extensionGscuCommandName"' in script
|
||||
assert 'id="extensionGscuNamedCommands"' in script
|
||||
assert 'data-open-gscu-tcp' in script
|
||||
assert 'api("gscu/config"' in script
|
||||
assert 'api("gscu/slots")' in script
|
||||
assert 'method: "PUT"' in script
|
||||
assert 'method: "DELETE"' in script
|
||||
assert '"gscu/erase-all"' in script
|
||||
assert 'verification: "slot_readback"' in script
|
||||
assert "学习指令已下发,但槽位回读失败" in script
|
||||
assert "确认擦除 GSCU1B 中的全部红外码吗" in script
|
||||
assert "GSCU1B 设备中的该红外码不会被擦除" in script
|
||||
assert "function upsertNamedInfraredCommand(slot, name" in script
|
||||
assert "button.disabled = true;" in script
|
||||
assert "buttons.forEach((item) => { item.disabled = true; });" not in script
|
||||
for action in (
|
||||
"info",
|
||||
"learn",
|
||||
"cancel",
|
||||
"emit",
|
||||
"read",
|
||||
"write",
|
||||
"restart",
|
||||
"wifi-lock",
|
||||
"erase",
|
||||
"reset",
|
||||
"raw",
|
||||
):
|
||||
assert f'data-gscu-action="{action}"' in script
|
||||
assert ".v2-gscu-tcp-dialog" in styles
|
||||
assert ".v2-gscu-config-form" in styles
|
||||
assert "#extensionGscuTcpResult" in styles
|
||||
assert "#extensionGscuNamedCommands" in styles
|
||||
|
||||
|
||||
def test_server_exposes_nfc_snapshot_through_same_origin_extension_api() -> None:
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
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 collections.abc import Callable
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from cognitive_agent.application.self_extension import (
|
||||
ExtensionIntegrationError,
|
||||
GatewayError,
|
||||
MqttGateway,
|
||||
SelfExtensionService,
|
||||
SimulationGateway,
|
||||
TcpGateway,
|
||||
active_peripheral_plan_catalog,
|
||||
build_gateway_command,
|
||||
create_gateway,
|
||||
resolve_active_peripheral_command,
|
||||
validate_slot,
|
||||
)
|
||||
@@ -39,7 +43,34 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
self.assertEqual(command["type"], "infrared")
|
||||
self.assertEqual(command["action"], "learn")
|
||||
self.assertEqual(command["data"], {"no": 17})
|
||||
self.assertTrue(str(command["messageId"]).startswith("bridge-"))
|
||||
self.assertNotIn("messageId", command)
|
||||
|
||||
self.assertEqual(
|
||||
build_gateway_command("emit", {"slot": 17}),
|
||||
{"action": "emit", "data": {"no": 17}, "type": "infrared"},
|
||||
)
|
||||
self.assertEqual(
|
||||
build_gateway_command("cancel", {}),
|
||||
{"action": "learnCancel", "type": "infrared"},
|
||||
)
|
||||
read_command = build_gateway_command("read", {"slot": 17})
|
||||
self.assertEqual(read_command["action"], "ir_read")
|
||||
self.assertEqual(read_command["data"], {"no": 17})
|
||||
self.assertTrue(str(read_command["messageId"]).startswith("bridge-"))
|
||||
write_command = build_gateway_command(
|
||||
"write", {"slot": 17, "ir_data": "sample-infrared-data"}
|
||||
)
|
||||
self.assertEqual(write_command["action"], "ir_write")
|
||||
self.assertEqual(write_command["no"], 17)
|
||||
self.assertEqual(write_command["ir_data"], "sample-infrared-data")
|
||||
self.assertEqual(
|
||||
build_gateway_command("restart", {}),
|
||||
{"system": "restart", "type": "setting"},
|
||||
)
|
||||
self.assertEqual(
|
||||
build_gateway_command("wifi_lock", {"state": 1}),
|
||||
{"wifiLock": 1, "type": "setting"},
|
||||
)
|
||||
|
||||
self.assertEqual(validate_slot("248"), 248)
|
||||
with self.assertRaises(GatewayError):
|
||||
@@ -47,6 +78,245 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
with self.assertRaises(GatewayError):
|
||||
build_gateway_command("erase", {"confirm": "wrong"})
|
||||
|
||||
def test_infrared_slot_catalog_is_durable_sorted_and_device_safe(self) -> None:
|
||||
with TemporaryDirectory() as directory:
|
||||
database_path = Path(directory, "infrared-slots.sqlite3")
|
||||
service = SelfExtensionService(
|
||||
SimulationGateway(), catalog_db_path=database_path
|
||||
)
|
||||
slot_12 = service.save_infrared_slot(
|
||||
12, {"name": "电视开关", "source": "tcp"}
|
||||
)["slot"]
|
||||
service.save_infrared_slot(3, {"name": "空调制冷", "source": "tcp"})
|
||||
renamed = service.save_infrared_slot(
|
||||
12, {"name": "电视电源", "source": "tcp"}
|
||||
)["slot"]
|
||||
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in service.list_infrared_slots()["slots"]],
|
||||
[3, 12],
|
||||
)
|
||||
self.assertEqual(renamed["name"], "电视电源")
|
||||
self.assertEqual(renamed["learned_at"], slot_12["learned_at"])
|
||||
|
||||
reloaded = SelfExtensionService(
|
||||
SimulationGateway(), catalog_db_path=database_path
|
||||
)
|
||||
self.assertEqual(reloaded.list_infrared_slots()["count"], 2)
|
||||
deleted = reloaded.delete_infrared_slot(3)
|
||||
self.assertTrue(deleted["removed"])
|
||||
self.assertFalse(deleted["device_signal_erased"])
|
||||
self.assertEqual(
|
||||
[item["slot"] for item in reloaded.list_infrared_slots()["slots"]],
|
||||
[12],
|
||||
)
|
||||
|
||||
def test_erase_all_clears_catalog_only_after_valid_device_submission(self) -> None:
|
||||
with TemporaryDirectory() as directory:
|
||||
service = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(directory, "infrared-slots.sqlite3"),
|
||||
)
|
||||
service.save_infrared_slot(8, {"name": "玩具启动"})
|
||||
|
||||
with self.assertRaisesRegex(GatewayError, "ERASE_ALL"):
|
||||
service.erase_all_infrared({"confirm": "wrong"})
|
||||
self.assertEqual(service.list_infrared_slots()["count"], 1)
|
||||
|
||||
result = service.erase_all_infrared({"confirm": "ERASE_ALL"})
|
||||
self.assertTrue(result["catalog_cleared"])
|
||||
self.assertEqual(result["catalog_removed_count"], 1)
|
||||
self.assertEqual(service.list_infrared_slots()["count"], 0)
|
||||
|
||||
def test_mqtt_gateway_defaults_to_local_infrared_topics(self) -> None:
|
||||
mqtt_variables = {
|
||||
"GSCU_MQTT_HOST",
|
||||
"GSCU_MQTT_PORT",
|
||||
"GSCU_COMMAND_TOPIC",
|
||||
"GSCU_RESPONSE_TOPIC",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_HOST",
|
||||
"COGNITIVE_AGENT_VEHICLE_MQTT_PORT",
|
||||
}
|
||||
environment = {
|
||||
key: value for key, value in os.environ.items() if key not in mqtt_variables
|
||||
}
|
||||
with patch.dict(os.environ, environment, clear=True):
|
||||
gateway = MqttGateway()
|
||||
|
||||
self.assertEqual(gateway.host, "127.0.0.1")
|
||||
self.assertEqual(gateway.port, 1883)
|
||||
self.assertEqual(gateway.command_topic, "topic/from_n7")
|
||||
self.assertEqual(gateway.response_topic, "topic/from_infrared")
|
||||
self.assertEqual(
|
||||
gateway.status(),
|
||||
{
|
||||
"transport": "mqtt",
|
||||
"connected": False,
|
||||
"broker": "127.0.0.1:1883",
|
||||
"command_topic": "topic/from_n7",
|
||||
"response_topic": "topic/from_infrared",
|
||||
"qos": 0,
|
||||
},
|
||||
)
|
||||
|
||||
def test_default_gateway_transport_is_tcp(self) -> None:
|
||||
with TemporaryDirectory() as directory:
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"GSCU_RUNTIME_CONFIG_PATH": f"{directory}/gateway.json"},
|
||||
clear=True,
|
||||
):
|
||||
gateway = create_gateway()
|
||||
self.assertIsInstance(gateway, TcpGateway)
|
||||
self.assertEqual(gateway.listen_host, "0.0.0.0")
|
||||
self.assertEqual(gateway.port, 4444)
|
||||
|
||||
def test_tcp_gateway_runtime_configuration_is_persisted_and_reloaded(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()
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
with TemporaryDirectory() as directory:
|
||||
config_path = f"{directory}/gateway.json"
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"GSCU_RUNTIME_CONFIG_PATH": config_path},
|
||||
clear=True,
|
||||
):
|
||||
service.start()
|
||||
try:
|
||||
result = service.configure_tcp_gateway(
|
||||
{"listen_host": "127.0.0.1", "port": port}
|
||||
)
|
||||
self.assertEqual(result["transport"], "tcp")
|
||||
self.assertEqual(result["listen_host"], "127.0.0.1")
|
||||
self.assertEqual(result["port"], port)
|
||||
self.assertTrue(
|
||||
self._wait_until(lambda: service.health().get("listening")),
|
||||
service.health(),
|
||||
)
|
||||
saved = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
||||
self.assertEqual(
|
||||
saved,
|
||||
{
|
||||
"transport": "tcp",
|
||||
"listen_host": "127.0.0.1",
|
||||
"port": port,
|
||||
},
|
||||
)
|
||||
reloaded = create_gateway()
|
||||
self.assertIsInstance(reloaded, TcpGateway)
|
||||
self.assertEqual(reloaded.listen_host, "127.0.0.1")
|
||||
self.assertEqual(reloaded.port, port)
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
def test_tcp_gateway_runtime_configuration_rejects_invalid_values(self) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
with self.assertRaisesRegex(GatewayError, "IPv4"):
|
||||
service.configure_tcp_gateway({"listen_host": "not-an-ip", "port": 4444})
|
||||
with self.assertRaisesRegex(GatewayError, "1–65535"):
|
||||
service.configure_tcp_gateway({"listen_host": "0.0.0.0", "port": 70000})
|
||||
|
||||
def test_tcp_gateway_submits_verified_learning_protocol_without_ack(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()
|
||||
gateway = TcpGateway("127.0.0.1", port)
|
||||
service = SelfExtensionService(gateway)
|
||||
service.command_timeout = 2
|
||||
received: list[dict[str, object]] = []
|
||||
device_finished = threading.Event()
|
||||
|
||||
def run_device() -> None:
|
||||
connection = socket.create_connection(("127.0.0.1", port), timeout=2)
|
||||
try:
|
||||
payload = b""
|
||||
while b"\n" not in payload:
|
||||
payload += connection.recv(4096)
|
||||
received.append(json.loads(payload.split(b"\n", 1)[0]))
|
||||
time.sleep(0.3)
|
||||
finally:
|
||||
connection.close()
|
||||
device_finished.set()
|
||||
|
||||
service.start()
|
||||
try:
|
||||
self.assertTrue(self._wait_until(lambda: gateway.status()["listening"]))
|
||||
device_thread = threading.Thread(target=run_device, daemon=True)
|
||||
device_thread.start()
|
||||
self.assertTrue(self._wait_until(lambda: gateway.status()["connected"]))
|
||||
started = time.monotonic()
|
||||
result = service.execute_gateway("learn", {"slot": 23})
|
||||
elapsed = time.monotonic() - started
|
||||
self.assertTrue(device_finished.wait(timeout=2))
|
||||
self.assertEqual(
|
||||
received,
|
||||
[
|
||||
{
|
||||
"action": "learn",
|
||||
"data": {"no": 23},
|
||||
"type": "infrared",
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertLess(elapsed, 0.5)
|
||||
self.assertEqual(result["device_response"]["delivery_status"], "submitted")
|
||||
self.assertTrue(result["device_response"]["transport_submitted"])
|
||||
self.assertFalse(result["learning_capture_confirmed"])
|
||||
self.assertIn("槽位回读确认", result["message"])
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
def test_tcp_gateway_returns_immediately_for_command_without_device_ack(
|
||||
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()
|
||||
gateway = TcpGateway("127.0.0.1", port)
|
||||
service = SelfExtensionService(gateway)
|
||||
service.command_timeout = 2
|
||||
received = threading.Event()
|
||||
|
||||
def run_device() -> None:
|
||||
connection = socket.create_connection(("127.0.0.1", port), timeout=2)
|
||||
try:
|
||||
payload = b""
|
||||
while b"\n" not in payload:
|
||||
payload += connection.recv(4096)
|
||||
command = json.loads(payload.split(b"\n", 1)[0])
|
||||
self.assertEqual(
|
||||
command,
|
||||
{"action": "emit", "data": {"no": 9}, "type": "infrared"},
|
||||
)
|
||||
received.set()
|
||||
time.sleep(0.3)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
service.start()
|
||||
try:
|
||||
self.assertTrue(self._wait_until(lambda: gateway.status()["listening"]))
|
||||
device_thread = threading.Thread(target=run_device, daemon=True)
|
||||
device_thread.start()
|
||||
self.assertTrue(self._wait_until(lambda: gateway.status()["connected"]))
|
||||
started = time.monotonic()
|
||||
result = service.execute_gateway("emit", {"slot": 9})
|
||||
elapsed = time.monotonic() - started
|
||||
self.assertTrue(received.wait(timeout=1))
|
||||
self.assertLess(elapsed, 0.5)
|
||||
self.assertEqual(
|
||||
result["device_response"]["delivery_status"], "submitted"
|
||||
)
|
||||
self.assertTrue(result["device_response"]["transport_submitted"])
|
||||
self.assertFalse(result["gateway_acknowledged"])
|
||||
finally:
|
||||
service.close()
|
||||
|
||||
def test_simulation_gateway_executes_without_physical_confirmation(self) -> None:
|
||||
service = SelfExtensionService(SimulationGateway())
|
||||
result = service.execute_gateway("emit", {"slot": 3, "repeat": 2})
|
||||
|
||||
Reference in New Issue
Block a user