Release version management: single VERSION file as source of truth
CI / Gateway plugin tests (push) Successful in 5m19s
CI / Kotlin tests (android host + desktop) (push) Successful in 7m0s

- VERSION at repo root (0.1.2); bump it to cut a release
- App: generated AppVersion.kt (config-cache-safe Gradle task with
  VERSION as declared input) shown in Settings; sent to the gateway
  via X-Iris-App-Version header on the SSE open
- Gateway: reports its own version in hello.ack server_caps.app_version
  (read from the repo-root VERSION via the plugin symlink); stores the
  app's version in the device registry caps (merge, not overwrite, so
  an old app reconnecting without the header doesn't wipe it)
- Settings: app + gateway version rows, mismatch hint, and a best-effort
  Gitea latest-release check (ReleaseCheck) with an 'update available' hint
- Release workflow: reads VERSION from the repo (no manual input), with
  a guard against an empty file
- Docs: frames.schema.json + 04-wire-protocol.md updated for app_version
This commit is contained in:
ARIA committed 2026-08-25 14:42:39 +02:00
1 parent f3f1b37221
commit fb980d12b4
17 files changed
+422 -37

No files matched your search

+55
View File
@@ -431,6 +431,61 @@ async def upload_file(
# ── Lifecycle ───────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_hello_ack_advertises_app_version_and_sse_header_is_stored(
adapter, ws_client
):
"""hello.ack server_caps carries the gateway's release version, and the
app's X-Iris-App-Version header (sent on the SSE open) is stored in the
device registry's caps JSON (docs/04)."""
client, ack = ws_client
caps = ack["payload"]["server_caps"]
assert caps["app_version"] == sys.modules["iris_plugin_under_test"].version.plugin_version()
# The test client does not send the header; open a second SSE stream that
# does, and check the device registry picked it up.
from http.client import HTTPConnection
conn = HTTPConnection("127.0.0.1", adapter._http_server.bound_port, timeout=5)
conn.request(
"GET",
"/v1/events?cursor=0",
headers={
"Authorization": f"Bearer {TOKEN}",
"X-Iris-Device": DEVICE_ID,
"X-Iris-App-Version": "9.9.9",
},
)
resp = conn.getresponse()
assert resp.status == 200
resp.read(1) # don't hold the stream
conn.close()
device = adapter._devices.get(DEVICE_ID)
assert device is not None
assert device["caps"].get("app_version") == "9.9.9"
# A reconnect from an OLD app build (no X-Iris-App-Version header) must
# NOT wipe the previously stored version — the caps merge preserves it.
conn2 = HTTPConnection("127.0.0.1", adapter._http_server.bound_port, timeout=5)
conn2.request(
"GET",
"/v1/events?cursor=0",
headers={
"Authorization": f"Bearer {TOKEN}",
"X-Iris-Device": DEVICE_ID,
},
)
resp2 = conn2.getresponse()
assert resp2.status == 200
resp2.read(1)
conn2.close()
device2 = adapter._devices.get(DEVICE_ID)
assert device2 is not None
assert device2["caps"].get("app_version") == "9.9.9"
@pytest.mark.asyncio
async def test_disconnect_broadcasts_status_restarting(adapter):
"""Teardown broadcasts ``status{state=restarting}`` before closing the