Ctrl K
ComunidadGuías…

tests/test_company_profiles.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""公司事实的来源、日期精度、填空与撤回。"""
import json
import sqlite3
import tempfile
import unittest
from contextlib import closing
from pathlib import Path
from types import SimpleNamespace

from peach import company_profiles
from peach.entities import upsert_asset_entity
from peach import web_contract
from support.ledger import fresh_ledger


class CompanyProfileTests(unittest.TestCase):
    def test_dates_keep_precision_and_reject_copyright_or_invalid_dates(self):
        for raw, expected in [('2002年5月', '2002-05'), ('1999年3月1日', '1999-03-01'), ('2016', '2016'), ('平成16年7月21日', '2004-07-21')]:
            self.assertEqual(company_profiles.date_text(raw), expected)
        for raw in ['© 2002', '20周年', '2025年2月30日', '2002年(ブランド発売)']:
            self.assertIsNone(company_profiles.date_text(raw))

    def test_official_tables_separate_company_and_brand_and_detect_conflicts(self):
        html = '<table><tr><th>設立</th><td>2002年5月</td></tr><tr><th>ブランド設立</th><td>2003年</td></tr><tr><th>主要取引先</th><td>WILL</td></tr></table><dl><dt>所在地</dt><dd>東京都中野区</dd></dl><a href="/company/">会社概要 Company</a>'
        parsed = company_profiles.extract(html, 'https://brand.example/')
        self.assertEqual(parsed['facts']['founded']['value'], '2002-05')
        self.assertEqual(parsed['facts']['launched']['value'], '2003')
        self.assertNotIn('parent', parsed['facts'])
        self.assertNotIn('country', parsed['facts'])
        self.assertEqual(parsed['company_pages'], ['https://brand.example/company/'])
        conflict = company_profiles.extract(html + '<dl><dt>設立</dt><dd>2001</dd></dl>', 'https://brand.example/')
        self.assertNotIn('founded', conflict['facts'])
        self.assertEqual(conflict['conflicts'][0]['field'], 'founded')

    def test_fill_protects_existing_facts_and_revert_preserves_metadata(self):
        temporary = tempfile.TemporaryDirectory()
        self.addCleanup(temporary.cleanup)
        connection = sqlite3.connect(fresh_ledger(Path(temporary.name).resolve()))
        self.addCleanup(connection.close)
        connection.execute("INSERT INTO entity(id,kind,canonical_name,normalized_name,metadata_json,created_at,updated_at) VALUES(1,'studio','Brand','brand','{\"provider\":\"catalog\"}','2026-10-07','2026-10-07')")
        fact = {'value': '2002', 'source_url': 'https://brand.example/company', 'evidence': '設立:2002年'}
        self.assertEqual(company_profiles.fill(connection, 1, {'founded': fact}, source='auto:company-profile', batch='batch1')['written'], ['founded'])
        conflict = company_profiles.fill(connection, 1, {'founded': {**fact, 'value': '2001'}}, source='auto:company-profile', batch='batch2')
        self.assertEqual(conflict, {'written': [], 'conflicts': ['founded']})
        self.assertEqual(company_profiles.planned_revert(connection, 'auto:company-profile', 'batch2'), [])
        rows = company_profiles.planned_revert(connection, 'auto:company-profile', 'batch1')
        self.assertEqual(company_profiles.revert(connection, rows), 1)
        self.assertEqual(json.loads(connection.execute('SELECT metadata_json FROM entity').fetchone()[0]), {'provider': 'catalog'})

    def test_group_company_tables_are_scoped_by_the_entity_names(self):
        html = '<table><tr><th>社名</th><td>ソフト・オン・デマンド株式会社</td></tr><tr><th>設立</th><td>1995年11月</td></tr></table><table><tr><th>社名</th><td>SODクリエイト株式会社</td></tr><tr><th>設立</th><td>1999年3月</td></tr></table>'
        result = company_profiles.extract_for_entity(html, 'https://group.example/about_us/info/', ['SOD Create', 'SODクリエイト'])
        self.assertEqual(result['facts']['founded']['value'], '1999-03')
        self.assertEqual(result['facts']['legal_name']['value'], 'SODクリエイト株式会社')
        unnamed = company_profiles.extract_for_entity(html, 'https://group.example/about_us/info/', ['SOD Create'])
        self.assertEqual(unnamed['facts'], {})

    def test_terms_operator_comes_from_the_page_sentence_and_listings_give_no_group(self):
        html = '<h1>Vixen Media Group</h1><p>OUR BRANDS Each studio is a world. Explore Vixen, Blacked, Blacked Raw, Tushy, and Deeper, all available on Vixen Plus</p>'
        self.assertEqual(company_profiles.page_facts(html, 'https://www.vixengroup.com/', ['Tushy'])['facts'], {})
        terms = '<p>These terms of service are entered into between you and General Media Systems, LLC (the Company).</p>'
        facts = company_profiles.page_facts(terms, 'https://www.anybrand.example/terms', ['Any'])['facts']
        self.assertEqual(facts['operator']['value'], 'General Media Systems, LLC')
        self.assertEqual(facts['operator']['evidence'], 'These terms of service are entered into between you and General Media Systems, LLC (')
        self.assertEqual(company_profiles.page_facts(terms, 'https://www.anybrand.example/news', ['Any'])['facts'], {})

    def test_group_statements_without_labels_are_not_observed(self):
        cases = [
            ('<p>SODグループ</p><p>SODクリエイト株式会社</p>', 'https://corporate.sod.co.jp/business/softondemand/', ['SOD Create']),
            ('<h2>メーカー一覧</h2><p>FALENO GROUP</p><p>素人CLOVER PROFILE</p>', 'https://falenogroup.com/makers/', ['素人CLOVER']),
            ('<dl><dt>2006</dt><dd>組織拡大の為、ティーパワーズ株式会社を設立</dd></dl>', 'https://www.t-powers.co.jp/company/', ['T-POWERS']),
        ]
        for html, url, names in cases:
            with self.subTest(url=url):
                self.assertEqual(company_profiles.page_facts(html, url, names)['facts'], {})

    def test_replay_reads_saved_pages_and_drops_cross_page_conflicts(self):
        temporary = tempfile.TemporaryDirectory()
        self.addCleanup(temporary.cleanup)
        root = Path(temporary.name)
        (root / 'a.html').write_text('<table><tr><th>設立</th><td>2002年5月</td></tr><tr><th>所在地</th><td>東京都</td></tr></table>', encoding='utf-8')
        (root / 'b.html').write_text('<dl><dt>設立</dt><dd>2001年</dd></dl>', encoding='utf-8')
        (root / 'c.html').write_text('<dl><dt>商号</dt><dd>株式会社ブランド</dd></dl>', encoding='utf-8')
        pages = [{'url': 'https://brand.example/', 'final_url': 'https://www.brand.example/company', 'http_status': 200, 'cache': str(root / 'a.html')},
                 {'url': 'https://brand.example/about', 'http_status': 404},
                 {'url': 'https://brand.example/x', 'final_url': 'https://brand.example/x', 'http_status': 200, 'cache': str(root / 'b.html')},
                 {'url': 'https://registry.example/brand', 'final_url': 'https://registry.example/brand', 'http_status': 200, 'cache': str(root / 'c.html')},
                 {'url': 'https://brand.example/y', 'final_url': 'https://brand.example/y', 'cache': str(root / 'c.html')}]
        result = company_profiles.replay(pages, ['Brand'], {'brand.example'})
        self.assertEqual(list(result['facts']), ['location'])
        self.assertEqual(result['facts']['location']['source_url'], 'https://www.brand.example/company')

    def test_observed_fact_replaces_a_candidate_and_revert_lists_candidates(self):
        temporary = tempfile.TemporaryDirectory()
        self.addCleanup(temporary.cleanup)
        connection = sqlite3.connect(fresh_ledger(Path(temporary.name).resolve()))
        self.addCleanup(connection.close)
        held = {'company_profile': {
            'group': {'value': 'Group', 'source_url': 'https://brand.example/', 'status': 'candidate', 'source': 'auto:company-profile', 'batch': 'old'},
            'location': {'value': '東京都', 'source_url': 'https://brand.example/', 'status': 'candidate', 'source': 'auto:company-profile', 'batch': 'old'}}}
        connection.execute("INSERT INTO entity(id,kind,canonical_name,normalized_name,metadata_json,created_at,updated_at) VALUES(1,'studio','Brand','brand',?,'2026-10-07','2026-10-07')",
                           (json.dumps(held),))
        self.assertEqual(company_profiles.planned_revert(connection, 'auto:company-profile', 'old'),
                         [{'entity_id': 1, 'entity': 'Brand', 'fields': ['group', 'location']}])
        fact = {'value': '大阪府', 'source_url': 'https://brand.example/company', 'evidence': '所在地:大阪府', 'status': 'observed'}
        self.assertEqual(company_profiles.fill(connection, 1, {'location': fact}, source='auto:company-profile', batch='new')['written'], ['location'])
        profile = json.loads(connection.execute('SELECT metadata_json FROM entity').fetchone()[0])['company_profile']
        self.assertEqual(profile['location']['value'], '大阪府')
        self.assertEqual(list(company_profiles.public_profile({'company_profile': profile})), ['location'])

    def test_landing_writes_only_replayed_facts_and_ignores_listed_aliases_and_links(self):
        from scripts import harvest_company_profiles as harvest

        temporary = tempfile.TemporaryDirectory()
        self.addCleanup(temporary.cleanup)
        root = Path(temporary.name).resolve()
        path = fresh_ledger(root)
        with sqlite3.connect(path) as connection:
            connection.execute("INSERT INTO entity(id,kind,canonical_name,normalized_name,created_at,updated_at) VALUES(1,'studio','Brand','brand','2026-10-07','2026-10-07')")
            connection.execute("INSERT INTO entity_link(entity_id,link_kind,label,url,hostname,created_at,updated_at) VALUES(1,'official','Brand','https://brand.example/','brand.example','2026-10-07','2026-10-07')")
        connection.close()
        (root / 'page.html').write_text('<table><tr><th>設立</th><td>2002年5月</td></tr></table>', encoding='utf-8')
        listed = {'value': 'Brand Group', 'source_url': 'https://brand.example/', 'evidence': 'Brand Group', 'status': 'observed'}
        plan = {'entities': [{'entity_id': 1, 'name': 'Brand', 'status': '已核查',
                              'facts': {'founded': {**listed, 'value': '2002-05'}, 'group': listed},
                              'pages': [{'url': 'https://brand.example/company', 'final_url': 'https://brand.example/company', 'http_status': 200, 'cache': str(root / 'page.html')}],
                              'aliases': [{'value': 'ブランド', 'source_url': 'https://brand.example/', 'status': 'observed'}],
                              'links': [{'entity_id': 1, 'link_kind': 'catalog', 'label': 'Brand', 'url': 'https://catalog.example/brand', 'status': 'observed'}]}]}
        (root / 'plan.json').write_text(json.dumps(plan, ensure_ascii=False), encoding='utf-8')
        args = SimpleNamespace(input=root / 'plan.json', db=path, apply=True, backup=root / 'backup.db',
                               batch='auto:company-profile@test', output=root / 'receipt.json')
        self.assertEqual(harvest.land(args), 0)
        with closing(sqlite3.connect(path)) as connection:
            profile = json.loads(connection.execute('SELECT metadata_json FROM entity WHERE id=1').fetchone()[0])['company_profile']
            self.assertEqual(list(profile), ['founded'])
            self.assertEqual(profile['founded']['evidence'], '設立:2002年5月')
            self.assertEqual(connection.execute('SELECT count(*) FROM entity_alias').fetchone()[0], 0)
            self.assertEqual(connection.execute('SELECT url FROM entity_link').fetchall(), [('https://brand.example/',)])
        change = json.loads((root / 'receipt.json').read_text(encoding='utf-8'))['changes'][0]
        self.assertEqual(change['not_replayed'], ['group'])
        self.assertEqual(change['ignored'], {'aliases': 1, 'links': 1})

    def test_repair_demotes_unreplayable_fields_and_drops_batch_aliases_and_links(self):
        from scripts import repair_company_profile_landing as repair

        temporary = tempfile.TemporaryDirectory()
        self.addCleanup(temporary.cleanup)
        root = Path(temporary.name).resolve()
        path = fresh_ledger(root)
        batch = 'auto:company-profile@old'
        stamp = {'source': 'auto:company-profile', 'batch': batch, 'status': 'observed', 'source_url': 'https://brand.example/company'}
        profile = {'founded': {**stamp, 'value': '2002-05', 'evidence': '設立:2002年5月'},
                   'group': {**stamp, 'value': 'Brand Group', 'evidence': 'Brand Group:Brand'},
                   'location': {'source': 'auto:company-profile', 'batch': 'other', 'status': 'observed', 'value': '東京都',
                                'source_url': 'https://brand.example/company'}}
        with closing(sqlite3.connect(path)) as connection, connection:
            connection.execute("INSERT INTO entity(id,kind,canonical_name,normalized_name,metadata_json,created_at,updated_at) VALUES(1,'studio','Brand','brand',?,'2026-10-07','2026-10-07')",
                               (json.dumps({'company_profile': profile}, ensure_ascii=False),))
            connection.execute("INSERT INTO entity_link(entity_id,link_kind,label,url,hostname,created_at,updated_at) VALUES(1,'official','Brand','https://brand.example/','brand.example','2026-10-07','2026-10-07')")
            connection.execute("INSERT INTO entity_link(entity_id,link_kind,label,url,hostname,metadata_json,created_at,updated_at) VALUES(1,'catalog','Brand','https://catalog.example/brand','catalog.example',?,'2026-10-07','2026-10-07')",
                               (json.dumps({'source': 'auto:company-profile', 'batch': batch}),))
            connection.execute("INSERT INTO entity_alias(entity_id,alias,normalized_alias,source) VALUES(1,'ブランド','ブランド',?)", (batch,))
        (root / 'page.html').write_text('<table><tr><th>設立</th><td>2002年5月</td></tr></table>', encoding='utf-8')
        plan = {'entities': [{'entity_id': 1, 'pages': [{'url': 'https://brand.example/company', 'final_url': 'https://brand.example/company',
                                                         'http_status': 200, 'cache': str(root / 'page.html')}]}]}
        (root / 'plan.json').write_text(json.dumps(plan), encoding='utf-8')
        common = ['--db', str(path), '--batch', batch, '--plan', str(root / 'plan.json'), '--output', str(root / 'receipt.json')]
        self.assertEqual(repair.main(common), 0)
        with closing(sqlite3.connect(path)) as connection:
            self.assertEqual(connection.execute('SELECT count(*) FROM entity_alias').fetchone()[0], 1)
        self.assertEqual(repair.main([*common, '--apply', '--backup', str(root / 'backup.db')]), 0)
        with closing(sqlite3.connect(path)) as connection:
            held = json.loads(connection.execute('SELECT metadata_json FROM entity').fetchone()[0])['company_profile']
            self.assertEqual({key: fact['status'] for key, fact in held.items()},
                             {'founded': 'observed', 'group': 'candidate', 'location': 'observed'})
            self.assertEqual(connection.execute('SELECT count(*) FROM entity_alias').fetchone()[0], 0)
            self.assertEqual(connection.execute('SELECT url FROM entity_link').fetchall(), [('https://brand.example/',)])
        self.assertEqual(json.loads((root / 'receipt.json').read_text(encoding='utf-8'))['summary'],
                         {'keep': 1, 'evidence': 0, 'candidate': 1, 'aliases': 1, 'links': 1})

    def test_resume_retries_rate_limited_entities(self):
        from scripts import harvest_company_profiles as harvest

        self.assertTrue(harvest.settled({'status': '已核查', 'pages': [{'http_status': 200}]}))
        self.assertTrue(harvest.settled({'status': '未取得', 'reason': '没有可访问的已确认官网', 'pages': []}))
        self.assertFalse(harvest.settled({'status': '已核查', 'pages': [{'http_status': 200}, {'http_status': 429}]}))
        self.assertFalse(harvest.settled({'status': '未取得', 'pages': [{'http_status': 403}]}))
        self.assertFalse(harvest.settled({'status': '未取得', 'pages': [{'reason': 'ConnectTimeout'}]}))

    def test_public_profile_requires_public_source_and_known_nonempty_values(self):
        facts = {key: {'value': value, 'source_url': url} for key, value, url in [('country','日本','https://brand.example/company'), ('operator','secret','https://localhost/admin'), ('founded','','https://brand.example/'), ('telephone','123','https://brand.example/')]}
        self.assertEqual(list(company_profiles.public_profile({'company_profile': facts})), ['country'])

    def test_company_profile_survives_catalog_ingest(self):
        temporary = tempfile.TemporaryDirectory()
        self.addCleanup(temporary.cleanup)
        root = Path(temporary.name).resolve()
        path = fresh_ledger(root)
        connection = sqlite3.connect(path)
        self.addCleanup(connection.close)
        connection.execute("INSERT INTO asset(id,location,path,name,medium) VALUES(1,'local','C:/test/a.mp4','a.mp4','video')")
        entity_id = upsert_asset_entity(connection, kind='studio', name='Brand', asset_id=1, role='studio', source='scan:filename')
        fact = {'value': '2002', 'source_url': 'https://brand.example/company'}
        company_profiles.fill(connection, entity_id, {'founded': fact}, source='auto:company-profile', batch='batch1')
        upsert_asset_entity(connection, kind='studio', name='Brand', asset_id=1, role='studio', source='scan:filename', metadata={'provider': 'fresh'})
        metadata = json.loads(connection.execute('SELECT metadata_json FROM entity WHERE id=?', (entity_id,)).fetchone()[0])
        self.assertEqual(metadata['provider'], 'fresh')
        self.assertEqual(metadata['company_profile']['founded']['value'], '2002')
        connection.commit()
        contract = web_contract.WebContract(path, avatar_root=root / 'avatars', logo_root=root / 'logos', cover_root=root / 'covers')
        response = web_contract.q_entity(contract, {'kind': 'studio', 'name': 'Brand'})
        self.assertEqual(response['company_profile']['founded']['value'], '2002')


if __name__ == '__main__':
    unittest.main()