From ebe6e54017edb8ed1dda8f648bd7955d26fb2616 Mon Sep 17 00:00:00 2001 From: Anders Asheim Hennum Date: Wed, 4 Nov 2026 09:18:23 +0100 Subject: [PATCH] test(compliance): expose the legacy payload accessor to the harness Declare feature_flag_payloads and add /get_feature_flag_payload, which reads a flag's payload remotely with get_feature_flag_payload. The harness uses it to check payloads read from /flags?v=4 records. --- sdk_compliance_adapter/README.md | 1 + sdk_compliance_adapter/adapter.py | 28 ++++++++++++++++++++++++++ sdk_compliance_adapter/test_adapter.py | 25 +++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/sdk_compliance_adapter/README.md b/sdk_compliance_adapter/README.md index eae25916b..79285792d 100644 --- a/sdk_compliance_adapter/README.md +++ b/sdk_compliance_adapter/README.md @@ -25,6 +25,7 @@ The adapter implements the standard SDK adapter interface defined in the [test h - `POST /flush` - Flush pending events - `GET /state` - Return internal state - `POST /get_feature_flag` - Evaluate a flag locally or remotely +- `POST /get_feature_flag_payload` - Read a flag payload remotely with the legacy payload accessor - `POST /reload_feature_flag_definitions` - Fresh, bounded definitions readiness barrier - `POST /reset` - Reset SDK state diff --git a/sdk_compliance_adapter/adapter.py b/sdk_compliance_adapter/adapter.py index d205d1792..1e197d758 100644 --- a/sdk_compliance_adapter/adapter.py +++ b/sdk_compliance_adapter/adapter.py @@ -313,6 +313,8 @@ def health(): capabilities.append("feature_flags_local_evaluation_v1") # Remote /flags requests send ?v=4 and read the v4 flag record. capabilities.append("flags_response_v4") + # The legacy payload accessor is exposed on /get_feature_flag_payload. + capabilities.append("feature_flag_payloads") return jsonify( { "sdk_name": "posthog-python", @@ -710,6 +712,32 @@ def get_feature_flag(): return jsonify({"error": str(e)}), 500 +@app.route("/get_feature_flag_payload", methods=["POST"]) +def get_feature_flag_payload(): + """Read a flag's payload with the legacy payload accessor (remote evaluation).""" + try: + if not state.client: + return jsonify({"error": "SDK not initialized"}), 400 + data = request.json or {} + if not data.get("key") or not data.get("distinct_id"): + return jsonify({"error": "key and distinct_id are required"}), 400 + client = state.remote_client or state.client + payload = client.get_feature_flag_payload( + data["key"], + data["distinct_id"], + person_properties=data.get("person_properties"), + groups=data.get("groups"), + group_properties=data.get("group_properties"), + disable_geoip=data.get("disable_geoip"), + only_evaluate_locally=False, + ) + client.flush() + return jsonify({"success": True, "value": payload}) + except Exception as e: + logger.exception("Error reading feature flag payload") + return jsonify({"error": str(e)}), 500 + + @app.route("/reset", methods=["POST"]) def reset(): """Reset SDK state""" diff --git a/sdk_compliance_adapter/test_adapter.py b/sdk_compliance_adapter/test_adapter.py index 0f40f20fc..514963aa9 100644 --- a/sdk_compliance_adapter/test_adapter.py +++ b/sdk_compliance_adapter/test_adapter.py @@ -87,6 +87,7 @@ def test_health_opts_into_local_evaluation_without_losing_capture( capabilities = adapter.app.test_client().get("/health").json["capabilities"] assert "feature_flags_local_evaluation_v1" in capabilities assert "flags_response_v4" in capabilities + assert "feature_flag_payloads" in capabilities assert capability in capabilities assert "capture_ai_v0" in capabilities @@ -181,6 +182,30 @@ def test_force_remote_bypasses_loaded_local_definitions(adapter, monkeypatch): remote.assert_called_once() +def test_payload_route_reads_remotely_and_returns_the_parsed_payload( + adapter, monkeypatch +): + client = initialize(adapter) + remote = Mock( + return_value=posthog.client.normalize_flags_response( + { + "featureFlags": {"flag": True}, + "featureFlagPayloads": {"flag": '{"columns": 2}'}, + } + ) + ) + monkeypatch.setattr(adapter.state.remote_client, "_get_flags_decision", remote) + response = client.post( + "/get_feature_flag_payload", json={"key": "flag", "distinct_id": "user"} + ) + assert response.json == {"success": True, "value": {"columns": 2}} + remote.assert_called_once() + assert ( + client.post("/get_feature_flag_payload", json={"key": "flag"}).status_code + == 400 + ) + + def test_rejects_conflicting_evaluation_modes(adapter): client = initialize(adapter) response = client.post(