"""核查差异按确定判据结算(ADR-0139):保留现值、并集补标签、官方一致覆盖与撤回、交人。 样例取自 2026-11-04 复核页快照里的真实行,每条规则一类正例,外加「看起来像但必须交人」 的反例。 """ import contextlib import csv import importlib.util import io import json import sqlite3 import tempfile import unittest from pathlib import Path from peach import ledger_backups, web_auto_landings from peach import metadata_auto_apply as rm_auto from peach import web_contract as rm_web from peach import web_review as rm_review from peach.entities import upsert_asset_entity from peach.field_owners import USER_MANUAL, auto_owner, owner_of, write_owned_fields from support.ledger import fresh_ledger ROOT = Path(__file__).resolve().parents[1] FIELDS = ["item_key", "code", "query", "asset_id", "asset_path", "field", "field_label", "current_value", "candidates_json", "source_count", "source_profile", "status", "size_gb", "videos", "fetched_at"] COLUMNS = {"title": "catalog_title", "release_date": "release_date", "studio": "studio", "series": "series"} def _revert_script(): spec = importlib.util.spec_from_file_location( "revert_auto_landing_verification", ROOT / "scripts" / "revert_auto_landing.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module class VerificationSettlementTests(unittest.TestCase): def setUp(self): holder = tempfile.TemporaryDirectory() self.addCleanup(holder.cleanup) root = Path(holder.name) self.candidates = root / "generated" self.candidates.mkdir() self.db_path = fresh_ledger(root) self.contract = rm_web.WebContract( Path(self.db_path), candidate_root=self.candidates, logo_root=root / "logos", avatar_root=root / "avatars") self.rows = [] self.next_id = 100 # ---- 夹具 ---- def asset(self, code, *, owner=None, tags=(), **values): """一部文件名带番号的片;`values` 按复核字段名给现值,`owner` 是这些字段的归属。""" asset_id = self.next_id self.next_id += 1 with contextlib.closing(sqlite3.connect(self.db_path)) as connection, connection: connection.execute("INSERT INTO asset(id,location,path,name,medium,code) " "VALUES(?,'local',?,?,'video',?)", (asset_id, f"/x/{code}.mp4", f"{code}.mp4", code)) columns = {COLUMNS[field]: value for field, value in values.items()} if columns and owner: write_owned_fields(connection, [asset_id], columns, owner) elif columns: connection.execute( f"UPDATE asset SET {','.join(f'{name}=?' for name in columns)} WHERE id=?", (*columns.values(), asset_id)) for tag in tags: upsert_asset_entity(connection, kind="tag", name=tag, asset_id=asset_id, role="tag", source="javinizer:dmm:tag", confidence=0.75, now="now") connection.execute("INSERT INTO asset_tag(asset_id,tag,confidence,source) " "VALUES(?,?,0.75,'javinizer:dmm:tag')", (asset_id, tag)) return asset_id def row(self, code, field, current, *candidates, pinned=None): """一行核查候选。`candidates` 是 `(来源, 取值)`,第三项给 False 表示不带核查标记; `pinned` 给 `(资产编号, 路径)` 就是处理任务按文件出的那种行。""" key = f"asset:{pinned[0]}:{field}" if pinned else f"{code}:{field}" built = [] for index, (source, value, *flag) in enumerate(candidates): shown = ("、".join(item["name"] if isinstance(item, dict) else item for item in value) if isinstance(value, list) else value) built.append({ "candidate_key": f"{key}:{index}", "source": source, "value": value, "display_value": shown, "confidence": 0.75, "provider_id": code, "source_url": "", "raw_snapshot": "", **({"javdb_rule": 2, "source_id": "FixtureWork"} if source == "javdb" else {}), **({"requires_review": True} if not flag or flag[0] else {})}) self.rows.append({"item_key": key, "code": code, "query": code, "field": field, **({"asset_id": str(pinned[0]), "asset_path": pinned[1]} if pinned else {}), "field_label": field, "current_value": current, "candidates_json": json.dumps(built, ensure_ascii=False), "source_count": str(len(built)), "source_profile": "library", "status": "candidate", "size_gb": "", "videos": "1", "fetched_at": ""}) with (self.candidates / "library-metadata-field-candidates.csv").open( "w", encoding="utf-8-sig", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=FIELDS) writer.writeheader() writer.writerows(self.rows) return key def queue(self): rows, _source, _skipped = rm_review._review_rows(self.contract, "metadata_fields") return [row["item_key"] for row in rows] def settle(self): return rm_auto.auto_apply_metadata(self.contract.database, self.candidates) def outcome(self, key): row = next(dict(row) for row in self.rows if row["item_key"] == key) row["candidates"] = rm_auto._row_candidates(row, {}) with self.contract.read_connection() as connection: rm_auto.refresh_current_values(connection, [row]) return rm_auto.verification_outcome(connection, row) def value(self, asset_id, field): with contextlib.closing(sqlite3.connect(self.db_path)) as connection: column = COLUMNS[field] value, owners = connection.execute( f"SELECT {column},field_owners FROM asset WHERE id=?", (asset_id,)).fetchone() return value, owner_of(owners, column) def tags(self, asset_id): with contextlib.closing(sqlite3.connect(self.db_path)) as connection: return {row[0] for row in connection.execute( "SELECT e.canonical_name FROM asset_entity ae JOIN entity e ON e.id=ae.entity_id" " WHERE ae.asset_id=? AND ae.role='tag'", (asset_id,))} def decisions(self): with contextlib.closing(sqlite3.connect(self.db_path)) as connection: return {key: json.loads(note) for key, note in connection.execute( "SELECT item_key,note FROM review_decision WHERE category='metadata_fields'")} def assertKept(self, key, rule, asset_id, field, current): self.assertEqual(self.outcome(key).rule, rule) self.assertNotIn(key, self.queue()) self.settle() self.assertEqual(self.value(asset_id, field)[0], current) self.assertNotIn(key, self.decisions()) # ---- 保留现值:不入队、不写 ---- def test_title_that_only_differs_in_width_or_is_shorter_keeps_the_current_value(self): current = ("「いますぐ精子たくさん飲ませて…」フェラテクィーン夢実かなえのごっくんしまくり逆ナンパ" " チ○ポ5本×ホンモノザーメン11射精ぜ〜んぶごっくん大満足!!") asset_id = self.asset("MEYD-911", owner=auto_owner("r18dev"), title=current) key = self.row("MEYD-911", "title", current, ("r18dev", current, False), ("javdb", current.replace("〜", "~").replace("!!", "!!")), ("makers", "「いますぐ精子たくさん飲ませて…」フェラテクィーン夢実かなえの" "ごっくんしまくり逆ナンパ")) self.assertKept(key, "adr-0139-no-new-information", asset_id, "title", current) def test_official_masked_title_does_not_replace_the_original_wording(self): current = "身動き出来ない私は何度も凌辱されー、早漏メイドに改造された 七沢みあ" asset_id = self.asset("MIDE-633", title=current) key = self.row("MIDE-633", "title", current, ("makers", "身動き出来ない私は何度も凌●されー、早漏メイドに改造された")) self.assertKept(key, "adr-0139-no-new-information", asset_id, "title", current) def test_store_decoration_on_the_candidate_keeps_the_current_title(self): current = "女優のたまご。ハロウィン前…クンニで切ないアヘ顔に膣中大量射精" asset_id = self.asset("FC2-PPV-3977699", title=current) key = self.row("FC2-PPV-3977699", "title", current, ("fc2", "【限定顔出し15000→5000】" + current)) self.assertKept(key, "adr-0139-no-new-information", asset_id, "title", current) def test_community_only_dissent_keeps_the_current_value(self): asset_id = self.asset("STH-123", release_date="2025-10-24") key = self.row("STH-123", "release_date", "2025-10-24", ("javdb", "2025-10-16")) self.assertKept(key, "adr-0139-community-dissent", asset_id, "release_date", "2025-10-24") def test_official_source_backing_the_current_value_outweighs_another_official(self): asset_id = self.asset("HUNTC-592", release_date="2026-10-13") key = self.row("HUNTC-592", "release_date", "2026-10-13", ("dmm", "2026-10-13", False), ("r18dev", "2026-10-09"), ("avbase", "2026-10-09")) self.assertKept(key, "adr-0139-official-backs-current", asset_id, "release_date", "2026-10-13") def test_value_written_by_a_higher_ranked_official_source_stays(self): asset_id = self.asset("ROYD-083", owner=auto_owner("makers"), studio="HHH") key = self.row("ROYD-083", "studio", "HHH", ("dmm", "ロイヤル", False), ("r18dev", "Royal"), ("javdb", "ROYAL")) self.assertKept(key, "adr-0139-chain-keeps-current", asset_id, "studio", "HHH") def test_r18dev_english_studio_name_is_a_translation_not_a_dissent(self): asset_id = self.asset("SIMM-876", owner=auto_owner("dmm"), studio="しろうとまんまん") key = self.row("SIMM-876", "studio", "しろうとまんまん", ("r18dev", "Amateur Pussies"), ("javbus", "DOC"), ("javdb", "DOC")) self.assertKept(key, "adr-0139-community-dissent", asset_id, "studio", "しろうとまんまん") def test_studio_with_a_reading_or_parent_note_adds_nothing(self): asset_id = self.asset("EBOD-531", owner=auto_owner("r18dev"), studio="E-BODY") key = self.row("EBOD-531", "studio", "E-BODY", ("makers", "E-BODY(イーボディ)")) self.assertKept(key, "adr-0139-no-new-information", asset_id, "studio", "E-BODY") def test_user_owned_value_is_never_challenged(self): asset_id = self.asset("ACC-006", owner=USER_MANUAL, release_date="2016-05-29") key = self.row("ACC-006", "release_date", "2016-05-29", ("r18dev", "2009-01-08")) self.assertKept(key, "adr-0139-user-owned", asset_id, "release_date", "2016-05-29") def test_tag_candidates_that_are_a_subset_keep_the_current_tags(self): asset_id = self.asset("DASS-468", tags=("中出内射", "巨乳", "母子设定", "痴女")) key = self.row("DASS-468", "tags", "中出内射、巨乳、母子设定、痴女", ("javdb", ["痴女", "巨乳", "中出内射"])) self.assertEqual(self.outcome(key).rule, "adr-0139-no-new-information") self.assertNotIn(key, self.queue()) self.settle() self.assertEqual(self.tags(asset_id), {"中出内射", "巨乳", "母子设定", "痴女"}) self.assertEqual(self.decisions(), {}) def test_row_pinned_to_an_id_now_holding_another_code_is_kept(self): """按文件出的行,那个编号已经是别的片:谁批准都写不进去,不摆给人,也不写。""" asset_id = self.asset("ABP-602", release_date="2017-08-01") key = self.row("ABP-601", "release_date", "2017-08-01", ("makers", "2017-08-05"), pinned=(asset_id, r"B:\云下载\ABP-601A-C.mp4")) self.asset("ABP-601", release_date="2017-08-01") self.assertKept(key, "adr-0139-asset-gone", asset_id, "release_date", "2017-08-01") def test_row_pinned_to_a_relocated_file_still_lands_on_the_same_asset(self): """扫描按番号改指挪过的文件,编号不变(ADR-0131);候选件里的旧路径不挡落库。""" asset_id = self.asset("ABP-601", release_date="2017-08-01") key = self.row("ABP-601", "release_date", "2017-08-01", ("makers", "2017-08-05"), pinned=(asset_id, r"B:\云下载\ABP-601A-C.mp4")) self.assertNotIn(key, self.queue()) self.settle() self.assertEqual(self.value(asset_id, "release_date"), ("2017-08-05", "auto:makers")) def test_human_approved_tags_grow_and_keep_the_human_record(self): asset_id = self.asset("ROYD-083", tags=("中出内射", "辣妹")) # 旧映射留在候选里的来源 `4K` 说的是发行版本,并集不补。 key = self.row("ROYD-083", "tags", "中出内射、辣妹", ("makers", ["中出内射", "辣妹", "痴女", "4K"])) human = {"candidate_key": "earlier", "source": "dmm", "user_note": "批量复核"} with contextlib.closing(sqlite3.connect(self.db_path)) as connection, connection: connection.execute("INSERT INTO review_decision(category,item_key,status,note,updated_at)" " VALUES('metadata_fields',?,'approved',?,'2026-10-01')", (key, json.dumps(human, ensure_ascii=False))) self.settle() self.assertEqual(self.tags(asset_id), {"中出内射", "辣妹", "痴女"}) with contextlib.closing(sqlite3.connect(self.db_path)) as connection: status, note, updated = connection.execute( "SELECT status,note,updated_at FROM review_decision WHERE item_key=?", (key,)).fetchone() note = json.loads(note) self.assertEqual((status, updated), ("approved", "2026-10-01")) self.assertEqual({name: note[name] for name in human}, human) self.assertEqual(note["added_tags"], ["痴女"]) self.assertNotIn(key, self.queue()) # ---- 可撤写入 ---- def test_tag_union_adds_only_missing_tags_and_reverts_by_rule(self): asset_id = self.asset("ROYD-083", tags=("中出内射", "御姐", "辣妹")) key = self.row("ROYD-083", "tags", "中出内射、御姐、辣妹", ("dmm", ["辣妹", "痴女", "中出内射", "姐弟"], False), ("makers", ["中出内射", "姐弟", "辣妹", "痴女"]), ("r18dev", ["辣妹", "痴女", "姐弟", "中出内射"])) self.assertEqual(self.outcome(key).kind, rm_auto.VERIFY_UNION) self.assertNotIn(key, self.queue()) self.assertEqual(self.settle()["applied"], 1) self.assertEqual(self.tags(asset_id), {"中出内射", "御姐", "辣妹", "痴女", "姐弟"}) note = self.decisions()[key] self.assertEqual(note["rule"], rm_auto.VERIFIED_UNION_RULE) self.assertEqual(set(note["added_tags"]), {"痴女", "姐弟"}) self.assertEqual(self.settle()["applied"], 0) self.assertNotIn(key, self.queue()) output = io.StringIO() with contextlib.redirect_stdout(output): _revert_script().main(["--db", str(self.db_path), "--source", rm_auto.VERIFIED_UNION_RULE, "--apply", "--backup", str(Path(self.db_path).with_suffix(".bak"))]) self.assertEqual(self.tags(asset_id), {"中出内射", "御姐", "辣妹"}) self.assertEqual(self.decisions(), {}) def test_official_date_replaces_an_unowned_value_and_reverts_to_it(self): asset_id = self.asset("DASS-468", release_date="2024-09-06") key = self.row("DASS-468", "release_date", "2024-09-06", ("javdb", "2024-09-10"), ("makers", "2024-09-10")) self.assertNotIn(key, self.queue()) self.assertEqual(self.settle()["applied"], 1) self.assertEqual(self.value(asset_id, "release_date"), ("2024-09-10", "auto:makers")) note = self.decisions()[key] self.assertEqual(note["rule"], rm_auto.VERIFIED_REPLACE_RULE) self.assertEqual(note["previous"], [{"asset_id": asset_id, "value": "2024-09-06", "owner": ""}]) self.assertNotIn(key, self.queue()) output = io.StringIO() with contextlib.redirect_stdout(output): _revert_script().main(["--db", str(self.db_path), "--source", rm_auto.VERIFIED_REPLACE_RULE, "--apply", "--backup", str(Path(self.db_path).with_suffix(".bak"))]) self.assertEqual(self.value(asset_id, "release_date"), ("2024-09-06", "")) self.assertEqual(self.decisions(), {}) def test_official_unmasked_title_replaces_a_masked_auto_value(self): current = "身動き出来ない私は何度も凌●されー、早漏メイドに改造された 七沢みあ" asset_id = self.asset("MIDE-633", owner=auto_owner("javdb"), title=current) self.row("MIDE-633", "title", current, ("makers", "身動き出来ない私は何度も凌辱されー、早漏メイドに改造された")) self.settle() self.assertEqual(self.value(asset_id, "title"), ("身動き出来ない私は何度も凌辱されー、早漏メイドに改造された", "auto:makers")) def test_revert_leaves_a_value_someone_changed_afterwards(self): asset_id = self.asset("DASS-468", release_date="2024-09-06") self.row("DASS-468", "release_date", "2024-09-06", ("makers", "2024-09-10")) self.settle() with contextlib.closing(sqlite3.connect(self.db_path)) as connection, connection: write_owned_fields(connection, [asset_id], {"release_date": "2024-09-11"}, USER_MANUAL) with self.contract.read_connection() as connection: plan = rm_auto.planned_verified_revert(connection, rm_auto.VERIFIED_REPLACE_RULE) self.assertEqual([entry["changed"] for entry in plan], [[asset_id]]) with contextlib.closing(sqlite3.connect(self.db_path)) as connection, connection: rm_auto.revert_verified(connection, plan) self.assertEqual(self.value(asset_id, "release_date"), ("2024-09-11", USER_MANUAL)) # ---- 复核页撤回(ADR-0141) ---- def auto_landings(self, **args): return rm_web.dispatch_api_get(self.contract, "/api/review/auto-landings", args) def revert(self, items, token): return rm_web.dispatch_api_post(self.contract, "/api/review/auto-landings/revert", {"items": items, "token": token}) def test_review_page_lists_each_write_by_work_and_reverts_the_chosen_ones(self): replaced = self.asset("DASS-468", release_date="2024-09-06") edited = self.asset("DASS-469", release_date="2024-09-07") tagged = self.asset("ROYD-083", tags=("中出内射",)) first = self.row("DASS-468", "release_date", "2024-09-06", ("makers", "2024-09-10")) second = self.row("DASS-469", "release_date", "2024-09-07", ("makers", "2024-09-12")) union = self.row("ROYD-083", "tags", "中出内射", ("makers", ["中出内射", "痴女"])) self.settle() with contextlib.closing(sqlite3.connect(self.db_path)) as connection, connection: write_owned_fields(connection, [edited], {"release_date": "2024-09-13"}, USER_MANUAL) listed = {item["item_key"]: item for item in self.auto_landings()["items"]} self.assertEqual(set(listed), {first, second, union}) self.assertEqual({key: (item["asset_id"], item["code"], item["field"], item["value"], item["previous"]) for key, item in listed.items() if key != union}, {first: (replaced, "DASS-468", "发行日期", "2024-09-10", ["2024-09-06"]), second: (edited, "DASS-469", "发行日期", "2024-09-12", ["2024-09-07"])}) self.assertEqual((listed[union]["asset_id"], listed[union]["tags"]), (tagged, ["痴女"])) # 一次结算一个运行时刻:覆盖与并集的批次号带同一个时刻。 self.assertEqual(len({item["batch"].rpartition("@")[2] for item in listed.values()}), 1) self.assertIsNotNone(listed[first]["at"]) preview = self.auto_landings(items="\n".join([first, second])) self.assertEqual([(row["asset_id"], row["code"], row["current"], row["previous"]) for row in preview["restores"]], [(replaced, "DASS-468", "2024-09-10", "2024-09-06")]) self.assertEqual([row["asset_id"] for row in preview["changed"]], [edited]) self.assertEqual((preview["decisions"], preview["tags"]), (2, [])) with self.assertRaisesRegex(ValueError, "重新预览"): self.revert([first, second], "stale") self.assertEqual(self.value(replaced, "release_date"), ("2024-09-10", "auto:makers")) result = self.revert([first, second], preview["token"]) self.assertEqual((result["restored"], result["decisions"]), (1, 2)) self.assertEqual([row["asset_id"] for row in result["changed"]], [edited]) self.assertEqual(self.value(replaced, "release_date"), ("2024-09-06", "")) self.assertEqual(self.value(edited, "release_date"), ("2024-09-13", USER_MANUAL)) self.assertIn("痴女", self.tags(tagged)) backup = Path(self.db_path).with_name(result["backup"]) self.assertTrue(backup.is_file()) self.assertEqual(ledger_backups.describe(backup, "database")["purpose"], "撤回自动写入前") self.assertEqual([item["item_key"] for item in self.auto_landings()["items"]], [union]) def test_review_page_reverts_union_tags_like_the_script(self): tagged = self.asset("ROYD-083", tags=("中出内射",)) key = self.row("ROYD-083", "tags", "中出内射", ("makers", ["中出内射", "痴女"])) self.settle() preview = self.auto_landings(items=key) self.assertEqual([(row["code"], row["tag"]) for row in preview["tags"]], [("ROYD-083", "痴女")]) result = self.revert([key], preview["token"]) self.assertEqual((result["removed_tags"], result["decisions"]), (1, 1)) self.assertEqual(self.tags(tagged), {"中出内射"}) self.assertEqual(self.decisions(), {}) def test_review_page_refuses_writes_it_does_not_list(self): for items in ([], ["ROYD-083:tags"]): with self.subTest(items=items), self.assertRaises(ValueError): self.revert(items, "") def test_batch_time_reads_both_stamp_formats(self): self.assertEqual(web_auto_landings.batch_time(f"{rm_auto.VERIFIED_REPLACE_RULE}@2026-11-04T06:10:56Z"), 1793772656.0) self.assertIsNotNone(web_auto_landings.batch_time(f"{rm_auto.UNION_TAGS_SOURCE}@20261104T141053")) self.assertIsNone(web_auto_landings.batch_time("adr-0139-official-replaces-verified@now")) # ---- 看起来像但必须交人 ---- def assertForHuman(self, key, rule): self.assertEqual(self.outcome(key).rule, rule) self.assertIn(key, self.queue()) self.settle() self.assertIn(key, self.queue()) self.assertNotIn(key, self.decisions()) def test_three_sources_naming_three_different_performers_go_to_a_human(self): self.asset("ACC-006") with contextlib.closing(sqlite3.connect(self.db_path)) as connection, connection: upsert_asset_entity(connection, kind="performer", name="永沢咲良", asset_id=100, role="performer", source="javinizer:dmm:performer", now="now") key = self.row("ACC-006", "performers", "永沢咲良", ("javdb", [{"name": "有栖七菜"}]), ("r18dev", [{"name": "三浦沙綾"}])) self.assertForHuman(key, "adr-0139-official-performers") def test_maker_and_label_mixed_in_the_studio_field_go_to_a_human(self): self.asset("HOISW-002", owner=auto_owner("r18dev"), studio="素人ホイホイsweet!") key = self.row("HOISW-002", "studio", "素人ホイホイsweet!", ("javdb", "素人ホイホイsweet!/妄想族"), ("r18dev", "Fresh Gordon/Daydream Tribe"), ("dmm", "フレッシュ・ゴードン/妄想族"), ("avbase", "フレッシュ・ゴードン/妄想族"), ("javbus", "素人ホイホイsweet!/妄想族")) self.assertForHuman(key, "adr-0139-official-entity") def test_official_sources_naming_different_dates_go_to_a_human(self): self.asset("SSIS-776", release_date="2023-01-20") key = self.row("SSIS-776", "release_date", "2023-01-20", ("dmm", "2023-01-24"), ("r18dev", "2023-01-27")) self.assertForHuman(key, "adr-0139-official-disagree") def test_a_local_nfo_disagreeing_with_the_ledger_goes_to_a_human(self): self.asset("FSDSS-275", title="腰が砕けても逃がさない!") key = self.row("FSDSS-275", "title", "腰が砕けても逃がさない!", ("local_nfo", "ひたすら膣奥を貫く強制立ちバックハンドル")) self.assertForHuman(key, "adr-0139-nfo-dissent") def test_official_masked_studio_in_unknown_series_still_needs_a_human(self): self.asset("CAWD-241", owner=auto_owner("r18dev"), series="社内研修相部屋NTR") key = self.row("CAWD-241", "series", "社内研修相部屋NTR", ("makers", "童貞陰キャ男とプライド高い絶倫彼女")) self.assertForHuman(key, "adr-0139-official-entity") class CodeMatchingScanTests(unittest.TestCase): """整队列查番号走整表单遍,收下哪些行、先后顺序都与逐段 SQL 相同。""" def setUp(self): holder = tempfile.TemporaryDirectory() self.addCleanup(holder.cleanup) self.db_path = fresh_ledger(Path(holder.name)) self.next_id = 100 def assets(self, *codes, medium="video", disposal=None): with contextlib.closing(sqlite3.connect(self.db_path)) as connection, connection: for code in codes: connection.execute( "INSERT INTO asset(id,location,path,name,medium,code,disposal) " "VALUES(?,'local',?,?,?,?,?)", (self.next_id, f"/x/{self.next_id}.mp4", f"{self.next_id}.mp4", medium, code, disposal)) self.next_id += 1 def both_paths(self, codes, columns="id,code"): with contextlib.closing(sqlite3.connect(self.db_path)) as connection: connection.row_factory = sqlite3.Row scan = [tuple(row) for row in rm_auto._codes_matching(connection, codes, columns)] original = rm_auto._BULK_CODE_KEYS rm_auto._BULK_CODE_KEYS = 10 ** 9 try: sql = [tuple(row) for row in rm_auto._codes_matching(connection, codes, columns)] finally: rm_auto._BULK_CODE_KEYS = original return scan, sql def test_scan_matches_chunked_sql_across_chunks_and_code_spellings(self): # 同号异写、Tokyo-Hot 账本写法、日期式番号里 `_` 在 LIKE 里是通配符、首尾空格、 # 只靠精确写法命中(`ABC-1` 归一化成 `ABC-001`,宽查找不到它)、全角写法,以及 # 回收站与非视频的同号资产。填充番号把宽查键撑过两段。 self.assets("SSIS-308", "ssis308", "SSIS308", "TOKYO-HOT-N0762", "092415_001", "092415-001", " SSIS-401 ", "ABC-1", "ssis-500", "FC2PPV1234567", "FC2-PPV-1234567", None, "") self.assets("SSIS-308", disposal="trash") self.assets("SSIS-308", medium="image") filler = [f"FILL-{index:03d}" for index in range(1, 260)] self.assets(*filler[::7]) codes = ["SSIS-308", "n0762", "092415_001", "SSIS-401", "ABC-1", "SSIS-500", "FC2-PPV-1234567", "", " ", *filler] self.assertGreater(len(filler), rm_auto._LIKE_CHUNK) scan, sql = self.both_paths(codes) self.assertEqual(scan, sql) found = [code for _asset_id, code in scan] self.assertIn("TOKYO-HOT-N0762", found) self.assertIn("ABC-1", found) self.assertNotIn("092415-001", found) self.assertEqual(found.count("SSIS-308"), 1) self.assertIn("FILL-253", found) def test_scan_matches_sql_with_extra_columns(self): self.assets("SSIS-308", "SSNI-001", "IPX-100") codes = ["SSIS-308", "ssni001", *[f"MISS-{index:03d}" for index in range(30)]] scan, sql = self.both_paths(codes, "id,code,name,path,field_owners") self.assertEqual(scan, sql) self.assertEqual([row[1] for row in scan], ["SSIS-308", "SSNI-001"]) if __name__ == "__main__": unittest.main()