@@ -1814,6 +1814,92 @@ def test_generate_diff_and_apply_create_vlan_translation_policy(self):
18141814 self .assertEqual (new_policy .description , "Policy for VLAN translation" )
18151815 self .assertEqual (new_policy .owner .name , f"Owner { owner_uuid } " )
18161816
1817+ def test_multiobject_cf_rediff_noop (self ):
1818+ """
1819+ Test that re-diffing a device with multiobject custom field produces no changes.
1820+
1821+ INT-219: multiobject custom field values are order-insensitive (sets),
1822+ but the differ was comparing them as ordered lists. This caused phantom
1823+ changesets on every re-diff because the "before" IDs (from queryset,
1824+ ordered by name) didn't match the "desired" IDs (sorted numerically).
1825+ """
1826+ device_cf = CustomField .objects .create (
1827+ name = 'int219_multi_sites' ,
1828+ type = CustomFieldTypeChoices .TYPE_MULTIOBJECT ,
1829+ required = False ,
1830+ related_object_type = ObjectType .objects .get_for_model (Site ),
1831+ )
1832+ device_object_type = ObjectType .objects .get_for_model (Device )
1833+ device_cf .object_types .set ([device_object_type ])
1834+ device_cf .save ()
1835+
1836+ # Pre-create "Gamma" so it gets a LOWER ID than Alpha and Beta.
1837+ # When diff+apply later creates Alpha and Beta, they get higher IDs.
1838+ # This ensures name-alphabetical order (Alpha, Beta, Gamma) differs
1839+ # from numeric ID order (Gamma, Alpha, Beta) — which triggers the bug.
1840+ Site .objects .create (name = "INT219-Site-Gamma" , slug = "int219-site-gamma" )
1841+
1842+ payload = {
1843+ "timestamp" : 1 ,
1844+ "object_type" : "dcim.device" ,
1845+ "entity" : {
1846+ "device" : {
1847+ "name" : "INT219-Test-Device" ,
1848+ "role" : {"name" : "INT219-Role" },
1849+ "site" : {"name" : "INT219-Site-Primary" },
1850+ "device_type" : {
1851+ "model" : "INT219-Model" ,
1852+ "manufacturer" : {"name" : "INT219-Manufacturer" },
1853+ },
1854+ "serial" : "INT219-SERIAL-001" ,
1855+ "custom_fields" : {
1856+ "int219_multi_sites" : {
1857+ "multiple_objects" : [
1858+ {"site" : {"name" : "INT219-Site-Alpha" }},
1859+ {"site" : {"name" : "INT219-Site-Beta" }},
1860+ {"site" : {"name" : "INT219-Site-Gamma" }},
1861+ ],
1862+ },
1863+ },
1864+ },
1865+ },
1866+ }
1867+
1868+ # First diff+apply: creates the device, Alpha, Beta (Gamma already exists)
1869+ self .diff_and_apply (payload )
1870+ device = Device .objects .get (name = "INT219-Test-Device" )
1871+ self .assertIsNotNone (device )
1872+ self .assertEqual (len (device .custom_field_data ['int219_multi_sites' ]), 3 )
1873+
1874+ # Verify IDs are NOT in alphabetical-name order (precondition for the bug)
1875+ alpha = Site .objects .get (name = "INT219-Site-Alpha" )
1876+ beta = Site .objects .get (name = "INT219-Site-Beta" )
1877+ gamma = Site .objects .get (name = "INT219-Site-Gamma" )
1878+ name_order_ids = [alpha .pk , beta .pk , gamma .pk ]
1879+ numeric_order_ids = sorted (name_order_ids )
1880+ self .assertNotEqual (
1881+ name_order_ids , numeric_order_ids ,
1882+ "Test precondition failed: IDs happen to match name order. "
1883+ "Pre-creating Gamma should have given it a lower ID than Alpha/Beta."
1884+ )
1885+
1886+ # Step 2: Re-diff with the exact same payload.
1887+ # Before the fix, this produces a false "update" changeset because
1888+ # cf.serialize() returns IDs in queryset name-order [alpha, beta, gamma]
1889+ # but the transformer sorts resolved IDs numerically [gamma, alpha, beta].
1890+ response = self .client .post (
1891+ self .diff_url , data = payload , format = "json" , ** self .authorization_header
1892+ )
1893+ self .assertEqual (response .status_code , status .HTTP_200_OK )
1894+ cs = response .json ().get ("change_set" , {})
1895+ changes = cs .get ("changes" , [])
1896+
1897+ # The re-diff should produce NO changes — the data hasn't changed.
1898+ self .assertEqual (
1899+ changes , [],
1900+ f"Expected no changes on re-diff, but got: { changes } "
1901+ )
1902+
18171903 def diff_and_apply (self , payload ):
18181904 """Diff and apply the payload."""
18191905 response1 = self .client .post (
0 commit comments