Initial Frame Flow read-only Console adapter

This commit is contained in:
way
2026-10-17 22:51:01 +08:00
commit e5331caa41
8 changed files with 514 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
import importlib.util
import json
import subprocess
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from unittest.mock import patch
import pytest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location(
"frame_adapter", ROOT / "plugin/worker.py"
)
worker = importlib.util.module_from_spec(spec)
spec.loader.exec_module(worker)
@pytest.fixture
def endpoint():
calls = []
data = {
"/health": {"status": "ok"},
"/ready": {"status": "ready", "checks": {"sqlite": True}},
"/api/v1/devices": [
{
"id": "frame",
"name": "Frame",
"status": "offline",
"power": {"battery_percent": 45},
}
],
"/api/v1/jobs": [{"id": "morning", "enabled": True, "last_result": "FAILED"}],
}
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
calls.append((self.path, self.headers.get("Authorization")))
value = data[self.path]
self.send_response(
503 if self.path == "/ready" and value["status"] == "not_ready" else 200
)
self.end_headers()
self.wfile.write(json.dumps(value).encode())
def log_message(self, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever)
thread.start()
device = {
"id": "test",
"connection": {"url": f"http://127.0.0.1:{server.server_port}"},
"credentials": {"token": "private-test-token"},
}
yield device, data, calls
server.shutdown()
server.server_close()
thread.join()
def test_collect_and_not_ready(endpoint):
device, data, calls = endpoint
snapshot = worker.collect(device)
assert {a["id"] for a in snapshot["alerts"]} == {
"DEVICE_OFFLINE:frame",
"JOB_FAILED:morning",
}
assert snapshot["alerts_complete"] is True
assert len(snapshot["resources"]) == 3
assert all(
auth == "Bearer private-test-token"
for path, auth in calls
if path.startswith("/api/")
)
data["/ready"]["status"] = "not_ready"
assert "SERVICE_NOT_READY" in {a["id"] for a in worker.collect(device)["alerts"]}
def test_no_result_is_not_success(endpoint):
device, data, _ = endpoint
data["/api/v1/jobs"][0]["last_result"] = None
assert not any(
m["key"] == "last_run_success" for m in worker.collect(device)["metrics"]
)
data["/api/v1/devices"] = {}
with pytest.raises(worker.AdapterError, match="INVALID_RESPONSE"):
worker.collect(device)
def test_rpc_and_write_rejected(endpoint):
device, _, _ = endpoint
messages = [
{
"jsonrpc": "2.0",
"id": 1,
"method": "adapter.initialize",
"params": {"protocol_version": 1},
},
{
"jsonrpc": "2.0",
"id": 2,
"method": "device.collect",
"params": {"device": device},
},
{
"jsonrpc": "2.0",
"id": 3,
"method": "device.write",
"params": {"device": device},
},
]
result = subprocess.run(
[sys.executable, str(ROOT / "plugin/worker.py")],
input="".join(json.dumps(m) + "\n" for m in messages),
text=True,
capture_output=True,
timeout=15,
check=True,
)
replies = {r["id"]: r for r in map(json.loads, result.stdout.splitlines())}
assert replies[1]["result"]["id"] == "pigway-frame-flow"
assert replies[2]["result"]["schema_version"] == 1
assert replies[3]["error"]["data"] == "UNSUPPORTED"
assert "private-test-token" not in result.stdout + result.stderr
def test_auth_and_redirect_are_not_followed(endpoint):
from urllib.error import HTTPError
device, _, _ = endpoint
for status, code in ((401, "AUTH_FAILED"), (302, "DEVICE_API_ERROR")):
with patch.object(worker, "build_opener") as opener:
opener.return_value.open.side_effect = HTTPError(
"url", status, "private", {}, None
)
with pytest.raises(worker.AdapterError, match=code):
worker.request(device, "/api/v1/jobs")
def test_current_frame_flow_api_contract(tmp_path):
pytest.importorskip("pigway_frame_flow")
from fastapi.testclient import TestClient
from pigway_frame_flow.api.app import create_app
from pigway_frame_flow.settings import Settings
settings = Settings(
data_dir=tmp_path / "data",
config_dir=tmp_path / "config",
api_token="contract-token",
mock=True,
)
app = create_app(settings, start_worker=False)
with TestClient(app) as client:
client.headers["Authorization"] = "Bearer contract-token"
def request(device, path):
response = client.get(path)
assert response.status_code == 200
return response.json()
with patch.object(worker, "request", side_effect=request):
snapshot = worker.collect({})
assert snapshot["alerts_complete"] is True
assert any(
m["key"] == "service_ready" and m["value"] == 1 for m in snapshot["metrics"]
)
assert any(r["type"] == "device" for r in snapshot["resources"])