"""Service-sync compatibility tests. Verifies the legacy compatibility helpers on backend/apps/service/client.py (record, submit_event, submit_session_close, etc.) still produce the right opaque payload through the unified sync() entry point. Forward-looking contract tests live in test_service.py; this file covers the legacy shim surface so it can be deprecated cleanly later. Run with: cd backend && python -m pytest tests/test_service_legacy.py -v """ import json import os import tempfile import pytest # Sandbox the data dir before any module import touches settings on disk. p_tmpdir = tempfile.mkdtemp() os.environ.setdefault("OPENSWARM_DATA_DIR", p_tmpdir) # Captured syncs from this test run. p_captured_syncs: list[dict] = [] @pytest.fixture(autouse=True) def reset_captured_syncs(): p_captured_syncs.clear() yield p_captured_syncs.clear() @pytest.fixture(autouse=True) def install_sync_sink(): """Install a service-sync sink and decode the opaque payload back into a structured shape for assertions. The sink translates the new shape {client_state, d, t} into a legacy-compatible {kind, distinct_id, props} bag so existing tests can keep their assertions terse.""" import backend.apps.service.client as svc_client def p_sink(label: str, body: dict): cs = body.get("client_state") or {} payload = body.get("d") or body.get("payload") or {} # Infer a synthetic kind from payload shape, same dispatch logic as the cloud uses in production. if "status" in payload and "messages" in payload: status = payload.get("status", "unknown") kind = f"session.{status}" if status != "unknown" else "session.completed" props = dict(payload) elif "identity" in payload: kind = "state.update" props = dict(payload) elif "diagnostic" in payload: kind = "diagnostic.fired" props = dict(payload) elif "s" in payload and "a" in payload: kind = f"{payload['s']}.{payload['a']}" props = dict(payload.get("p") or {}) elif "surface" in payload: surface = payload.get("surface", "") action = payload.get("action", "fired") kind = f"{surface}.{action}" props = dict(payload.get("props") or {}) else: kind = "state.update" props = dict(payload) if payload.get("session_id"): props["session_id"] = payload["session_id"] if payload.get("dashboard_id"): props["dashboard_id"] = payload["dashboard_id"] props.setdefault("os", cs.get("os", "")) props.setdefault("platform", cs.get("os", "")) p_captured_syncs.append({ "kind": kind, "distinct_id": cs.get("install_id", ""), "properties": props, }) old_sink = svc_client.test_sink old_iid = svc_client.install_id svc_client.set_test_sink(p_sink) svc_client.install_id = "test-install-id" yield svc_client.set_test_sink(old_sink) svc_client.install_id = old_iid @pytest.fixture(autouse=True) def mock_settings(tmp_path): """Sandbox settings so tests don't read or write the real config.""" settings_file = tmp_path / "settings.json" settings_file.write_text(json.dumps({ "service_diagnostics_mode": "standard", "installation_id": "test-install-id", })) import backend.apps.settings.store as settings_mod old_file = settings_mod.SETTINGS_FILE settings_mod.SETTINGS_FILE = str(settings_file) yield settings_mod.SETTINGS_FILE = old_file @pytest.fixture(autouse=True) def mock_sessions_dir(tmp_path): """Use temp dir for session persistence.""" sessions_dir = tmp_path / "sessions" sessions_dir.mkdir() import backend.config.paths as paths_mod old_dir = paths_mod.SESSIONS_DIR paths_mod.SESSIONS_DIR = str(sessions_dir) yield str(sessions_dir) paths_mod.SESSIONS_DIR = old_dir def syncs(kind: str | None = None) -> list[dict]: """Return captured syncs, optionally filtered by inferred kind.""" if kind: return [s for s in p_captured_syncs if s["kind"] == kind] return list(p_captured_syncs) def last_sync(kind: str) -> dict: """Return the last captured sync of a given inferred kind.""" matching = syncs(kind) assert matching, f"No {kind} syncs captured. Got: {[s['kind'] for s in p_captured_syncs]}" return matching[-1] # Import application modules (after fixtures are wired). from backend.apps.service.client import record # --------------------------------------------------------------------------- 1. record(), legacy shim correctness --------------------------------------------------------------------------- class TestRecordBasics: def test_record_sends_payload(self): record("test.report", {"key": "value"}) s = last_sync("test.report") assert s["properties"]["key"] == "value" assert s["distinct_id"] == "test-install-id" def test_record_adds_os_and_platform(self): record("test.report", {}) s = last_sync("test.report") assert "os" in s["properties"] assert "platform" in s["properties"] def test_record_includes_session_id(self): record("test.report", {}, session_id="sess123") s = last_sync("test.report") assert s["properties"]["session_id"] == "sess123" def test_record_includes_dashboard_id(self): record("test.report", {}, dashboard_id="dash456") s = last_sync("test.report") assert s["properties"]["dashboard_id"] == "dash456" # Session-close cloud sync (the old session.completed snapshot path) was retired in favour of Haik's # per-message product-analytics bridge, so its close-fires-once + token-on-close tests were removed.