Files
MeshCore/test/test_official_room_app_compatibility.py
T
mikecarper d031609f26 Improve room retention, moderation and local browser access
Add opt-in bounded chat history, persist explicit room ACL roles, enforce full-key bans and per-user quotas, and provide versioned information-board reads plus an ESP32 local room page. Preserve stored evidence on faults and withhold room delivery acknowledgments when a Companion cannot retain the message.

Exercise production handlers and filesystem fault recovery, pinned official-app JavaScript, real Chromium interactions, and representative ESP32, nRF52, RP2040 and STM32 builds. Add regression suites and bounded browser-workspace RAM reservations to CI.
2026-10-09 23:44:00 -07:00

144 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""Run pinned stock JavaScript room handlers against real firmware output.
This witnesses the pinned public web app, not the installed phone UI. The
production room queue and Companion framing/login code are extracted without
edits. Dart runtime, SQLite, packet encryption and transports are boundaries.
No PlatformIO, hardware, app fork or persistent settings are involved.
"""
from pathlib import Path
import json
import re
import shutil
import tempfile
import unittest
from test_companion_delayed_reply_delivery import production_delayed_reply_inputs
from test_official_app_compatibility import checked, official_app_bundle, SANITIZERS
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "test/fixtures/official_room_app_compatibility"
def room_outputs(work, compiler):
source = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text()
acl = (ROOT / "src/helpers/ClientACL.h").read_text()
header = (ROOT / "examples/simple_room_server/MyMesh.h").read_text()
state = "\n".join(line for line in acl.splitlines()
if line.startswith(("#define PERM_ACL_", "#define OUT_PATH_")))
state += "\n" + extract_braced(acl, "struct ClientInfo") + ";\n"
state += extract_braced(header, "struct PostInfo") + ";\n"
clock = (ROOT / "src/MeshCore.h").read_text()
state += "namespace mesh {\n" + extract_braced(clock, "class RTCClock") + ";\n}\n"
(work / "state.inc").write_text(state, encoding="ascii")
definitions = "\n".join(extract_braced(source, signature) for signature in (
"bool MyMesh::pushPostToClient(", "bool MyMesh::pushRoomTextToClient(",
"bool MyMesh::processAck(", "void MyMesh::activateRoomTopic(",
"bool MyMesh::handleRoomTopicCommand(", "void MyMesh::serviceRoomPush("))
(work / "production.inc").write_text(definitions, encoding="ascii")
fixture = (ROOT / "test/fixtures/room_topic_delivery/test.cpp").read_text()
fixture = fixture[:fixture.index("int main()")]
signed_type = next(line for line in (ROOT / "src/helpers/TxtDataHelpers.h").read_text().splitlines()
if line.startswith("#define TXT_TYPE_SIGNED_PLAIN"))
maximum = next(line for line in header.splitlines() if line.startswith("#define MAX_POST_TEXT_LEN"))
fixture = re.sub(r"^#define TXT_TYPE_SIGNED_PLAIN .*", signed_type, fixture, flags=re.M)
fixture = re.sub(r"^#define MAX_POST_TEXT_LEN .*", maximum, fixture, flags=re.M)
(work / "room_delivery_fixture.inc").write_text(fixture, encoding="ascii")
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
codes = "\n".join(line for line in companion.splitlines()
if line.startswith("#define RESP_CODE_CONTACT_MSG_RECV"))
(work / "frame_codes.inc").write_text(codes, encoding="ascii")
presentation = extract_braced(companion, "bool MyMesh::queueMessage(")
# The entire original framing prefix runs unchanged; the offline queue/UI
# boundary is replaced by capture of those exact bytes, not a serializer.
presentation = presentation[:presentation.index(" const char* sensitive_reply = text;")]
presentation += " (void)path_len;\n frame.assign(out_frame, out_frame + i);\n return true;\n}\n"
presentation = presentation.replace("MyMesh::queueMessage", "WirePresentation::queueMessage")
presentation = presentation.replace("const ContactInfo &from", "const RoomContact &from")
presentation = presentation.replace("mesh::Packet *pkt", "CompanionPacket *pkt")
(work / "presentation.inc").write_text(presentation, encoding="ascii")
binary = work / "room-messages"
checked([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-Wno-missing-field-initializers", *SANITIZERS,
f"-I{work}", f"-I{ROOT / 'src'}",
f"-I{ROOT / 'test/fixtures/room_topic_delivery'}",
str(FIXTURES / "room_messages.cpp"), "-o", str(binary)])
records = []
for line in checked([str(binary)], timeout=20).splitlines():
kind, group, index, version, flood, timestamp, author, text, frame = line.split(":")
if kind != "POST":
raise AssertionError(line)
records.append({"group": group, "index": int(index), "version": int(version),
"flood": bool(int(flood)), "timestamp": int(timestamp),
"author": author, "text": text, "frame": frame})
if len(records) != 13:
raise AssertionError(records)
return records
def login_binary(work, compiler):
production_delayed_reply_inputs(work)
fixture = (ROOT / "test/fixtures/companion_delayed_reply_delivery/test.cpp").read_text()
(work / "companion_login_fixture.inc").write_text(
fixture[:fixture.index("int main()")], encoding="ascii")
binary = work / "room-login"
checked([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-Wno-unused-function", "-Wno-sign-compare", "-Wno-class-memaccess",
*SANITIZERS, "-DCOMPANION_FEATURE_TEXT_TERMINAL=0", "-DMESH_ENABLE_ONE_KEY_DM=0",
f"-I{work}", f"-I{ROOT / 'test/mocks'}",
f"-I{ROOT / 'test/fixtures/serial_wifi_sessions/mocks'}", f"-I{ROOT / 'src'}",
str(FIXTURES / "room_login.cpp"), *[str(ROOT / "src/helpers" / name) for name in (
"CompanionDelayedReplies.cpp", "ArduinoSerialInterface.cpp",
"wifi/SerialWifiInterface.cpp", "TxtDataHelpers.cpp")], "-o", str(binary)])
return binary
class OfficialRoomAppCompatibilityTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.node = shutil.which("node")
compiler = shutil.which("g++") or shutil.which("clang++")
if not cls.node or not compiler:
raise AssertionError("Node.js and a host C++ compiler are required for stock app room compatibility")
cls.bundle = official_app_bundle()
temporary = tempfile.TemporaryDirectory(prefix="official-room-app-")
cls.addClassCleanup(temporary.cleanup)
cls.work = Path(temporary.name)
cls.posts = room_outputs(cls.work, compiler)
cls.login = login_binary(cls.work, compiler)
def app(self, mode, **values):
config = self.work / (mode + ".json")
config.write_text(json.dumps({"bundle": str(self.bundle), "mode": mode, **values}), encoding="ascii")
return json.loads(checked([self.node, str(FIXTURES / "room_contract.js"), str(config)], timeout=20))
def test_actual_app_room_decode_storage_and_maximum_topic(self):
result = self.app("posts", posts=self.posts)
self.assertEqual(result["post_cases"], 13)
self.assertEqual(result["ordered_timestamps"], [100, 200, 1000, 1100])
self.assertEqual(result["negative_controls"], 3)
def test_actual_app_guest_admin_and_legacy_login(self):
requests = self.app("build_logins")
self.assertEqual([request["password"] for request in requests], ["", "admin", "legacy"])
replies = []
for format, request in enumerate(requests):
output = checked([str(self.login), request["command"], str(format)], timeout=20)
kind, sent, frame = output.strip().split(":")
self.assertEqual(kind, "LOGIN")
replies.append({**request, "sent": sent, "frame": frame,
"admin": 1 if format == 1 else 0,
"timestamp": None if format == 2 else 0x76543210,
"permissions": None if format == 2 else (3 if format else 1),
"firmware_level": None if format == 2 else 13})
result = self.app("logins", logins=replies)
self.assertEqual(result["login_cases"], 3)
self.assertTrue(result["guest_and_admin_distinct"])
self.assertTrue(result["wrong_server_ignored"])
if __name__ == "__main__":
unittest.main()