@@ -70,13 +70,13 @@ def test_insert_citation_creates_std_reference(monkeypatch, engine, fresh_clause
7070 with engine .connect () as conn :
7171 row = conn .execute (text (
7272 "SELECT source_clause_id, target_clause_id, citation_text, "
73- "confidence, verified_by FROM std_reference WHERE id=:i"
73+ "confidence, inserted_by FROM std_reference WHERE id=:i"
7474 ), {"i" : ref_id }).first ()
7575 assert str (row .source_clause_id ) == cid
7676 assert str (row .target_clause_id ) == target_clause
7777 assert row .citation_text == "test snippet"
7878 assert float (row .confidence ) == 0.85
79- assert row .verified_by == "admin"
79+ assert row .inserted_by == "admin"
8080 finally :
8181 with engine .begin () as c :
8282 c .execute (text ("DELETE FROM std_document WHERE id=:d" ),
@@ -92,3 +92,197 @@ def test_insert_citation_rejects_invalid_kind(monkeypatch, fresh_clause):
9292 r = _client ().post ("/api/std/citation/insert" ,
9393 json = {"clause_id" : cid , "candidate" : cand })
9494 assert r .status_code == 400
95+
96+
97+ # ---------------------------------------------------------------------------
98+ # Wave 3 v1 fixes: Fix #3 (target FK dispatch) + Fix #4 (empty text guard)
99+ # + Fix #5 (inserted_by / verification_status)
100+ # ---------------------------------------------------------------------------
101+
102+ import uuid as _uuid_mod
103+
104+
105+ def _seed_data_element (engine , version_id ):
106+ de_id = str (_uuid_mod .uuid4 ())
107+ with engine .begin () as conn :
108+ conn .execute (text (
109+ "INSERT INTO std_data_element (id, document_version_id, "
110+ "name_zh, code) VALUES (:i, :v, '测试要素', :c)"
111+ ), {"i" : de_id , "v" : version_id , "c" : f"DE-W3-{ de_id [:6 ]} " })
112+ return de_id
113+
114+
115+ def _seed_term (engine , version_id ):
116+ t_id = str (_uuid_mod .uuid4 ())
117+ with engine .begin () as conn :
118+ conn .execute (text (
119+ "INSERT INTO std_term (id, document_version_id, term_code, "
120+ "name_zh, definition) VALUES (:i, :v, :tc, '测试术语', '定义')"
121+ ), {"i" : t_id , "v" : version_id , "tc" : f"TC-W3-{ t_id [:6 ]} " })
122+ return t_id
123+
124+
125+ def test_citation_insert_data_element_target (monkeypatch , engine , fresh_clause ):
126+ """Fix #3: target_kind=std_data_element writes target_data_element_id,
127+ not target_clause_id."""
128+ cid , _ , vid = fresh_clause
129+ de_id = _seed_data_element (engine , vid )
130+ _auth_user (monkeypatch , username = "admin" , role = "admin" )
131+ ref_id = None
132+ try :
133+ r = _client ().post ("/api/std/citation/insert" , json = {
134+ "clause_id" : cid ,
135+ "candidate" : {
136+ "kind" : "std_data_element" ,
137+ "target_id" : de_id ,
138+ "snippet" : "数据要素引用" ,
139+ "extra" : {"confidence" : 0.85 },
140+ },
141+ })
142+ assert r .status_code == 200 , r .text
143+ ref_id = r .json ()["ref_id" ]
144+ with engine .connect () as conn :
145+ row = conn .execute (text (
146+ "SELECT target_kind, target_clause_id, target_data_element_id, "
147+ "target_term_id, inserted_by, verified_by, verification_status "
148+ "FROM std_reference WHERE id=:i"
149+ ), {"i" : ref_id }).first ()
150+ assert row [0 ] == "std_data_element"
151+ assert row [1 ] is None
152+ assert str (row [2 ]) == de_id
153+ assert row [3 ] is None
154+ assert row [4 ] == "admin" # inserted_by populated (Fix #5)
155+ assert row [5 ] is None # verified_by NULL (Fix #5)
156+ assert row [6 ] == "pending" # verification_status default (Fix #5)
157+ finally :
158+ with engine .begin () as c :
159+ # ON DELETE CASCADE from std_data_element → std_reference
160+ c .execute (text ("DELETE FROM std_data_element WHERE id=:i" ), {"i" : de_id })
161+
162+
163+ def test_citation_insert_term_target (monkeypatch , engine , fresh_clause ):
164+ """Fix #3: target_kind=std_term writes target_term_id."""
165+ cid , _ , vid = fresh_clause
166+ t_id = _seed_term (engine , vid )
167+ _auth_user (monkeypatch , username = "admin" , role = "admin" )
168+ try :
169+ r = _client ().post ("/api/std/citation/insert" , json = {
170+ "clause_id" : cid ,
171+ "candidate" : {
172+ "kind" : "std_term" ,
173+ "target_id" : t_id ,
174+ "snippet" : "术语引用" ,
175+ "extra" : {"confidence" : 0.75 },
176+ },
177+ })
178+ assert r .status_code == 200 , r .text
179+ ref_id = r .json ()["ref_id" ]
180+ with engine .connect () as conn :
181+ row = conn .execute (text (
182+ "SELECT target_kind, target_term_id, target_clause_id "
183+ "FROM std_reference WHERE id=:i"
184+ ), {"i" : ref_id }).first ()
185+ assert row [0 ] == "std_term"
186+ assert str (row [1 ]) == t_id
187+ assert row [2 ] is None
188+ finally :
189+ with engine .begin () as c :
190+ # ON DELETE CASCADE from std_term → std_reference
191+ c .execute (text ("DELETE FROM std_term WHERE id=:i" ), {"i" : t_id })
192+
193+
194+ def test_citation_insert_clause_target_still_works (monkeypatch , engine , fresh_clause ):
195+ """Regression: target_kind=std_clause still works post-fix."""
196+ cid , _ , _ = fresh_clause
197+ _auth_user (monkeypatch , username = "admin" , role = "admin" )
198+ ref_id = None
199+ try :
200+ r = _client ().post ("/api/std/citation/insert" , json = {
201+ "clause_id" : cid ,
202+ "candidate" : {
203+ "kind" : "std_clause" ,
204+ "target_id" : cid , # self-reference for test purposes
205+ "snippet" : "条款引用" ,
206+ "extra" : {"confidence" : 0.9 },
207+ },
208+ })
209+ assert r .status_code == 200 , r .text
210+ ref_id = r .json ()["ref_id" ]
211+ finally :
212+ if ref_id :
213+ with engine .begin () as c :
214+ c .execute (text ("DELETE FROM std_reference WHERE id=:i" ), {"i" : ref_id })
215+
216+
217+ def test_citation_insert_empty_text_rejected (monkeypatch , fresh_clause ):
218+ """Fix #4: empty citation_text returns 400."""
219+ cid , _ , _ = fresh_clause
220+ _auth_user (monkeypatch , username = "admin" , role = "admin" )
221+ r = _client ().post ("/api/std/citation/insert" , json = {
222+ "clause_id" : cid ,
223+ "candidate" : {
224+ "kind" : "std_clause" ,
225+ "target_id" : cid ,
226+ "snippet" : "" ,
227+ "extra" : {"confidence" : 0.5 },
228+ },
229+ })
230+ assert r .status_code == 400
231+ assert "citation_text" in r .json ().get ("error" , "" )
232+
233+
234+ def test_citation_insert_whitespace_text_rejected (monkeypatch , fresh_clause ):
235+ """Fix #4: whitespace-only citation_text returns 400."""
236+ cid , _ , _ = fresh_clause
237+ _auth_user (monkeypatch , username = "admin" , role = "admin" )
238+ r = _client ().post ("/api/std/citation/insert" , json = {
239+ "clause_id" : cid ,
240+ "candidate" : {
241+ "kind" : "std_clause" ,
242+ "target_id" : cid ,
243+ "snippet" : " \n \t " ,
244+ "extra" : {"confidence" : 0.5 },
245+ },
246+ })
247+ assert r .status_code == 400
248+
249+
250+ def test_citation_insert_web_snapshot_target (monkeypatch , engine , fresh_clause ):
251+ """Regression: web_snapshot target writes target_url and snapshot_id."""
252+ cid , _ , _ = fresh_clause
253+ snap_id = str (_uuid_mod .uuid4 ())
254+ with engine .begin () as conn :
255+ conn .execute (text (
256+ "INSERT INTO std_web_snapshot (id, url, http_status, "
257+ "extracted_text) VALUES (:i, 'https://example.com/x', 200, "
258+ "'snippet')"
259+ ), {"i" : snap_id })
260+ _auth_user (monkeypatch , username = "admin" , role = "admin" )
261+ ref_id = None
262+ try :
263+ r = _client ().post ("/api/std/citation/insert" , json = {
264+ "clause_id" : cid ,
265+ "candidate" : {
266+ "kind" : "web_snapshot" ,
267+ "target_id" : snap_id ,
268+ "target_url" : "https://example.com/x" ,
269+ "snippet" : "网页引用" ,
270+ "extra" : {"confidence" : 0.6 },
271+ },
272+ })
273+ assert r .status_code == 200 , r .text
274+ ref_id = r .json ()["ref_id" ]
275+ with engine .connect () as conn :
276+ row = conn .execute (text (
277+ "SELECT target_kind, target_url, snapshot_id "
278+ "FROM std_reference WHERE id=:i"
279+ ), {"i" : ref_id }).first ()
280+ assert row [0 ] == "web_snapshot"
281+ assert row [1 ] == "https://example.com/x"
282+ assert str (row [2 ]) == snap_id
283+ finally :
284+ if ref_id :
285+ with engine .begin () as c :
286+ c .execute (text ("DELETE FROM std_reference WHERE id=:i" ), {"i" : ref_id })
287+ with engine .begin () as c :
288+ c .execute (text ("DELETE FROM std_web_snapshot WHERE id=:i" ), {"i" : snap_id })
0 commit comments