@@ -115,3 +115,67 @@ async def test_learn_endpoint_mismatch_classification(proxy, cache_db, monkeypat
115115 assert result == expected_mismatch , (
116116 f"predicted={ predicted } actual={ actual } expected={ expected_mismatch } got={ result } "
117117 )
118+
119+
120+ @pytest .mark .asyncio
121+ async def test_learn_endpoint_synthesizes_nzb_id_when_release_is_new (
122+ proxy , cache_db , monkeypatch ,
123+ ):
124+ """Two different releases with NO previous cache entry must NOT collide
125+ on the same empty primary key. Each gets a stable synthetic nzb_id
126+ derived from its release_name so INSERT OR REPLACE doesn't clobber
127+ the other."""
128+ monkeypatch .setattr (proxy , "PROWLARR_API_KEY" , "k" )
129+
130+ from aiohttp .test_utils import make_mocked_request
131+
132+ async def post (body_json : str ):
133+ req = make_mocked_request (
134+ "POST" , "/learn/imported" ,
135+ headers = {"X-Api-Key" : "k" , "Content-Type" : "application/json" },
136+ )
137+ async def text (): return body_json
138+ req .text = text
139+ return await proxy ._handle_learn_imported (req )
140+
141+ # First release — new to cache
142+ resp1 = await post (json .dumps ({
143+ "release_name" : "FirstRelease.2026.NORDiC.1080p-FOO" ,
144+ "audio_languages" : ["dan" , "eng" ],
145+ "subtitle_languages" : ["dan" ],
146+ }))
147+ assert resp1 .status == 200
148+
149+ # Second release — also new to cache
150+ resp2 = await post (json .dumps ({
151+ "release_name" : "SecondRelease.2026.NORDiC.1080p-BAR" ,
152+ "audio_languages" : ["dan" , "eng" ],
153+ "subtitle_languages" : ["dan" ],
154+ }))
155+ assert resp2 .status == 200
156+
157+ # BOTH releases must be present in nfo_cache after the two POSTs.
158+ # Pre-fix bug: both writes used nzb_id='' which is the PK, so the
159+ # second INSERT OR REPLACE clobbered the first.
160+ async with cache_db .execute (
161+ "SELECT nzb_id, release_name FROM nfo_cache "
162+ "WHERE release_name IN (?, ?)" ,
163+ ("FirstRelease.2026.NORDiC.1080p-FOO" ,
164+ "SecondRelease.2026.NORDiC.1080p-BAR" ),
165+ ) as cur :
166+ rows = await cur .fetchall ()
167+
168+ rel_names = {r [1 ] for r in rows }
169+ assert "FirstRelease.2026.NORDiC.1080p-FOO" in rel_names , (
170+ f"first release lost from cache: { rows !r} "
171+ )
172+ assert "SecondRelease.2026.NORDiC.1080p-BAR" in rel_names
173+
174+ # The synthetic IDs must be distinct (no PK collision)
175+ ids = {r [0 ] for r in rows }
176+ assert len (ids ) == 2 , f"expected 2 distinct nzb_ids, got: { ids !r} "
177+ # Synthetic IDs should be predictable: ffp:<hash>
178+ for nid in ids :
179+ assert nid .startswith ("ffp:" ), (
180+ f"new-release nzb_id should be synthetic 'ffp:...', got { nid !r} "
181+ )
0 commit comments