@@ -1835,6 +1835,270 @@ def test_flag_dependencies_without_context_raises_inconclusive(self):
18351835 self .assertIn ("Cannot evaluate flag dependency" , str (cm .exception ))
18361836 self .assertIn ("some-flag" , str (cm .exception ))
18371837
1838+ @mock .patch ("posthog.client.flags" )
1839+ @mock .patch ("posthog.client.get" )
1840+ def test_multi_level_multivariate_dependency_chain (self , patch_get , patch_flags ):
1841+ """Test multi-level multivariate dependency chain: dependent-flag -> intermediate-flag -> leaf-flag"""
1842+ client = Client (FAKE_TEST_API_KEY , personal_api_key = FAKE_TEST_API_KEY )
1843+ client .feature_flags = [
1844+ # Leaf flag: multivariate with "control" and "test" variants using person property overrides
1845+ {
1846+ "id" : 1 ,
1847+ "name" : "Leaf Flag" ,
1848+ "key" : "leaf-flag" ,
1849+ "active" : True ,
1850+ "rollout_percentage" : 100 ,
1851+ "filters" : {
1852+ "groups" : [
1853+ {
1854+ "properties" : [
1855+ {
1856+ "key" : "email" ,
1857+ "type" : "person" ,
1858+ 1859+ "operator" : "exact" ,
1860+ }
1861+ ],
1862+ "rollout_percentage" : 100 ,
1863+ "variant" : "control" ,
1864+ },
1865+ {
1866+ "properties" : [
1867+ {
1868+ "key" : "email" ,
1869+ "type" : "person" ,
1870+ 1871+ "operator" : "exact" ,
1872+ }
1873+ ],
1874+ "rollout_percentage" : 100 ,
1875+ "variant" : "test" ,
1876+ },
1877+ {"rollout_percentage" : 50 , "variant" : "control" }, # Default fallback
1878+ ],
1879+ "multivariate" : {
1880+ "variants" : [
1881+ {
1882+ "key" : "control" ,
1883+ "name" : "Control" ,
1884+ "rollout_percentage" : 50 ,
1885+ },
1886+ {"key" : "test" , "name" : "Test" , "rollout_percentage" : 50 },
1887+ ]
1888+ },
1889+ },
1890+ },
1891+ # Intermediate flag: multivariate with "blue" and "green" variants, depends on leaf-flag="control"
1892+ {
1893+ "id" : 2 ,
1894+ "name" : "Intermediate Flag" ,
1895+ "key" : "intermediate-flag" ,
1896+ "active" : True ,
1897+ "rollout_percentage" : 100 ,
1898+ "filters" : {
1899+ "groups" : [
1900+ {
1901+ "properties" : [
1902+ {
1903+ "key" : "leaf-flag" ,
1904+ "operator" : "flag_evaluates_to" ,
1905+ "value" : "control" ,
1906+ "type" : "flag" ,
1907+ "dependency_chain" : ["leaf-flag" ],
1908+ },
1909+ {
1910+ "key" : "variant_type" ,
1911+ "type" : "person" ,
1912+ "value" : "blue" ,
1913+ "operator" : "exact" ,
1914+ }
1915+ ],
1916+ "rollout_percentage" : 100 ,
1917+ "variant" : "blue" ,
1918+ },
1919+ {
1920+ "properties" : [
1921+ {
1922+ "key" : "leaf-flag" ,
1923+ "operator" : "flag_evaluates_to" ,
1924+ "value" : "control" ,
1925+ "type" : "flag" ,
1926+ "dependency_chain" : ["leaf-flag" ],
1927+ },
1928+ {
1929+ "key" : "variant_type" ,
1930+ "type" : "person" ,
1931+ "value" : "green" ,
1932+ "operator" : "exact" ,
1933+ }
1934+ ],
1935+ "rollout_percentage" : 100 ,
1936+ "variant" : "green" ,
1937+ }
1938+ ],
1939+ "multivariate" : {
1940+ "variants" : [
1941+ {"key" : "blue" , "name" : "Blue" , "rollout_percentage" : 50 },
1942+ {"key" : "green" , "name" : "Green" , "rollout_percentage" : 50 },
1943+ ]
1944+ },
1945+ },
1946+ },
1947+ # Dependent flag: boolean flag that depends on intermediate-flag="blue"
1948+ {
1949+ "id" : 3 ,
1950+ "name" : "Dependent Flag" ,
1951+ "key" : "dependent-flag" ,
1952+ "active" : True ,
1953+ "rollout_percentage" : 100 ,
1954+ "filters" : {
1955+ "groups" : [
1956+ {
1957+ "properties" : [
1958+ {
1959+ "key" : "intermediate-flag" ,
1960+ "operator" : "flag_evaluates_to" ,
1961+ "value" : "blue" ,
1962+ "type" : "flag" ,
1963+ "dependency_chain" : [
1964+ "leaf-flag" ,
1965+ "intermediate-flag" ,
1966+ ],
1967+ }
1968+ ],
1969+ "rollout_percentage" : 100 ,
1970+ }
1971+ ],
1972+ },
1973+ },
1974+ ]
1975+
1976+ # Test using person properties and variant overrides to ensure predictable variants
1977+
1978+ # Test 1: Make sure the leaf flag evaluates to the variant we expect using email overrides
1979+ self .assertEqual (
1980+ "control" ,
1981+ client .get_feature_flag (
1982+ "leaf-flag" ,
1983+ "any-user" ,
1984+ person_properties = {
"email" :
"[email protected] " },
1985+ ),
1986+ )
1987+ self .assertEqual (
1988+ "test" ,
1989+ client .get_feature_flag (
1990+ "leaf-flag" ,
1991+ "any-user" ,
1992+ person_properties = {
"email" :
"[email protected] " },
1993+ ),
1994+ )
1995+
1996+ # Test 2: Make sure the intermediate flag evaluates to the expected variants when dependency is satisfied
1997+ self .assertEqual (
1998+ "blue" ,
1999+ client .get_feature_flag (
2000+ "intermediate-flag" ,
2001+ "any-user" ,
2002+ person_properties = {
2003+ 2004+ "variant_type" : "blue"
2005+ },
2006+ ),
2007+ )
2008+
2009+ self .assertEqual (
2010+ "green" ,
2011+ client .get_feature_flag (
2012+ "intermediate-flag" ,
2013+ "any-user" ,
2014+ person_properties = {
2015+ 2016+ "variant_type" : "green"
2017+ },
2018+ ),
2019+ )
2020+
2021+ # Test 3: Make sure the intermediate flag evaluates to false when leaf dependency fails
2022+ self .assertEqual (
2023+ False ,
2024+ client .get_feature_flag (
2025+ "intermediate-flag" ,
2026+ "any-user" ,
2027+ person_properties = {
2028+ "email" :
"[email protected] " ,
# This makes leaf-flag="test", breaking dependency 2029+ "variant_type" : "blue"
2030+ },
2031+ ),
2032+ )
2033+
2034+ # Test 4: When leaf-flag="control", intermediate="blue", dependent should be true
2035+ self .assertEqual (
2036+ True ,
2037+ client .get_feature_flag (
2038+ "dependent-flag" ,
2039+ "any-user" ,
2040+ person_properties = {
2041+ 2042+ "variant_type" : "blue"
2043+ },
2044+ ),
2045+ )
2046+
2047+ # Test 5: When leaf-flag="control", intermediate="green", dependent should be false
2048+ self .assertEqual (
2049+ False ,
2050+ client .get_feature_flag (
2051+ "dependent-flag" ,
2052+ "any-user" ,
2053+ person_properties = {
2054+ 2055+ "variant_type" : "green"
2056+ },
2057+ ),
2058+ )
2059+
2060+ # Test 6: When leaf-flag="test", intermediate is False, dependent should be false
2061+ self .assertEqual (
2062+ False ,
2063+ client .get_feature_flag (
2064+ "dependent-flag" ,
2065+ "any-user" ,
2066+ person_properties = {
2067+ 2068+ "variant_type" : "blue"
2069+ },
2070+ ),
2071+ )
2072+
2073+ def test_matches_dependency_value (self ):
2074+ """Test the matches_dependency_value function logic"""
2075+ from posthog .feature_flags import matches_dependency_value
2076+
2077+ # String variant matches string exactly (case-insensitive)
2078+ self .assertTrue (matches_dependency_value ("control" , "control" ))
2079+ self .assertTrue (matches_dependency_value ("Control" , "control" ))
2080+ self .assertTrue (matches_dependency_value ("CONTROL" , "control" ))
2081+ self .assertFalse (matches_dependency_value ("test" , "control" ))
2082+
2083+ # String variant matches boolean true (any variant is truthy)
2084+ self .assertTrue (matches_dependency_value ("control" , True ))
2085+ self .assertTrue (matches_dependency_value ("test" , True ))
2086+ self .assertFalse (matches_dependency_value ("control" , False ))
2087+
2088+ # Boolean matches boolean exactly
2089+ self .assertTrue (matches_dependency_value (True , True ))
2090+ self .assertTrue (matches_dependency_value (False , False ))
2091+ self .assertFalse (matches_dependency_value (True , False ))
2092+ self .assertFalse (matches_dependency_value (False , True ))
2093+
2094+ # Empty string doesn't match
2095+ self .assertFalse (matches_dependency_value ("" , True ))
2096+ self .assertFalse (matches_dependency_value ("" , "control" ))
2097+
2098+ # Type mismatches
2099+ self .assertFalse (matches_dependency_value ("control" , 123 ))
2100+ self .assertFalse (matches_dependency_value (True , "control" ))
2101+
18382102 @mock .patch ("posthog.client.Poller" )
18392103 @mock .patch ("posthog.client.get" )
18402104 def test_load_feature_flags (self , patch_get , patch_poll ):
0 commit comments